1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#!/usr/bin/env ruby
# frozen_string_literal: true
# Upload extracted Akkoma bookmarks to Raindrop.io
#
# Prerequisites:
# 1. Create a Raindrop.io app at https://app.raindrop.io/settings/integrations
# 2. Generate a "test token" (personal access token)
# 3. Set RAINDROP_TOKEN env var, or create a .raindrop_token file
#
# Usage:
# RAINDROP_TOKEN=xxx ruby upload_to_raindrop.rb [--dry-run] [--collection NAME]
#
# Each bookmark becomes a Raindrop entry linking to the original fedi post.
# The post content is stored as the excerpt, tags are preserved,
# and any URLs from the post content are listed in a note.
require "json"
require "net/http"
require "uri"
require "time"
BOOKMARKS_FILE = File.join(__dir__, "bookmarks.json")
# ── Config ──────────────────────────────────────────────────────────────────
def raindrop_token
ENV["RAINDROP_TOKEN"] ||
(File.read(File.join(__dir__, ".raindrop_token")).strip rescue nil) ||
abort("Set RAINDROP_TOKEN env var or create .raindrop_token file")
end
DRY_RUN = ARGV.include?("--dry-run")
COLLECTION_NAME = if (idx = ARGV.index("--collection"))
ARGV[idx + 1] || abort("--collection requires a name")
else
"Akkoma Bookmarks"
end
# ── Raindrop API helpers ────────────────────────────────────────────────────
API_BASE = URI("https://api.raindrop.io/rest/v1")
def api_request(method, path, body: nil)
uri = URI("#{API_BASE}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = case method
when :get then Net::HTTP::Get.new(uri)
when :post then Net::HTTP::Post.new(uri)
when :put then Net::HTTP::Put.new(uri)
end
req["Authorization"] = "Bearer #{raindrop_token}"
req["Content-Type"] = "application/json"
if body
req.body = JSON.generate(body)
end
response = http.request(req)
unless response.is_a?(Net::HTTPSuccess)
warn "API error #{response.code}: #{response.body}"
return nil
end
JSON.parse(response.body)
end
# ── Find or create collection ───────────────────────────────────────────────
def find_or_create_collection(name)
# List root collections
result = api_request(:get, "/collections")
return nil unless result
existing = result["items"]&.find { |c| c["title"] == name }
return existing["_id"] if existing
# Create it
result = api_request(:post, "/collection", body: {
title: name,
sort: 0, # manual sort
public: false,
view: "list",
})
result&.dig("item", "_id")
end
# ── Build raindrop payload ──────────────────────────────────────────────────
def build_raindrop(bookmark, collection_id)
link = bookmark["post_url"]
note = bookmark["content_text"]
tags = bookmark["tags"].dup
tags << "akkoma-bookmark" # easy to find them all later
created = Time.parse(bookmark["bookmarked_at"]).utc.iso8601
{
link: link,
note: note,
tags: tags,
collection: { "$id" => collection_id },
created: created,
pleaseParse: {}, # ask Raindrop to fetch metadata/favicon
}
end
# ── Main ────────────────────────────────────────────────────────────────────
bookmarks = JSON.parse(File.read(BOOKMARKS_FILE))
puts "Loaded #{bookmarks.size} bookmarks from #{BOOKMARKS_FILE}"
if DRY_RUN
puts "[DRY RUN] Would upload #{bookmarks.size} bookmarks to collection '#{COLLECTION_NAME}'"
bookmarks.each_with_index do |bm, i|
puts " #{i + 1}. #{bm["post_url"]}"
puts " #{bm["content_text"][0, 80]}..."
end
exit
end
collection_id = find_or_create_collection(COLLECTION_NAME)
abort "Failed to find/create collection '#{COLLECTION_NAME}'" unless collection_id
puts "Using collection '#{COLLECTION_NAME}' (id: #{collection_id})"
# Upload in batches — Raindrop supports bulk create via POST /rest/v1/raindrops
# (up to 100 items per request)
created = 0
failed = 0
bookmarks.each_slice(100).with_index do |batch, batch_idx|
items = batch.map { |bm| build_raindrop(bm, collection_id) }
puts "Uploading batch #{batch_idx + 1} (#{items.size} items)…"
result = api_request(:post, "/raindrops", body: { items: items })
if result && result["items"]
created += result["items"].size
puts " ✓ created #{result["items"].size} raindrops"
else
failed += batch.size
puts " ✗ batch failed"
end
sleep 1 # be polite to the API
end
puts
puts "Done! Created #{created} raindrops, #{failed} failed."