Last active
May 24, 2024 21:08
-
-
Save baptiste-roullin/29459f021d3d0989461a681052d467d1 to your computer and use it in GitHub Desktop.
Crude script to export Raindrop bookmarks and images into Eagle.app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Crude, offensively non-pythonic script to export Raindrop bookmarks in Eagle.app, as URL objects or media. | |
# Needs a CSV export from a Raindrop collection. https://help.raindrop.io/export/ | |
# The collection needs to be set to public. | |
import csv | |
from random import choice | |
import re | |
from sys import argv | |
from os import path, sep, mkdir | |
from datetime import datetime | |
import string | |
import requests | |
outputEncoding = "utf-8" | |
inputEncoding = "utf-8" | |
# the path to the IMAGES folder in your Eagle library. | |
libraryPath = "" | |
# beginning of the name of the folder of last item added to your Eagle Library, | |
# exemple: | |
# prefix = "LWJKQGX4" | |
# TODO: Automate this part | |
prefix = "" | |
chars = string.ascii_uppercase + string.digits | |
class PasswdDialect(csv.Dialect): | |
# field separator | |
delimiter = "," | |
# string separator | |
quotechar = '"' | |
escapechar = None | |
doublequote = True | |
lineterminator = "\n" | |
quoting = csv.QUOTE_MINIMAL | |
skipinitialspace = True | |
def retrieveFilePath(fileSendedByArg): | |
if path.isabs(fileSendedByArg): # is path absolute | |
return fileSendedByArg | |
else: | |
return path.dirname(path.abspath(__file__)) + sep + fileSendedByArg | |
def getInputFile(): | |
if len(argv) > 1: | |
csvInputFile = retrieveFilePath(argv[1]) | |
if not path.isfile(csvInputFile): | |
exit("the file " + csvInputFile + " doesn't exist") | |
print("Using :" + csvInputFile) | |
else: | |
print("You must provide path to an input file") | |
exit() | |
print("######################################") | |
print("# PARSING FILE : " + csvInputFile) | |
print("######################################") | |
return csvInputFile | |
def importItems(reader, count): | |
# The tricky, hacky, brittle part. | |
# From what I can tell, Eagle generate ids for each items this way | |
# [4 chars][4 chars][5 chars] | |
# The start seems linked to folders. | |
# For instance: LWJKQGX40BGQ4 | |
# We can add stuff directly to the library folder and it will be recognized IFF: | |
# the metadata.json inside the item folder has the proper format | |
# AND it finds any .url file in the item folder. | |
# AND the first two part of the id is the same as some item recently | |
# So we generate as many ids as we need. | |
# And a bit more to account for potential duplicates from collisions of ids. | |
# Didn't I say it was hacky? | |
countWithSpare = count + 20 | |
ids = [ | |
prefix + "".join([choice(chars) for i in range(5)]) | |
for j in range(countWithSpare) | |
] | |
# removing duplicates. | |
uids = set(ids) | |
for row in reader: | |
id = uids.pop() | |
title = re.sub("[,:'/\\\"\\n]", "", row["title"]) | |
note = row["note"] or row["excerpt"] | |
url = row["url"] | |
thumb = row["cover"] | |
created = str(row["created"])[0:10] | |
tags = str(row["tags"].replace("-", " ").replace(", ", ",").split(",")).replace( | |
"'", '"' | |
) | |
btime = str(datetime.fromisoformat(created).timestamp()).split(".0")[0] | |
# creating folder | |
folderPath = path.join(libraryPath, id + ".info") | |
ext = "url" | |
if not path.exists(folderPath): | |
mkdir(folderPath) | |
if url.startswith("https://api.raindrop"): | |
res = requests.get(url, timeout=5) | |
contentType = res.headers["Content-Type"] | |
if contentType.startswith("image"): | |
ext = contentType.replace("image/", "") | |
filename = title + "." + ext | |
with open(path.join(folderPath, filename), "wb") as handler: | |
handler.write(res.content) | |
elif contentType.startswith("video"): | |
ext = contentType.replace("video/", "") | |
filename = "test" + "." + ext | |
with open(path.join(folderPath, filename), "wb") as handler: | |
handler.write(res.content) | |
else: | |
with open(path.join(folderPath, title + ".url"), "a") as file: | |
file.write(f"[InternetShortcut]\nURL={url}") | |
# Not bothering with a real JSON manipulation lib. | |
with open(path.join(folderPath, "metadata.json"), "a") as file: | |
file.writelines( | |
[ | |
"{", | |
f'"id": "{id}",', | |
f'"name": "{title}",', | |
'"size": 56,', | |
f'"btime": {btime},', | |
'"mtime": 1716478593302,', | |
f'"ext": "{ext}",', | |
f'"tags": {tags},', | |
f'"folders": [],', | |
'"isDeleted": false,', | |
f'"url": "{url}",', | |
f'"annotation": "{note}",', | |
'"modificationTime": 1716478593300,', | |
'"height": 408,', | |
'"width": 720,', | |
'"lastModified": 1716479798004,', | |
'"palettes": [],', | |
f'"noThumbnail": false', | |
"}", | |
] | |
) | |
else: | |
# should not happen hashtag Famous Last Words. | |
print("folder already exists") | |
continue | |
csvInputFile = getInputFile() | |
with open(csvInputFile, mode="r", newline="", encoding=inputEncoding) as csvfile: | |
counter = csv.DictReader(csvfile, dialect=PasswdDialect()) | |
count = sum(1 for _ in counter) | |
csvfile.seek(0) | |
# TODO : read file only once. | |
reader = csv.DictReader(csvfile, dialect=PasswdDialect()) | |
importItems(reader, count) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment