@@ -23,6 +23,7 @@ import (
2323 "errors"
2424 "io"
2525 "sort"
26+ "strconv"
2627 "strings"
2728 "time"
2829
@@ -44,32 +45,84 @@ type Migrator struct {
4445}
4546
4647type tickTickTask struct {
47- FolderName string `csv:"Folder Name"`
48- ProjectName string `csv:"List Name"`
49- Title string `csv:"Title"`
50- TagsList string `csv:"Tags"`
51- Tags []string `csv:"-"`
52- Content string `csv:"Content"`
53- IsChecklistString string `csv:"Is Check list"`
54- IsChecklist bool `csv:"-"`
55- StartDate tickTickTime `csv:"Start Date"`
56- DueDate tickTickTime `csv:"Due Date"`
57- ReminderDuration string `csv:"Reminder"`
58- Reminder time.Duration `csv:"-"`
59- Repeat string `csv:"Repeat"`
60- Priority int `csv:"Priority"`
61- Status string `csv:"Status"`
62- CreatedTime tickTickTime `csv:"Created Time"`
63- CompletedTime tickTickTime `csv:"Completed Time"`
64- Order float64 `csv:"Order"`
65- TaskID int64 `csv:"taskId"`
66- ParentID int64 `csv:"parentId"`
48+ FolderName string `csv:"Folder Name"`
49+ ProjectName string `csv:"List Name"`
50+ Title string `csv:"Title"`
51+ TagsList string `csv:"Tags"`
52+ Tags []string `csv:"-"`
53+ Content string `csv:"Content"`
54+ IsChecklistString string `csv:"Is Check list"`
55+ IsChecklist bool `csv:"-"`
56+ StartDate tickTickTime `csv:"Start Date"`
57+ DueDate tickTickTime `csv:"Due Date"`
58+ ReminderDuration string `csv:"Reminder"`
59+ Reminder time.Duration `csv:"-"`
60+ Repeat string `csv:"Repeat"`
61+ Priority tickTickPriority `csv:"Priority"`
62+ Status string `csv:"Status"`
63+ CreatedTime tickTickTime `csv:"Created Time"`
64+ CompletedTime tickTickTime `csv:"Completed Time"`
65+ Order float64 `csv:"Order"`
66+ TaskID tickTickNumber `csv:"taskId"`
67+ ParentID tickTickNumber `csv:"parentId"`
6768}
6869
6970type tickTickTime struct {
7071 time.Time
7172}
7273
74+ // tickTickNumber is an int64 that tolerates non-numeric or malformed values in
75+ // the numeric ID columns (taskId, parentId) of TickTick exports. Such values can
76+ // occur through column misalignment caused by unescaped delimiters, which would
77+ // otherwise make gocsv fail the entire import with an internal server error
78+ // (go-vikunja/vikunja#2822). Rather than aborting the import, we fall back to 0
79+ // for any value we cannot parse.
80+ type tickTickNumber int64
81+
82+ func (n * tickTickNumber ) UnmarshalCSV (csv string ) error {
83+ csv = strings .TrimSpace (csv )
84+ if csv == "" {
85+ * n = 0
86+ return nil
87+ }
88+ parsed , err := strconv .ParseInt (csv , 10 , 64 )
89+ if err != nil {
90+ // Deliberately ignore the parse error and fall back to 0 so a single
91+ // malformed value does not abort the whole import.
92+ * n = 0
93+ return nil //nolint:nilerr
94+ }
95+ * n = tickTickNumber (parsed )
96+ return nil
97+ }
98+
99+ // tickTickPriority parses the TickTick "Priority" column. TickTick exports the
100+ // priority either as a plain number (0, 1, 3, 5) or, in some exports, prefixed
101+ // with "p" (p1, p2, p3). We accept both forms and fall back to 0 (no priority)
102+ // for anything we cannot parse, so a stray value never fails the whole import
103+ // (go-vikunja/vikunja#2822). Vikunja's task priority is a free-form sortable
104+ // integer, so the parsed value is carried over as-is.
105+ type tickTickPriority int64
106+
107+ func (p * tickTickPriority ) UnmarshalCSV (csv string ) error {
108+ csv = strings .TrimSpace (csv )
109+ csv = strings .TrimPrefix (csv , "p" )
110+ csv = strings .TrimPrefix (csv , "P" )
111+ if csv == "" {
112+ * p = 0
113+ return nil
114+ }
115+ parsed , err := strconv .ParseInt (csv , 10 , 64 )
116+ if err != nil {
117+ // Deliberately ignore the parse error and fall back to 0 so a single
118+ // malformed value does not abort the whole import.
119+ * p = 0
120+ return nil //nolint:nilerr
121+ }
122+ * p = tickTickPriority (parsed )
123+ return nil
124+ }
125+
73126func (date * tickTickTime ) UnmarshalCSV (csv string ) (err error ) {
74127 date .Time = time.Time {}
75128 if csv == "" {
@@ -88,12 +141,12 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
88141// appears before any of its children. Tasks without a parent come first.
89142// The relative order of siblings / unrelated tasks is preserved.
90143func sortParentsBeforeChildren (tasks []* tickTickTask ) []* tickTickTask {
91- tasksByID := make (map [int64 ]* tickTickTask , len (tasks ))
144+ tasksByID := make (map [tickTickNumber ]* tickTickTask , len (tasks ))
92145 for _ , t := range tasks {
93146 tasksByID [t .TaskID ] = t
94147 }
95148
96- placed := make (map [int64 ]bool , len (tasks ))
149+ placed := make (map [tickTickNumber ]bool , len (tasks ))
97150 result := make ([]* tickTickTask , 0 , len (tasks ))
98151
99152 var place func (t * tickTickTask )
@@ -161,7 +214,7 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWi
161214
162215 task := & models.TaskWithComments {
163216 Task : models.Task {
164- ID : t .TaskID ,
217+ ID : int64 ( t .TaskID ) ,
165218 Title : t .Title ,
166219 Description : t .Content ,
167220 StartDate : t .StartDate .Time ,
@@ -170,6 +223,7 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWi
170223 Done : t .Status == "1" || t .Status == "2" ,
171224 DoneAt : t .CompletedTime .Time ,
172225 Position : t .Order ,
226+ Priority : int64 (t .Priority ),
173227 Labels : labels ,
174228 },
175229 }
@@ -185,7 +239,7 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWi
185239
186240 if t .ParentID != 0 {
187241 task .RelatedTasks = map [models.RelationKind ][]* models.Task {
188- models .RelationKindParenttask : {{ID : t .ParentID }},
242+ models .RelationKindParenttask : {{ID : int64 ( t .ParentID ) }},
189243 }
190244 }
191245
0 commit comments