Skip to content

Instantly share code, notes, and snippets.

@Terrance
Created May 21, 2021 15:02
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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 27, 2021

Hey there,
I'm trying to leave ticktick behind me, so I was happy to find your script.

The CSV contains more header information than you are currently consuming:

Folder Name,"List Name","Title","Tags","Content","Is Check list","Start Date","Due Date","Reminder","Repeat","Priority","Status","Created Time","Completed Time","Order","Timezone","Is All Day","Is Floating","Column Name","Column Order","View Mode","taskId","parentId"

I think "List Name" & "Title" could be helpful - even better would be a grouping by "List Name", so that if one has multiple list, the CSV gets parsed into grouped .ics files.

I wish I would know how to code to do this myself :)

@Terrance
Copy link
Author

I think "List Name" & "Title" could be helpful

The task title's already being used as the corresponding VTODO's title.

If you want a directory for each list (so you can import them into separate calendars), you could try (here assuming the list name is always filled in -- looks like non-list items are just in Inbox) replacing line 50 with:

path = os.path.join(item["List Name"], "{}@ticktick.com.ics".format(item["taskId"]))
with open(path, "wb") as out:

...and add import os.path near the top.

@gymnae
Copy link

gymnae commented Jul 27, 2021

Wow, super speedy answer :) So this works in a way: I need to create folders with the the titles of the lists first and then it saves the ics files for each individual task in there.

Would it be possible to have one consolidated .ics file per list, with the name of the list as a title? This would importing all tasks to my calendar, which creates the todos, a breeze.

BEGIN:VCALENDAR
<content with and inside VTODO tag of of a individual ics>
<content with and inside VTODO tag of of a individual ics>
<content with and inside VTODO tag of of a individual ics>
...
END:VCALENDAR

@Terrance
Copy link
Author

Terrance commented Jul 28, 2021

Then what you want is one calendar per list -- untested, but something like this:

from collections import defaultdict

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()
            ...
            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
        with open(name, "wb") as out:
            out.write(cal.to_ical())

@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