mirror of
https://github.com/hanzoskill/raindrop.git
synced 2026-10-30 21:03:40 +00:00
288 lines
9.3 KiB
Bash
288 lines
9.3 KiB
Bash
#!/usr/bin/env bash
|
|
# raindrop.sh - Raindrop.io CLI wrapper
|
|
# Requires: RAINDROP_TOKEN env var or --token flag
|
|
|
|
set -euo pipefail
|
|
|
|
API="https://api.raindrop.io/rest/v1"
|
|
TOKEN="${RAINDROP_TOKEN:-}"
|
|
FORMAT="pretty" # pretty or json
|
|
DELAY=0 # ms delay between requests for rate limiting
|
|
|
|
# Auto-source token from config if not set
|
|
if [[ -z "$TOKEN" && -f ~/.config/raindrop.env ]]; then
|
|
source ~/.config/raindrop.env
|
|
fi
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Raindrop.io Bookmarks CLI
|
|
|
|
Usage: raindrop.sh [OPTIONS] COMMAND [ARGS]
|
|
|
|
Commands:
|
|
whoami Show authenticated user info
|
|
collections List all collections
|
|
create-collection NAME Create a new collection
|
|
list [COLLECTION_ID] List bookmarks (default: 0 = all)
|
|
count [COLLECTION_ID] Count bookmarks in collection
|
|
search QUERY [COLLECTION] Search bookmarks
|
|
get ID Get bookmark by ID
|
|
add URL [COLLECTION_ID] Add bookmark (default: -1 = Unsorted)
|
|
delete ID Delete bookmark
|
|
move ID COLLECTION_ID Move bookmark to collection
|
|
update ID [OPTIONS] Update bookmark (--tags, --title, --collection)
|
|
bulk-move IDS COLLECTION Move multiple bookmarks (comma-separated IDs)
|
|
tags List all tags with counts
|
|
suggest URL Get AI-suggested tags/title for URL
|
|
list-untagged [COLLECTION] List bookmarks without tags
|
|
cache ID Get permanent copy (Pro only)
|
|
|
|
Options:
|
|
--token TOKEN API token (or set RAINDROP_TOKEN)
|
|
--json Output raw JSON
|
|
--limit N Max results (default: 25)
|
|
--delay MS Delay between API calls in ms (for bulk ops)
|
|
--page N Page number for pagination (0-indexed)
|
|
-h, --help Show this help
|
|
|
|
Update Options (for 'update' command):
|
|
--tags TAG1,TAG2 Set tags (comma-separated)
|
|
--title TITLE Set title
|
|
--collection ID Move to collection
|
|
|
|
Collection IDs:
|
|
0 = All bookmarks
|
|
-1 = Unsorted
|
|
-99 = Trash
|
|
N = Specific collection ID
|
|
|
|
Examples:
|
|
raindrop.sh collections
|
|
raindrop.sh create-collection "AI Coding"
|
|
raindrop.sh list -1 --limit 50 # List unsorted
|
|
raindrop.sh count -1 # Count unsorted
|
|
raindrop.sh move 12345 50436390 # Move to collection
|
|
raindrop.sh update 12345 --tags "ai,coding" --collection 50436390
|
|
raindrop.sh bulk-move "123,456,789" 50436390 --delay 100
|
|
raindrop.sh list-untagged -1 # Untagged in unsorted
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
die() { echo "Error: $1" >&2; exit 1; }
|
|
|
|
# Rate limiting helper
|
|
rate_limit() {
|
|
if [[ "$DELAY" -gt 0 ]]; then
|
|
sleep "$(echo "scale=3; $DELAY/1000" | bc)"
|
|
fi
|
|
}
|
|
|
|
LIMIT=25
|
|
PAGE=0
|
|
POSITIONAL=()
|
|
UPDATE_TAGS=""
|
|
UPDATE_TITLE=""
|
|
UPDATE_COLLECTION=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--token) TOKEN="$2"; shift 2 ;;
|
|
--json) FORMAT="json"; shift ;;
|
|
--limit) LIMIT="$2"; shift 2 ;;
|
|
--delay) DELAY="$2"; shift 2 ;;
|
|
--page) PAGE="$2"; shift 2 ;;
|
|
--tags) UPDATE_TAGS="$2"; shift 2 ;;
|
|
--title) UPDATE_TITLE="$2"; shift 2 ;;
|
|
--collection) UPDATE_COLLECTION="$2"; shift 2 ;;
|
|
-h|--help) usage ;;
|
|
*) POSITIONAL+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
set -- "${POSITIONAL[@]:-}"
|
|
|
|
[[ -z "$TOKEN" ]] && die "Missing token. Set RAINDROP_TOKEN or use --token"
|
|
|
|
cmd="${1:-}"; shift || true
|
|
|
|
api() {
|
|
local method="$1" endpoint="$2"
|
|
shift 2
|
|
rate_limit
|
|
curl -sf -X "$method" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
"$@" "${API}${endpoint}"
|
|
}
|
|
|
|
format_bookmarks() {
|
|
if [[ "$FORMAT" == "json" ]]; then
|
|
cat
|
|
else
|
|
jq -r '.items[] | "[\(.collectionId)] \(.title)\n \(.link)\n tags: \(.tags | join(", ") | if . == "" then "-" else . end)\n id: \(._id)\n"'
|
|
fi
|
|
}
|
|
|
|
format_collections() {
|
|
if [[ "$FORMAT" == "json" ]]; then
|
|
cat
|
|
else
|
|
jq -r '.items[] | "\(.title) (id: \(._id), count: \(.count))"'
|
|
fi
|
|
}
|
|
|
|
case "$cmd" in
|
|
whoami)
|
|
api GET "/user" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '.user | "User: \(.name)\nEmail: \(.email)\nPro: \(.pro)"'; fi
|
|
;;
|
|
|
|
collections)
|
|
api GET "/collections" | format_collections
|
|
;;
|
|
|
|
create-collection)
|
|
[[ -z "${1:-}" ]] && die "Collection name required"
|
|
name="$1"
|
|
api POST "/collection" -d "{\"title\":\"$name\"}" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '"Created: \(.item.title)\nID: \(.item._id)"'; fi
|
|
;;
|
|
|
|
list)
|
|
collection="${1:-0}"
|
|
api GET "/raindrops/${collection}?perpage=${LIMIT}&page=${PAGE}" | format_bookmarks
|
|
;;
|
|
|
|
count)
|
|
collection="${1:-0}"
|
|
if [[ "$collection" == "0" ]]; then
|
|
# For "all", get user stats
|
|
api GET "/user" | jq -r '.user.files.count'
|
|
else
|
|
# For specific collection, get collection info
|
|
if [[ "$collection" == "-1" ]]; then
|
|
# Unsorted - need to query and get count from response
|
|
api GET "/raindrops/-1?perpage=1" | jq -r '.count'
|
|
elif [[ "$collection" == "-99" ]]; then
|
|
api GET "/raindrops/-99?perpage=1" | jq -r '.count'
|
|
else
|
|
api GET "/collection/$collection" | jq -r '.item.count'
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
search)
|
|
[[ -z "${1:-}" ]] && die "Search query required"
|
|
query="$1"
|
|
collection="${2:-0}"
|
|
encoded=$(printf '%s' "$query" | jq -sRr @uri)
|
|
api GET "/raindrops/${collection}?search=${encoded}&perpage=${LIMIT}" | format_bookmarks
|
|
;;
|
|
|
|
get)
|
|
[[ -z "${1:-}" ]] && die "Bookmark ID required"
|
|
api GET "/raindrop/$1" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '.item | "Title: \(.title)\nURL: \(.link)\nCollection: \(.collectionId)\nTags: \(.tags | join(", "))\nCreated: \(.created)\nNote: \(.note // "-")\nCache: \(.cache.status // "none")"'; fi
|
|
;;
|
|
|
|
add)
|
|
[[ -z "${1:-}" ]] && die "URL required"
|
|
url="$1"
|
|
collection="${2:--1}"
|
|
api POST "/raindrop" -d "{\"link\":\"$url\",\"collectionId\":$collection}" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '"Added: \(.item.title)\nID: \(.item._id)"'; fi
|
|
;;
|
|
|
|
delete)
|
|
[[ -z "${1:-}" ]] && die "Bookmark ID required"
|
|
api DELETE "/raindrop/$1" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r 'if .result then "Deleted" else "Failed" end'; fi
|
|
;;
|
|
|
|
move)
|
|
[[ -z "${1:-}" ]] && die "Bookmark ID required"
|
|
[[ -z "${2:-}" ]] && die "Collection ID required"
|
|
id="$1"
|
|
collection="$2"
|
|
api PUT "/raindrop/$id" -d "{\"collectionId\":$collection}" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '"Moved: \(.item.title) -> collection \(.item.collectionId)"'; fi
|
|
;;
|
|
|
|
update)
|
|
[[ -z "${1:-}" ]] && die "Bookmark ID required"
|
|
id="$1"
|
|
|
|
# Build update payload
|
|
payload="{"
|
|
needs_comma=false
|
|
|
|
if [[ -n "$UPDATE_TAGS" ]]; then
|
|
# Convert comma-separated tags to JSON array
|
|
tags_json=$(echo "$UPDATE_TAGS" | tr ',' '\n' | jq -R . | jq -s .)
|
|
payload+='"tags":'"$tags_json"
|
|
needs_comma=true
|
|
fi
|
|
|
|
if [[ -n "$UPDATE_TITLE" ]]; then
|
|
[[ "$needs_comma" == "true" ]] && payload+=","
|
|
payload+='"title":"'"$UPDATE_TITLE"'"'
|
|
needs_comma=true
|
|
fi
|
|
|
|
if [[ -n "$UPDATE_COLLECTION" ]]; then
|
|
[[ "$needs_comma" == "true" ]] && payload+=","
|
|
payload+='"collectionId":'"$UPDATE_COLLECTION"
|
|
fi
|
|
|
|
payload+="}"
|
|
|
|
[[ "$payload" == "{}" ]] && die "No update options provided. Use --tags, --title, or --collection"
|
|
|
|
api PUT "/raindrop/$id" -d "$payload" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '"Updated: \(.item.title)\nTags: \(.item.tags | join(", "))\nCollection: \(.item.collectionId)"'; fi
|
|
;;
|
|
|
|
bulk-move)
|
|
[[ -z "${1:-}" ]] && die "Comma-separated bookmark IDs required"
|
|
[[ -z "${2:-}" ]] && die "Target collection ID required"
|
|
ids="$1"
|
|
target_collection="$2"
|
|
source_collection="${3:--1}" # Default to Unsorted (-1) if not specified
|
|
|
|
# Convert comma-separated IDs to JSON array
|
|
ids_json=$(echo "$ids" | tr ',' '\n' | jq -R 'tonumber' | jq -s .)
|
|
|
|
# Use bulk update API - requires source collection in path, target as collection.$id in body
|
|
api PUT "/raindrops/${source_collection}" -d "{\"ids\":$ids_json,\"collection\":{\"\$id\":$target_collection}}" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '"Moved \(.modified) bookmarks to collection '"$target_collection"'"'; fi
|
|
;;
|
|
|
|
tags)
|
|
api GET "/tags" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '.items[] | "\(._id) (\(.count))"'; fi
|
|
;;
|
|
|
|
suggest)
|
|
[[ -z "${1:-}" ]] && die "URL required"
|
|
url="$1"
|
|
encoded=$(printf '%s' "$url" | jq -sRr @uri)
|
|
api GET "/import/url/parse?url=${encoded}" | if [[ "$FORMAT" == "json" ]]; then cat; else jq -r '"Title: \(.item.title // "-")\nType: \(.item.type // "-")\nSuggested tags: \(.item.meta.tags // [] | join(", ") | if . == "" then "-" else . end)"'; fi
|
|
;;
|
|
|
|
list-untagged)
|
|
collection="${1:--1}"
|
|
# Search for bookmarks and filter those with empty tags
|
|
api GET "/raindrops/${collection}?perpage=${LIMIT}&page=${PAGE}" | \
|
|
if [[ "$FORMAT" == "json" ]]; then
|
|
jq '{items: [.items[] | select(.tags | length == 0)], count: .count}'
|
|
else
|
|
jq -r '.items[] | select(.tags | length == 0) | "[\(.collectionId)] \(.title)\n \(.link)\n id: \(._id)\n"'
|
|
fi
|
|
;;
|
|
|
|
cache)
|
|
[[ -z "${1:-}" ]] && die "Bookmark ID required"
|
|
curl -sL -H "Authorization: Bearer $TOKEN" "${API}/raindrop/$1/cache"
|
|
;;
|
|
|
|
""|help)
|
|
usage
|
|
;;
|
|
|
|
*)
|
|
die "Unknown command: $cmd. Run with --help for usage."
|
|
;;
|
|
esac
|