Skip to content

Commit ad0b189

Browse files
committed
chore: preserve list items order
1 parent 1b9c1d9 commit ad0b189

File tree

6 files changed

+117
-80
lines changed

6 files changed

+117
-80
lines changed

internal/entities/common.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,29 @@ package entities
22

33
import (
44
"regexp"
5+
"slices"
56
"strings"
67
)
78

8-
func ListDifference(imdbList IMDbList, traktList TraktList) map[string]TraktItems {
9+
type Diff struct {
10+
Add []TraktItem
11+
Remove []TraktItem
12+
}
13+
14+
func newDiff() Diff {
15+
return Diff{
16+
Add: make([]TraktItem, 0),
17+
Remove: make([]TraktItem, 0),
18+
}
19+
}
20+
21+
func (d *Diff) Sort() {
22+
sortFunc := func(a, b TraktItem) int { return a.created.Compare(b.created) }
23+
slices.SortFunc(d.Add, sortFunc)
24+
slices.SortFunc(d.Remove, sortFunc)
25+
}
26+
27+
func ListDiff(imdbList IMDbList, traktList TraktList) Diff {
928
imdbItems := make(map[string]IMDbItem)
1029
for _, item := range imdbList.ListItems {
1130
imdbItems[item.ID] = item
@@ -21,24 +40,25 @@ func ListDifference(imdbList IMDbList, traktList TraktList) map[string]TraktItem
2140
return ItemsDifference(imdbItems, traktItems)
2241
}
2342

24-
func ItemsDifference(imdbItems map[string]IMDbItem, traktItems map[string]TraktItem) map[string]TraktItems {
25-
diff := make(map[string]TraktItems)
43+
func ItemsDifference(imdbItems map[string]IMDbItem, traktItems map[string]TraktItem) Diff {
44+
diff := newDiff()
2645
for id, imdbItem := range imdbItems {
2746
traktItem := imdbItem.toTraktItem()
2847
if _, found := traktItems[id]; !found {
29-
diff["add"] = append(diff["add"], traktItem)
48+
diff.Add = append(diff.Add, traktItem)
3049
continue
3150
}
3251
if imdbItem.Rating != nil && *imdbItem.Rating != traktItems[id].Rating {
33-
diff["add"] = append(diff["add"], traktItem)
34-
continue
52+
diff.Add = append(diff.Add, traktItem)
3553
}
3654
}
3755
for id, traktItem := range traktItems {
3856
if _, found := imdbItems[id]; !found {
39-
diff["remove"] = append(diff["remove"], traktItem)
57+
traktItem.created = imdbItems[id].Created
58+
diff.Remove = append(diff.Remove, traktItem)
4059
}
4160
}
61+
diff.Sort()
4262
return diff
4363
}
4464

internal/entities/imdb.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,23 @@ const (
1313
)
1414

1515
type IMDbItem struct {
16-
ID string
17-
Kind string
18-
Rating *int
19-
RatingDate *time.Time
16+
ID string
17+
Kind string
18+
Created time.Time
19+
Rating *int
2020
}
2121

2222
func (i *IMDbItem) toTraktItem() TraktItem {
23-
ti := TraktItem{}
23+
ti := TraktItem{
24+
created: i.Created,
25+
}
2426
tiSpec := TraktItemSpec{
2527
IDMeta: TraktIDMeta{
2628
IMDb: i.ID,
2729
},
2830
}
2931
if i.Rating != nil {
30-
ratedAt := i.RatingDate.UTC().String()
32+
ratedAt := i.Created.UTC().String()
3133
tiSpec.RatedAt = &ratedAt
3234
tiSpec.WatchedAt = &ratedAt
3335
tiSpec.Rating = i.Rating

internal/entities/trakt.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package entities
22

33
import (
44
"fmt"
5+
"time"
56
)
67

78
const (
@@ -65,6 +66,7 @@ type TraktItem struct {
6566
Show TraktItemSpec `json:"show,omitempty"`
6667
Episode TraktItemSpec `json:"episode,omitempty"`
6768
Person TraktItemSpec `json:"person,omitempty"`
69+
created time.Time
6870
}
6971

7072
type TraktItems []TraktItem

internal/syncer/syncer.go

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,20 @@ func NewSyncer(ctx context.Context, conf *appconfig.Config) (*Syncer, error) {
4343
logger: log,
4444
imdbClient: imdbClient,
4545
traktClient: traktClient,
46-
user: &user{
47-
imdbLists: make(map[string]entities.IMDbList, len(*conf.IMDb.Lists)),
48-
imdbRatings: make(map[string]entities.IMDbItem),
49-
traktLists: make(map[string]entities.TraktList, len(*conf.IMDb.Lists)),
50-
traktRatings: make(map[string]entities.TraktItem),
51-
},
52-
conf: conf.Sync,
53-
authless: *conf.IMDb.Auth == appconfig.IMDbAuthMethodNone,
46+
user: &user{},
47+
conf: conf.Sync,
48+
authless: *conf.IMDb.Auth == appconfig.IMDbAuthMethodNone,
5449
}
55-
for _, lid := range *conf.IMDb.Lists {
56-
syncer.user.imdbLists[lid] = entities.IMDbList{ListID: lid}
50+
if *conf.Sync.Ratings {
51+
syncer.user.imdbRatings = make(map[string]entities.IMDbItem)
52+
syncer.user.traktRatings = make(map[string]entities.TraktItem)
53+
}
54+
if *conf.Sync.Lists || *conf.Sync.Watchlist {
55+
syncer.user.imdbLists = make(map[string]entities.IMDbList, len(*conf.IMDb.Lists))
56+
syncer.user.traktLists = make(map[string]entities.TraktList, len(*conf.IMDb.Lists))
57+
for _, lid := range *conf.IMDb.Lists {
58+
syncer.user.imdbLists[lid] = entities.IMDbList{ListID: lid}
59+
}
5760
}
5861
return syncer, nil
5962
}
@@ -81,11 +84,9 @@ func (s *Syncer) Sync() error {
8184
}
8285

8386
func (s *Syncer) hydrate() error {
84-
lids := make([]string, len(s.user.imdbLists))
85-
var i int
87+
lids := make([]string, 0, len(s.user.imdbLists))
8688
for lid := range s.user.imdbLists {
87-
lids[i] = lid
88-
i++
89+
lids = append(lids, lid)
8990
}
9091
if *s.conf.Ratings {
9192
if err := s.imdbClient.RatingsExport(); err != nil {
@@ -183,52 +184,54 @@ func (s *Syncer) syncLists() error {
183184
}
184185
if !*s.conf.Lists {
185186
s.logger.Info("skipping lists sync")
187+
}
188+
if !*s.conf.Watchlist && !*s.conf.Lists {
186189
return nil
187190
}
188-
for _, list := range s.user.imdbLists {
189-
traktListSlug := entities.InferTraktListSlug(list.ListName)
190-
diff := entities.ListDifference(list, s.user.traktLists[list.ListID])
191-
if list.IsWatchlist {
192-
if len(diff["add"]) > 0 {
191+
for _, imdbList := range s.user.imdbLists {
192+
diff := entities.ListDiff(imdbList, s.user.traktLists[imdbList.ListID])
193+
if imdbList.IsWatchlist {
194+
if len(diff.Add) > 0 {
193195
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun {
194-
msg := fmt.Sprintf("sync mode %s would have added %d trakt list item(s)", syncMode, len(diff["add"]))
195-
s.logger.Info(msg, slog.Any("watchlist", diff["add"]))
196+
msg := fmt.Sprintf("sync mode %s would have added %d trakt list item(s)", syncMode, len(diff.Add))
197+
s.logger.Info(msg, slog.Any("watchlist", diff.Add))
196198
continue
197199
}
198-
if err := s.traktClient.WatchlistItemsAdd(diff["add"]); err != nil {
200+
if err := s.traktClient.WatchlistItemsAdd(diff.Add); err != nil {
199201
return fmt.Errorf("failure adding items to trakt watchlist: %w", err)
200202
}
201203
}
202-
if len(diff["remove"]) > 0 {
204+
if len(diff.Remove) > 0 {
203205
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun || syncMode == appconfig.SyncModeAddOnly {
204-
msg := fmt.Sprintf("sync mode %s would have deleted %d trakt list item(s)", syncMode, len(diff["remove"]))
205-
s.logger.Info(msg, slog.Any("watchlist", diff["remove"]))
206+
msg := fmt.Sprintf("sync mode %s would have deleted %d trakt list item(s)", syncMode, len(diff.Remove))
207+
s.logger.Info(msg, slog.Any("watchlist", diff.Remove))
206208
continue
207209
}
208-
if err := s.traktClient.WatchlistItemsRemove(diff["remove"]); err != nil {
210+
if err := s.traktClient.WatchlistItemsRemove(diff.Remove); err != nil {
209211
return fmt.Errorf("failure removing items from trakt watchlist: %w", err)
210212
}
211213
}
212214
continue
213215
}
214-
if len(diff["add"]) > 0 {
216+
slug := entities.InferTraktListSlug(imdbList.ListName)
217+
if len(diff.Add) > 0 {
215218
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun {
216-
msg := fmt.Sprintf("sync mode %s would have added %d trakt list item(s)", syncMode, len(diff["add"]))
217-
s.logger.Info(msg, slog.Any(traktListSlug, diff["add"]))
219+
msg := fmt.Sprintf("sync mode %s would have added %d trakt list item(s)", syncMode, len(diff.Add))
220+
s.logger.Info(msg, slog.Any(slug, diff.Add))
218221
continue
219222
}
220-
if err := s.traktClient.ListItemsAdd(traktListSlug, diff["add"]); err != nil {
221-
return fmt.Errorf("failure adding items to trakt list %s: %w", traktListSlug, err)
223+
if err := s.traktClient.ListItemsAdd(slug, diff.Add); err != nil {
224+
return fmt.Errorf("failure adding items to trakt list %s: %w", slug, err)
222225
}
223226
}
224-
if len(diff["remove"]) > 0 {
227+
if len(diff.Remove) > 0 {
225228
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun || syncMode == appconfig.SyncModeAddOnly {
226-
msg := fmt.Sprintf("sync mode %s would have deleted %d trakt list item(s)", syncMode, len(diff["remove"]))
227-
s.logger.Info(msg, slog.Any(traktListSlug, diff["remove"]))
229+
msg := fmt.Sprintf("sync mode %s would have deleted %d trakt list item(s)", syncMode, len(diff.Remove))
230+
s.logger.Info(msg, slog.Any(slug, diff.Remove))
228231
continue
229232
}
230-
if err := s.traktClient.ListItemsRemove(traktListSlug, diff["remove"]); err != nil {
231-
return fmt.Errorf("failure removing items from trakt list %s: %w", traktListSlug, err)
233+
if err := s.traktClient.ListItemsRemove(slug, diff.Remove); err != nil {
234+
return fmt.Errorf("failure removing items from trakt list %s: %w", slug, err)
232235
}
233236
}
234237
}
@@ -245,22 +248,22 @@ func (s *Syncer) syncRatings() error {
245248
return nil
246249
}
247250
diff := entities.ItemsDifference(s.user.imdbRatings, s.user.traktRatings)
248-
if len(diff["add"]) > 0 {
251+
if len(diff.Add) > 0 {
249252
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun {
250-
msg := fmt.Sprintf("sync mode %s would have added %d trakt rating item(s)", syncMode, len(diff["add"]))
251-
s.logger.Info(msg, slog.Any("ratings", diff["add"]))
253+
msg := fmt.Sprintf("sync mode %s would have added %d trakt rating item(s)", syncMode, len(diff.Add))
254+
s.logger.Info(msg, slog.Any("ratings", diff.Add))
252255
} else {
253-
if err := s.traktClient.RatingsAdd(diff["add"]); err != nil {
256+
if err := s.traktClient.RatingsAdd(diff.Add); err != nil {
254257
return fmt.Errorf("failure adding trakt ratings: %w", err)
255258
}
256259
}
257260
}
258-
if len(diff["remove"]) > 0 {
261+
if len(diff.Remove) > 0 {
259262
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun || syncMode == appconfig.SyncModeAddOnly {
260-
msg := fmt.Sprintf("sync mode %s would have deleted %d trakt rating item(s)", syncMode, len(diff["remove"]))
261-
s.logger.Info(msg, slog.Any("ratings", diff["remove"]))
263+
msg := fmt.Sprintf("sync mode %s would have deleted %d trakt rating item(s)", syncMode, len(diff.Remove))
264+
s.logger.Info(msg, slog.Any("ratings", diff.Remove))
262265
} else {
263-
if err := s.traktClient.RatingsRemove(diff["remove"]); err != nil {
266+
if err := s.traktClient.RatingsRemove(diff.Remove); err != nil {
264267
return fmt.Errorf("failure removing trakt ratings: %w", err)
265268
}
266269
}
@@ -281,21 +284,21 @@ func (s *Syncer) syncHistory() error {
281284
// the syncer will assume a user to have watched an item if they've submitted a rating for it
282285
// if the above is satisfied and the user's history for this item is empty, a new history entry is added!
283286
diff := entities.ItemsDifference(s.user.imdbRatings, s.user.traktRatings)
284-
if len(diff["add"]) > 0 {
287+
if len(diff.Add) > 0 {
285288
var historyToAdd entities.TraktItems
286-
for i := range diff["add"] {
287-
traktItemID, err := diff["add"][i].GetItemID()
289+
for i := range diff.Add {
290+
traktItemID, err := diff.Add[i].GetItemID()
288291
if err != nil {
289292
return fmt.Errorf("failure fetching trakt item id: %w", err)
290293
}
291-
history, err := s.traktClient.HistoryGet(diff["add"][i].Type, *traktItemID)
294+
history, err := s.traktClient.HistoryGet(diff.Add[i].Type, *traktItemID)
292295
if err != nil {
293-
return fmt.Errorf("failure fetching trakt history for %s %s: %w", diff["add"][i].Type, *traktItemID, err)
296+
return fmt.Errorf("failure fetching trakt history for %s %s: %w", diff.Add[i].Type, *traktItemID, err)
294297
}
295298
if len(history) > 0 {
296299
continue
297300
}
298-
historyToAdd = append(historyToAdd, diff["add"][i])
301+
historyToAdd = append(historyToAdd, diff.Add[i])
299302
}
300303
if len(historyToAdd) > 0 {
301304
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun {
@@ -308,21 +311,21 @@ func (s *Syncer) syncHistory() error {
308311
}
309312
}
310313
}
311-
if len(diff["remove"]) > 0 {
314+
if len(diff.Remove) > 0 {
312315
var historyToRemove entities.TraktItems
313-
for i := range diff["remove"] {
314-
traktItemID, err := diff["remove"][i].GetItemID()
316+
for i := range diff.Remove {
317+
traktItemID, err := diff.Remove[i].GetItemID()
315318
if err != nil {
316319
return fmt.Errorf("failure fetching trakt item id: %w", err)
317320
}
318-
history, err := s.traktClient.HistoryGet(diff["remove"][i].Type, *traktItemID)
321+
history, err := s.traktClient.HistoryGet(diff.Remove[i].Type, *traktItemID)
319322
if err != nil {
320-
return fmt.Errorf("failure fetching trakt history for %s %s: %w", diff["remove"][i].Type, *traktItemID, err)
323+
return fmt.Errorf("failure fetching trakt history for %s %s: %w", diff.Remove[i].Type, *traktItemID, err)
321324
}
322325
if len(history) == 0 {
323326
continue
324327
}
325-
historyToRemove = append(historyToRemove, diff["remove"][i])
328+
historyToRemove = append(historyToRemove, diff.Remove[i])
326329
}
327330
if len(historyToRemove) > 0 {
328331
if syncMode := *s.conf.Mode; syncMode == appconfig.SyncModeDryRun || syncMode == appconfig.SyncModeAddOnly {

pkg/client/imdb.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -635,9 +635,14 @@ func transformData(data []byte) ([]entities.IMDbItem, error) {
635635
)
636636
if isTitlesList(header) {
637637
for i, record := range records {
638+
created, err := time.Parse(time.DateOnly, record[2])
639+
if err != nil {
640+
return nil, fmt.Errorf("failure parsing created date: %w", err)
641+
}
638642
items[i] = entities.IMDbItem{
639-
ID: record[1],
640-
Kind: record[8],
643+
ID: record[1],
644+
Kind: record[8],
645+
Created: created,
641646
}
642647
}
643648
return items, nil
@@ -648,24 +653,29 @@ func transformData(data []byte) ([]entities.IMDbItem, error) {
648653
if err != nil {
649654
return nil, fmt.Errorf("failure parsing rating value to integer: %w", err)
650655
}
651-
ratingDate, err := time.Parse(time.DateOnly, record[2])
656+
created, err := time.Parse(time.DateOnly, record[2])
652657
if err != nil {
653-
return nil, fmt.Errorf("failure parsing rating date: %w", err)
658+
return nil, fmt.Errorf("failure parsing created date: %w", err)
654659
}
655660
items[i] = entities.IMDbItem{
656-
ID: record[0],
657-
Kind: record[6],
658-
Rating: &rating,
659-
RatingDate: &ratingDate,
661+
ID: record[0],
662+
Kind: record[6],
663+
Created: created,
664+
Rating: &rating,
660665
}
661666
}
662667
return items, nil
663668
}
664669
if isPeopleList(header) {
665670
for i, record := range records {
671+
created, err := time.Parse(time.DateOnly, record[2])
672+
if err != nil {
673+
return nil, fmt.Errorf("failure parsing created date: %w", err)
674+
}
666675
items[i] = entities.IMDbItem{
667-
ID: record[1],
668-
Kind: "Person",
676+
ID: record[1],
677+
Kind: "Person",
678+
Created: created,
669679
}
670680
}
671681
return items, nil

pkg/client/trakt.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ func (tc *TraktClient) ListsGet(idsMeta entities.TraktIDMetas) ([]entities.Trakt
531531
func (tc *TraktClient) ListAdd(listID, listName string) error {
532532
body, err := json.Marshal(entities.TraktListAddBody{
533533
Name: listName,
534-
Description: fmt.Sprintf("list auto imported from imdb by https://github.com/cecobask/imdb-trakt-sync on %v", time.Now().Format(time.RFC1123)),
534+
Description: fmt.Sprintf("List imported from IMDb using https://github.com/cecobask/imdb-trakt-sync on %v", time.Now().Format(time.RFC1123)),
535535
Privacy: "public",
536536
DisplayNumbers: false,
537537
AllowComments: true,

0 commit comments

Comments
 (0)