Skip to content

Instantly share code, notes, and snippets.

@Terrance
Created May 21, 2021 15:02
Show Gist options
  • Save Terrance/0e21ef32801c846f07b28fc6fc3310e9 to your computer and use it in GitHub Desktop.
Save Terrance/0e21ef32801c846f07b28fc6fc3310e9 to your computer and use it in GitHub Desktop.
Script to convert from TickTick's backup CSV file to CalDAV-compatible VTODO files.
#!/usr/bin/env python3
from csv import DictReader
from datetime import datetime
from dateutil.rrule import rrulestr
from icalendar import Alarm, Calendar, Todo, vRecur
PRIORITY = {"0": "0", "1": "6", "3": "5", "5": "4"}
STATUS = {"0": "NEEDS-ACTION", "1": "COMPLETED", "2": "COMPLETED"}
def date(value):
value = value.replace("-", "").replace(":", "").replace("+0000", "Z")
assert value.endswith("Z")
return value
def main():
with open("backup.csv") as backup:
for _ in range(6):
next(backup)
reader = DictReader(backup)
for item in reader:
print(item["taskId"])
uid = "{}@ticktick.com.ics".format(item["taskId"])
todo = Todo()
todo["uid"] = uid
todo["created"] = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
todo["summary"] = item["Title"]
if item["Content"]:
todo["description"] = item["Content"].replace("\r", "\n")
if item["Is Check list"]:
todo["description"] = todo["description"].replace("▫", "- [ ] ").replace("▪", "- [x] ")
if item["Start Date"]:
todo["dtstart"] = date(item["Start Date"])
if item["Due Date"]:
todo["due"] = date(item["Due Date"])
for repeat in item["Repeat"].rstrip().splitlines():
todo.add("rrule", vRecur.from_ical(repeat))
todo["priority"] = PRIORITY[item["Priority"]]
todo["status"] = STATUS[item["Status"]]
todo["dtstamp"] = date(item["Created Time"])
if item["Completed Time"]:
todo["completed"] = date(item["Completed Time"])
cal = Calendar()
cal.add_component(todo)
with open("{}@ticktick.com.ics".format(item["taskId"]), "wb") as out:
out.write(cal.to_ical())
if __name__ == "__main__":
main()
@gymnae
Copy link

gymnae commented Jul 28, 2021

Ok, I tried to adapt your snippet to the rest of your python script, but the code below throws an error:

#!/usr/bin/env python3

from csv import DictReader
from datetime import datetime

from dateutil.rrule import rrulestr
from icalendar import Alarm, Calendar, Todo, vRecur
import os.path
from collections import defaultdict


PRIORITY = {"0": "0", "1": "6", "3": "5", "5": "4"}

STATUS = {"0": "NEEDS-ACTION", "1": "COMPLETED", "2": "COMPLETED"}


def date(value):
    value = value.replace("-", "").replace(":", "").replace("+0000", "Z")
    assert value.endswith("Z")
    return value


def main():
    cals = defaultdict(Calendar)
    with open("backup.csv") as backup:
        for _ in range(6):
            next(backup)
        reader = DictReader(backup)
        for item in reader:
            todo = Todo()
          #  todo["uid"] = uid
            todo["created"] = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
            todo["summary"] = item["Title"]
            if item["Content"]:
                todo["description"] = item["Content"].replace("\r", "\n")
                if item["Is Check list"]:
                    todo["description"] = todo["description"].replace("▫", "- [ ] ").replace("▪", "- [x] ")
            if item["Start Date"]:
                todo["dtstart"]     = date(item["Start Date"])
            if item["Due Date"]:
                todo["due"] = date(item["Due Date"])
            for repeat in item["Repeat"].rstrip().splitlines():
                todo.add("rrule", vRecur.from_ical(repeat))
            todo["priority"] = PRIORITY[item["Priority"]]
            todo["status"] =  STATUS[item["Status"]]
            todo["dtstamp"] = date(item["Created Time"])
            if item["Completed Time"]:
                todo["completed"] = date(item["Completed Time"])
            cal = cals[item["List Name"]]  # get or create a calendar for the current list
            cal.add_component(todo)
    for name, cal in cals:  # write calendars out at the end
        with open(name, "wb") as out:
            out.write(cal.to_ical())

if __name__ == "__main__":
   main()

I get the following result:

Traceback (most recent call last):
  File "ticktick2cal.py", line 56, in <module>
    main()
  File "ticktick2cal.py", line 51, in main
    for name, cal in cals:  # write calendars out at the end
ValueError: too many values to unpack (expected 2)

@Terrance
Copy link
Author

It's a dict, that should be cals.items().

@gymnae
Copy link

gymnae commented Jul 28, 2021

Wow, yeah, this code works now perfectly :)

This script creates one .ics file per list, each .ics file can be imported into Nextcloud, which in turns add the todos to the tasks app, which in turn is synced via webdav. With this, I can completely move away from ticktick without losing any current and historical todo.
Thank you, this is awesome.

Here's the working version:

#!/usr/bin/env python3

from csv import DictReader
from datetime import datetime

from dateutil.rrule import rrulestr
from icalendar import Alarm, Calendar, Todo, vRecur
import os.path
from collections import defaultdict


PRIORITY = {"0": "0", "1": "6", "3": "5", "5": "4"}

STATUS = {"0": "NEEDS-ACTION", "1": "COMPLETED", "2": "COMPLETED"}


def date(value):
    value = value.replace("-", "").replace(":", "").replace("+0000", "Z")
    assert value.endswith("Z")
    return value


def main():
    cals = defaultdict(Calendar)
    with open("backup.csv") as backup:
        for _ in range(6):
            next(backup)
        reader = DictReader(backup)
        for item in reader:
            uid = format(item["taskId"])
            todo = Todo()
            todo["uid"] = uid
            todo["created"] = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
            todo["summary"] = item["Title"]
            if item["Content"]:
                todo["description"] = item["Content"].replace("\r", "\n")
                if item["Is Check list"]:
                    todo["description"] = todo["description"].replace("▫", "- [ ] ").replace("▪", "- [x] ")
            if item["Start Date"]:
                todo["dtstart"]     = date(item["Start Date"])
            if item["Due Date"]:
                todo["due"] = date(item["Due Date"])
            for repeat in item["Repeat"].rstrip().splitlines():
                todo.add("rrule", vRecur.from_ical(repeat))
            todo["priority"] = PRIORITY[item["Priority"]]
            todo["status"] =  STATUS[item["Status"]]
            todo["dtstamp"] = date(item["Created Time"])
            if item["Completed Time"]:
                todo["completed"] = date(item["Completed Time"])
            cal = cals[item["List Name"]]  # get or create a calendar for the current list
            cal.add_component(todo)
    for name, cal in cals.items():  # write calendars out at the end
        listname = name + ".ics"
        print(listname)
        with open(listname, "wb") as out:
            out.write(cal.to_ical())

if __name__ == "__main__":
   main()

@LinuxMint20
Copy link

Wow, yeah, this code works now perfectly :)

This script creates one .ics file per list, each .ics file can be imported into Nextcloud, which in turns add the todos to the tasks app, which in turn is synced via webdav. With this, I can completely move away from ticktick without losing any current and historical todo. Thank you, this is awesome.

Here's the working version:

#!/usr/bin/env python3

from csv import DictReader
from datetime import datetime

from dateutil.rrule import rrulestr
from icalendar import Alarm, Calendar, Todo, vRecur
import os.path
from collections import defaultdict


PRIORITY = {"0": "0", "1": "6", "3": "5", "5": "4"}

STATUS = {"0": "NEEDS-ACTION", "1": "COMPLETED", "2": "COMPLETED"}


def date(value):
    value = value.replace("-", "").replace(":", "").replace("+0000", "Z")
    assert value.endswith("Z")
    return value


def main():
    cals = defaultdict(Calendar)
    with open("backup.csv") as backup:
        for _ in range(6):
            next(backup)
        reader = DictReader(backup)
        for item in reader:
            uid = format(item["taskId"])
            todo = Todo()
            todo["uid"] = uid
            todo["created"] = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
            todo["summary"] = item["Title"]
            if item["Content"]:
                todo["description"] = item["Content"].replace("\r", "\n")
                if item["Is Check list"]:
                    todo["description"] = todo["description"].replace("▫", "- [ ] ").replace("▪", "- [x] ")
            if item["Start Date"]:
                todo["dtstart"]     = date(item["Start Date"])
            if item["Due Date"]:
                todo["due"] = date(item["Due Date"])
            for repeat in item["Repeat"].rstrip().splitlines():
                todo.add("rrule", vRecur.from_ical(repeat))
            todo["priority"] = PRIORITY[item["Priority"]]
            todo["status"] =  STATUS[item["Status"]]
            todo["dtstamp"] = date(item["Created Time"])
            if item["Completed Time"]:
                todo["completed"] = date(item["Completed Time"])
            cal = cals[item["List Name"]]  # get or create a calendar for the current list
            cal.add_component(todo)
    for name, cal in cals.items():  # write calendars out at the end
        listname = name + ".ics"
        print(listname)
        with open(listname, "wb") as out:
            out.write(cal.to_ical())

if __name__ == "__main__":
   main()

how do you use it? I just copied it into a py script

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment