Skip to content

Instantly share code, notes, and snippets.

@xyb
Last active November 13, 2024 08:13
Show Gist options
  • Save xyb/b2b950a2b0f1b522d94fd462a87b5df6 to your computer and use it in GitHub Desktop.
Save xyb/b2b950a2b0f1b522d94fd462a87b5df6 to your computer and use it in GitHub Desktop.
scripts to setup joplin web clipper api token and create new note
#!/usr/bin/env python3
"""
Joplin Note Creator
A command line tool to create notes in Joplin. Features:
- Create notes from stdin or clipboard
- Specify note title
- Create notes in specific folders (supports partial folder name matching)
Usage:
pbpaste | python jnote.py --title "Note Title"
pbpaste | python jnote.py --title "Note Title" --folder "Folder Name"
python jnote.py --list-folders
Options:
--title Required. The title for the new note
--folder Optional. The folder to create the note in. Supports partial matching
--list-folders List all available folders
Requirements:
- Joplin desktop app must be running
- Web Clipper Service must be enabled
- Joplin API token must be configured (use setup_joplin_token.py)
Configuration:
Token file: ~/.config/joplinapi/token
"""
import os
import sys
import json
import requests
def check_joplin_service():
"""Check if Joplin Web Clipper Service is running"""
try:
response = requests.get("http://127.0.0.1:41184/ping")
if response.status_code != 200 or response.text.strip() != "JoplinClipperServer":
print("Error: Joplin Web Clipper Service is not running.")
print("Please start Joplin and enable Web Clipper Service in:")
print("Tools > Options > Web Clipper > Enable Web Clipper Service")
sys.exit(1)
except requests.exceptions.ConnectionError:
print("Error: Cannot connect to Joplin Web Clipper Service.")
print("Please start Joplin and enable Web Clipper Service in:")
print("Tools > Options > Web Clipper > Enable Web Clipper service")
sys.exit(1)
def validate_token(token):
"""Validate token by making a test API call"""
url = f"http://127.0.0.1:41184/folders?token={token}"
try:
response = requests.get(url)
if response.status_code == 200:
return True
print("Error: Invalid Joplin API token.")
print("\nPlease run setup_joplin_token.py to configure your Joplin API token:")
print(" python setup_joplin_token.py")
return False
except requests.exceptions.RequestException as e:
print(f"Error validating token: {e}")
return False
def load_token():
"""Load Joplin API token from config file and validate connection"""
# First check if Joplin service is running
check_joplin_service()
# Then load and validate token
token_file = os.path.expanduser("~/.config/joplinapi/token")
try:
with open(token_file, "r") as f:
token = f.read().strip()
except FileNotFoundError:
print("Error: Joplin token file not found.")
print(f"Token file path: {token_file}")
print("\nPlease run setup_joplin_token.py to configure your Joplin API token:")
print(" python setup_joplin_token.py")
sys.exit(1)
except Exception as e:
print(f"Error reading token file: {e}")
print(f"Token file path: {token_file}")
print("\nPlease run setup_joplin_token.py to reconfigure your Joplin API token:")
print(" python setup_joplin_token.py")
sys.exit(1)
# Validate token
if not validate_token(token):
sys.exit(1)
return token
def get_folder_id_by_name(folder_name, token):
"""Get folder ID by partial name match (case-insensitive)
Returns (folder_id, error_message). If no match or multiple matches found,
folder_id will be None and error_message will contain the error details."""
url = f"http://localhost:41184/folders?token={token}"
response = requests.get(url)
if response.status_code == 200:
folders = response.json().get("items", [])
matches = []
search_term = folder_name.lower()
for folder in folders:
if search_term in folder["title"].lower():
matches.append(folder)
if len(matches) == 0:
return None, f"No folders found containing '{folder_name}'"
elif len(matches) > 1:
matching_names = ", ".join(f"'{f['title']}'" for f in matches)
return None, f"Multiple matching folders found: {matching_names}"
else:
return matches[0]["id"], None
return None, "Failed to retrieve folders"
def get_folder_name_by_id(folder_id, token):
"""Get folder name by ID"""
url = f"http://localhost:41184/folders/{folder_id}?token={token}"
response = requests.get(url)
if response.status_code == 200:
return response.json().get("title")
return None
def create_note(title, body, token, folder_id=None):
"""Create a new note in Joplin using the provided title, body, and optional folder_id"""
url = f"http://localhost:41184/notes?token={token}"
headers = {"Content-Type": "application/json"}
payload = {
"title": title,
"body": body
}
if folder_id:
payload["parent_id"] = folder_id
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
response_data = response.json()
if "id" in response_data:
folder_info = ""
parent_id = response_data.get("parent_id")
if parent_id:
folder_name = get_folder_name_by_id(parent_id, token)
if folder_name:
folder_info = f" in folder '{folder_name}'"
print(f"Note '{title}' created successfully{folder_info}.")
else:
print(f"Error creating note: {response_data}")
else:
print(f"Error creating note: {response.text}")
def list_folders(token):
"""List all folders in Joplin, sorted by name, and print their names and IDs"""
url = f"http://localhost:41184/folders?token={token}"
response = requests.get(url)
if response.status_code == 200:
folders = response.json().get("items", [])
# Sort folders by name
sorted_folders = sorted(folders, key=lambda folder: folder["title"].lower())
if sorted_folders:
print("Folders (sorted by name):")
for folder in sorted_folders:
print(f"Name: {folder['title']}, ID: {folder['id']}")
else:
print("No folders found.")
else:
print(f"Error retrieving folders: {response.text}")
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" pbpaste | python jnote.py --title 'new note' [--folder 'folder name']")
print(" python jnote.py --list-folders")
sys.exit(1)
token = load_token()
if sys.argv[1] == "--list-folders":
list_folders(token)
elif sys.argv[1] == "--title" and len(sys.argv) > 2:
title = sys.argv[2]
folder_id = None
# Check for folder argument
if "--folder" in sys.argv:
folder_index = sys.argv.index("--folder")
if folder_index + 1 < len(sys.argv):
folder_name = sys.argv[folder_index + 1]
folder_id, error_msg = get_folder_id_by_name(folder_name, token)
if error_msg:
print(f"Error: {error_msg}")
sys.exit(1)
body = sys.stdin.read()
create_note(title, body, token, folder_id)
else:
print("Invalid arguments.")
print("Usage:")
print(" pbpaste | python jnote.py --title 'new note' [--folder 'folder name']")
print(" python jnote.py --list-folders")
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
This script automatically obtains a Joplin Web Clipper token and saves it to
~/.config/joplinapi/token. It first checks if a valid token already exists, and
if so, uses it directly. If no valid token is found, the script initiates the
authorization process, waits for the user to approve access in Joplin, and then
saves the new token once approved.
Steps:
1. Check if the Joplin Web Clipper Service is running and reachable.
2. Check for an existing token and validate it.
3. If no valid token exists, request an auth token and prompt user authorization.
4. Once authorized, save the new token for future use.
Requirements:
- Joplin Web Clipper Service must be enabled and running on localhost:41184.
"""
import requests
import time
import os
import sys
def check_server_status():
"""Check if Joplin Web Clipper Service is running on localhost:41184"""
try:
response = requests.get("http://127.0.0.1:41184/ping")
if response.status_code == 200 and response.text == "JoplinClipperServer":
print("Joplin Web Clipper Service is running.")
else:
print(response)
print("The server on port 41184 is not Joplin. Please check your setup.")
sys.exit(1)
except requests.ConnectionError:
print("Joplin Web Clipper Service is not running. Please enable it and try again.")
sys.exit(1)
def load_existing_token():
"""Load existing token if it exists, otherwise return None"""
token_path = os.path.expanduser("~/.config/joplinapi/token")
if os.path.exists(token_path):
with open(token_path, "r") as f:
return f.read().strip()
return None
def check_token_validity(token):
"""Check if the token is valid"""
response = requests.get(f"http://localhost:41184/notes?token={token}")
return response.status_code == 200
def get_auth_token():
"""Request an auth token from Joplin"""
response = requests.post("http://localhost:41184/auth")
if response.status_code == 200 and "auth_token" in response.json():
return response.json()["auth_token"]
else:
raise Exception("Failed to obtain auth_token")
def check_auth_status(auth_token):
"""Continuously check authorization status until access token is received"""
url = f"http://localhost:41184/auth/check?auth_token={auth_token}"
while True:
response = requests.get(url)
if response.status_code == 200:
data = response.json()
if data["status"] == "accepted":
return data["token"]
elif data["status"] == "rejected":
print("Authorization rejected by the user in Joplin.")
sys.exit(1)
elif data["status"] == "waiting":
print("Waiting for authorization in Joplin...")
time.sleep(2)
else:
raise Exception("Unexpected status received")
else:
raise Exception("Failed to check auth status")
def save_token(token):
"""Save the token to the specified file"""
config_path = os.path.expanduser("~/.config/joplinapi")
os.makedirs(config_path, exist_ok=True)
token_path = os.path.join(config_path, "token")
with open(token_path, "w") as f:
f.write(token)
print(f"Token saved to {token_path}")
def main():
try:
# Check if Joplin Web Clipper Service is running
check_server_status()
# Check if a valid token already exists
existing_token = load_existing_token()
if existing_token and check_token_validity(existing_token):
print("Existing token is valid.")
print(f"Token: {existing_token}")
else:
print("No valid token found. Starting authorization process.")
auth_token = get_auth_token()
print(f"Auth token received: {auth_token}")
token = check_auth_status(auth_token)
print(f"Access token received: {token}")
save_token(token)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment