mirror of
https://github.com/vale981/wunderlist-to-org
synced 2025-03-04 08:41:41 -05:00
289 lines
7.2 KiB
Python
Executable file
289 lines
7.2 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
import sys
|
|
import datetime
|
|
import json
|
|
import io
|
|
from collections import namedtuple
|
|
from contextlib import contextmanager
|
|
import re
|
|
|
|
|
|
def _format_org_date(date):
|
|
return date.strftime("%Y-%m-%d %a %H:%M")
|
|
|
|
|
|
def sanitize_text(text):
|
|
star_re = re.compile(r"^\*+", flags=re.MULTILINE)
|
|
return star_re.sub(" +", text)
|
|
|
|
|
|
class OrgWriter:
|
|
def __init__(self, level=0):
|
|
self._level = level
|
|
self._org_string = ""
|
|
|
|
def emit(self, message="", newline=True):
|
|
"""Emit the message keep it simple for now."""
|
|
|
|
if newline:
|
|
message += "\n"
|
|
|
|
self._org_string += message
|
|
|
|
return self
|
|
|
|
def get_org_string(self):
|
|
return self._org_string
|
|
|
|
@contextmanager
|
|
def new_level(self, level=None):
|
|
if not level:
|
|
level = self._level + 1
|
|
|
|
old_level = self._level
|
|
|
|
self.set_level(level)
|
|
yield self._level
|
|
self.set_level(old_level)
|
|
|
|
@contextmanager
|
|
def drawer(self, name):
|
|
self.emit(f":{name.upper()}:")
|
|
yield self
|
|
self.emit(f":END:")
|
|
|
|
###########################################################################
|
|
# Fluent Interface #
|
|
###########################################################################
|
|
|
|
def raise_level(self):
|
|
self._level += 1
|
|
|
|
return self
|
|
|
|
def lower_level(self):
|
|
if self._level > 0:
|
|
self._level -= 1
|
|
|
|
return self
|
|
|
|
def set_level(self, level):
|
|
if level < 0:
|
|
raise RuntimeError("Heading levels have to be positive.")
|
|
|
|
self._level = level
|
|
|
|
return self
|
|
|
|
def emit_node_title(self, title, todo_state=None, tags=None):
|
|
out_title = "*" * (self._level + 1)
|
|
|
|
if todo_state:
|
|
out_title += f" {todo_state.upper()}"
|
|
|
|
out_title += f" {title}"
|
|
|
|
if tags:
|
|
for i, tag in enumerate(tags):
|
|
tags[i] = tag.upper()
|
|
|
|
out_title += f" :{':'.join(tags)}:"
|
|
|
|
return self.emit(out_title)
|
|
|
|
def emit_timestamp(self, date, timestamp_type=None, active=True, newline=True):
|
|
if not date:
|
|
return self
|
|
|
|
stamp = (
|
|
((self._level + 2) * " " + f"{timestamp_type.upper()}: ")
|
|
if timestamp_type
|
|
else ""
|
|
)
|
|
|
|
formated_date = _format_org_date(date)
|
|
stamp += f"<{formated_date}>" if active else f"[{formated_date}]"
|
|
|
|
return self.emit(stamp, newline)
|
|
|
|
def emit_node(
|
|
self,
|
|
title,
|
|
content=None,
|
|
tags=None,
|
|
timestamp=None,
|
|
timestamp_type=None,
|
|
todo_state=None,
|
|
):
|
|
self.emit_node_title(title, todo_state, tags)
|
|
self.emit_timestamp(timestamp, timestamp_type)
|
|
|
|
if content:
|
|
self.emit(content)
|
|
|
|
return self
|
|
|
|
def emit_content(self, content):
|
|
self.emit(sanitize_text(content))
|
|
return self.emit()
|
|
|
|
def emit_list_item(self, text):
|
|
return self.emit(f" - {text}")
|
|
|
|
def emit_property(self, name, value=None):
|
|
self.emit(f":{name.upper()}: ", newline=False)
|
|
|
|
return self.emit(value) if value else self
|
|
|
|
|
|
def convert_wunderlist(filename):
|
|
with io.open(filename, "r", encoding="utf-8-sig") as wunder_file:
|
|
wunder_data = json.loads(wunder_file.read())
|
|
|
|
if not wunder_data:
|
|
raise RuntimeError("Could not read wunderlist data.")
|
|
|
|
writer = OrgWriter()
|
|
|
|
for todo_list in wunder_data:
|
|
convert_wunderlist_list(writer, todo_list)
|
|
|
|
return writer.get_org_string()
|
|
|
|
|
|
def convert_wunderlist_list(writer, todo_list):
|
|
tags = []
|
|
if todo_list["folder"]:
|
|
tags.append(todo_list["folder"]["title"])
|
|
|
|
writer.emit_node_title(
|
|
todo_list["title"], tags=tags,
|
|
)
|
|
|
|
with writer.new_level():
|
|
for task in todo_list["tasks"]:
|
|
convert_wunderlist_task(writer, task)
|
|
|
|
|
|
def parse_wunderlist_date(date_str):
|
|
if not date_str:
|
|
return None
|
|
|
|
try:
|
|
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
except ValueError:
|
|
pass
|
|
|
|
try:
|
|
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
|
|
except ValueError:
|
|
pass
|
|
|
|
try:
|
|
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
|
|
except ValueError:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def convert_wunderlist_person(person):
|
|
return f"[[mailto:{person['email']}][{person['name']}]]"
|
|
|
|
|
|
def convert_wunderlist_title(title):
|
|
tag_re = re.compile(r"#(\S+?)(?:\s+|$)", re.MULTILINE)
|
|
tags = [tag for tag in tag_re.findall(title)]
|
|
|
|
return title, tags
|
|
|
|
|
|
def convert_wunderlist_task(writer, task):
|
|
title, tags = convert_wunderlist_title(task["title"])
|
|
writer.emit_node(
|
|
title,
|
|
todo_state="next"
|
|
if task["starred"]
|
|
else ("done" if task["completed"] else "todo"),
|
|
timestamp=parse_wunderlist_date(task["dueDate"]),
|
|
timestamp_type="deadline",
|
|
tags=tags,
|
|
)
|
|
|
|
with writer.drawer("properties"):
|
|
writer.emit_property("created-by", convert_wunderlist_person(task["createdBy"]))
|
|
|
|
if task["createdAt"]:
|
|
writer.emit_property("created")
|
|
writer.emit_timestamp(
|
|
parse_wunderlist_date(task["createdAt"]), active=False
|
|
)
|
|
|
|
if task["completedBy"]:
|
|
writer.emit_property(
|
|
"COMPLETED-BY", convert_wunderlist_person(task["completedBy"])
|
|
)
|
|
|
|
if task["completedAt"]:
|
|
writer.emit_property("completed")
|
|
writer.emit_timestamp(
|
|
parse_wunderlist_date(task["completedAt"]), active=False
|
|
)
|
|
|
|
if task["reminders"]:
|
|
writer.emit_property("reminders")
|
|
first = True
|
|
for reminder in task["reminders"]:
|
|
if first:
|
|
first = False
|
|
else:
|
|
writer.emit(" ", newline=False)
|
|
|
|
writer.emit_timestamp(
|
|
parse_wunderlist_date(reminder["remindAt"]), newline=False
|
|
)
|
|
|
|
writer.emit()
|
|
|
|
if task["assignee"]:
|
|
writer.emit_property(
|
|
"assignee", convert_wunderlist_person(task["assignee"])
|
|
)
|
|
|
|
for note in task["notes"]:
|
|
writer.emit_content(note["content"])
|
|
|
|
if task["comments"]:
|
|
with writer.new_level():
|
|
writer.emit_node_title("Comments")
|
|
for comment in task["comments"]:
|
|
convert_wunderlist_comment(writer, comment)
|
|
|
|
writer.emit()
|
|
|
|
if task["files"]:
|
|
with writer.new_level():
|
|
writer.emit_node_title("Files")
|
|
for w_file in task["files"]:
|
|
convert_wunderlist_file(writer, w_file)
|
|
|
|
writer.emit()
|
|
|
|
|
|
def convert_wunderlist_comment(writer, comment):
|
|
comment_text = convert_wunderlist_person(comment["author"])
|
|
comment_text += f": {comment['text']}"
|
|
writer.emit_list_item(comment_text)
|
|
|
|
|
|
def convert_wunderlist_file(writer, w_file):
|
|
writer.emit_list_item(f"[[file:{w_file['filePath']}][{w_file['fileName']}]]")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} [input-file]")
|
|
sys.exit(1)
|
|
|
|
print(convert_wunderlist(sys.argv[1]))
|