-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgoodlinks-exporter.py
173 lines (148 loc) · 5.84 KB
/
goodlinks-exporter.py
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import argparse
from csv import DictWriter
import json
from math import trunc
from os import path
from re import sub
import sys
def main():
# Configure command-line arguments
parser = argparse.ArgumentParser()
parser.add_argument(
"-d",
"--destination",
choices=["instapaper", "raindrop"],
help="destination format",
required=True,
)
parser.add_argument("filename", help="JSON file exported by GoodLinks")
args = parser.parse_args()
# Verify input file is a JSON
file, ext = path.splitext(args.filename)
if ext.lower() != ".json":
sys.exit("error: input file must have a .json extension")
# Verify input file exists
if path.exists(args.filename) is False:
sys.exit("error: input file not found")
# Call function to parse JSON and return a list of links
links = parse_json(args.filename)
# Send links to specified conversion function
format = args.destination
match format:
case "instapaper":
instapaper(links)
case "raindrop":
raindrop(links)
def parse_json(file):
"""
Parse a GoodLinks JSON file into a list.
:param file: JSON file exported by GoodLinks
:raise json.JSONDecodeError: if JSON format is invalid
:raise IOError: if file cannot be opened
:return: a list containing each object in the JSON array
:rtype: list
"""
try:
with open(file) as f:
try:
links = json.load(f)
except json.JSONDecodeError:
sys.exit("error: could not parse JSON file")
return links
except IOError:
sys.exit(f"error: could not open {file}")
def instapaper(links):
"""
Convert a list of links to an Instapaper CSV file.
The provided list of links is formatted for Instapaper and written to
a CSV file. Untagged links are marked for either the "Archive" or
"Unread" folders in Instapaper (depending on read status), tagged
links are stored in a folder corresponding to the first tag, and URLs
and timestamps are cleaned for compatibility.
:param links: a list of links to be writen to a CSV file
:type links: list
:raise IOError: if the file cannot be successfully opened for writing
:rtype: None
"""
filename = "instapaper-export.csv"
try:
with open(filename, "w") as file:
linkcount = 0
# Define CSV columns required by Instapaper's import tool
fields = ["URL", "Title", "Selection", "Folder", "Timestamp"]
writer = DictWriter(file, fieldnames=fields)
writer.writeheader()
for link in links:
# Determine what folder to save each link into
if "readAt" in link:
# If the link is read then archive it
folder = "Archive"
elif link["tags"] != []:
# If the link is tagged then put it in a folder named
# for the first tag.
folder = link["tags"][0]
else:
# Put unread and untagged links into the "Unread" folder
folder = "Unread"
# Clean up timestamp and URL
created = striptime(link["addedAt"])
url = cleanurl(link["url"])
# Write final link output to CSV row
writer.writerow({"URL": url, "Folder": folder, "Timestamp": created})
linkcount += 1
success(linkcount, filename)
except IOError:
sys.exit(f"error: could not write to {filename}")
def raindrop(links):
"""
Convert a list of links to a Raindrop.io CSV file.
The provided list of links is formatted for Instapaper and written to
a CSV file. Links are marked for either "Archive" or "Inbox" folders in
Raindrop.io, tags are converted from a list to a single string, and URLs
and timestamps are cleaned for compatibility.
:param links: a list of links to be written to a CSV file
:type links: list
:raise IOError: if the file cannot be successfully opened for writing
:rtype: None
"""
filename = "raindrop-export.csv"
try:
with open(filename, "w") as file:
linkcount = 0
# Define CSV columns required by Raindrop's import tool
fields = ["url", "folder", "title", "description", "tags", "created"]
writer = DictWriter(file, fieldnames=fields)
writer.writeheader()
for link in links:
# Determine what folder to save each link into
if "readAt" in link:
# If the link is read put it in an Archive folder
folder = "Archive"
else:
# Otherwise put it in an Inbox folder
folder = "Inbox"
# Concatenate tags into a single string
tags = ", ".join(link["tags"])
# Clean the timestamp and URL
created = striptime(link["addedAt"])
url = cleanurl(link["url"])
# Write final link output to CSV row
writer.writerow(
{"url": url, "folder": folder, "tags": tags, "created": created}
)
linkcount += 1
success(linkcount, filename)
except IOError:
sys.exit(f"error: could not write to {filename}")
def striptime(datetime):
"""Remove time suffix from Unix timestamps."""
return trunc(datetime)
def cleanurl(link):
"""Clean positional suffixes from saved URLs."""
return sub(r"\#.*", "", link)
def success(linkcount, filename):
"""Print a success summary message and exit with status 0."""
print(f"Successfully converted {linkcount} links and saved output as '{filename}'.")
sys.exit(0)
if __name__ == "__main__":
main()