Skip to content

Commit f0af010

Browse files
committed
fix: correct export mode behavior and cache refresh
- Fix rewatch logic in ExportMovieHistory to properly identify first vs subsequent viewings - Add cache invalidation after export completion to ensure new exports appear immediately - Add comprehensive test for movie history export with rewatch verification - Improve export history refresh functionality Fixes issues where: - Individual mode showed aggregated results and vice versa - New exports didn't appear in web interface without manual refresh
1 parent c2080e8 commit f0af010

File tree

3 files changed

+150
-6
lines changed

3 files changed

+150
-6
lines changed

pkg/export/letterboxd.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,23 @@ func (e *LetterboxdExporter) ExportMovieHistory(history []api.HistoryItem, apiCl
364364
}
365365

366366
// Track which movies we've seen to determine rewatch
367+
// Process from oldest to newest to correctly identify rewatches
367368
seenMovies := make(map[string]bool)
369+
rewatchMap := make(map[int]bool) // Map index to rewatch status
368370

369-
// Write history entries
370-
for _, item := range sortedHistory {
371+
// First pass: Process in reverse order (oldest first) to determine rewatch status
372+
for i := len(sortedHistory) - 1; i >= 0; i-- {
373+
item := sortedHistory[i]
374+
if seenMovies[item.Movie.IDs.IMDB] {
375+
rewatchMap[i] = true // This is a rewatch
376+
} else {
377+
rewatchMap[i] = false // First time watching this movie
378+
seenMovies[item.Movie.IDs.IMDB] = true
379+
}
380+
}
381+
382+
// Write history entries (in newest to oldest order)
383+
for i, item := range sortedHistory {
371384
// Parse watched date
372385
watchedDate := ""
373386
if item.WatchedAt != "" {
@@ -382,12 +395,10 @@ func (e *LetterboxdExporter) ExportMovieHistory(history []api.HistoryItem, apiCl
382395
rating = r
383396
}
384397

385-
// Determine if this is a rewatch
398+
// Determine if this is a rewatch using pre-calculated map
386399
rewatch := "false"
387-
if seenMovies[item.Movie.IDs.IMDB] {
400+
if rewatchMap[i] {
388401
rewatch = "true"
389-
} else {
390-
seenMovies[item.Movie.IDs.IMDB] = true
391402
}
392403

393404
// Convert TMDB ID to string

pkg/export/letterboxd_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,4 +835,123 @@ func TestGetTimeInConfigTimezone(t *testing.T) {
835835
assert.True(t, timeDiff.Seconds() < 5, "Time difference should be small")
836836
})
837837
}
838+
}
839+
840+
func TestExportMovieHistory(t *testing.T) {
841+
// Create a temporary directory for test exports
842+
tmpDir, err := os.MkdirTemp("", "letterboxd_history_test")
843+
if err != nil {
844+
t.Fatalf("Failed to create temp directory: %v", err)
845+
}
846+
defer os.RemoveAll(tmpDir)
847+
848+
// Create test configuration
849+
cfg := &config.Config{
850+
Letterboxd: config.LetterboxdConfig{
851+
ExportDir: tmpDir,
852+
},
853+
Export: config.ExportConfig{
854+
Format: "csv",
855+
DateFormat: "2006-01-02",
856+
},
857+
}
858+
log := &MockLogger{}
859+
860+
// Create test history with multiple viewings of the same movie
861+
testHistory := []api.HistoryItem{
862+
{
863+
Movie: api.MovieInfo{
864+
Title: "Test Movie",
865+
Year: 2020,
866+
IDs: api.MovieIDs{
867+
IMDB: "tt1234567",
868+
TMDB: 12345,
869+
},
870+
},
871+
WatchedAt: "2023-06-01T12:00:00Z", // First viewing (oldest)
872+
Action: "watch",
873+
},
874+
{
875+
Movie: api.MovieInfo{
876+
Title: "Test Movie",
877+
Year: 2020,
878+
IDs: api.MovieIDs{
879+
IMDB: "tt1234567",
880+
TMDB: 12345,
881+
},
882+
},
883+
WatchedAt: "2023-08-15T15:30:00Z", // Second viewing (rewatch)
884+
Action: "watch",
885+
},
886+
{
887+
Movie: api.MovieInfo{
888+
Title: "Another Movie",
889+
Year: 2021,
890+
IDs: api.MovieIDs{
891+
IMDB: "tt7654321",
892+
TMDB: 54321,
893+
},
894+
},
895+
WatchedAt: "2023-07-10T18:45:00Z", // Single viewing
896+
Action: "watch",
897+
},
898+
}
899+
900+
// Create exporter and export
901+
exporter := NewLetterboxdExporter(cfg, log)
902+
err = exporter.ExportMovieHistory(testHistory, nil)
903+
if err != nil {
904+
t.Fatalf("Failed to export movie history: %v", err)
905+
}
906+
907+
// Verify file exists
908+
filePath := filepath.Join(tmpDir, "watched-history-test.csv")
909+
if _, err := os.Stat(filePath); os.IsNotExist(err) {
910+
t.Fatalf("Export file was not created at %s", filePath)
911+
}
912+
913+
// Read and verify CSV content
914+
content, err := os.ReadFile(filePath)
915+
if err != nil {
916+
t.Fatalf("Failed to read export file: %v", err)
917+
}
918+
919+
// Parse CSV to verify rewatch logic
920+
reader := csv.NewReader(strings.NewReader(string(content)))
921+
records, err := reader.ReadAll()
922+
if err != nil {
923+
t.Fatalf("Failed to parse CSV: %v", err)
924+
}
925+
926+
// Should have header + 3 records
927+
if len(records) != 4 {
928+
t.Fatalf("Expected 4 records (header + 3 data), got %d", len(records))
929+
}
930+
931+
// Records should be sorted by date (newest first)
932+
// So order should be: Aug 15 (rewatch=true), Jul 10 (rewatch=false), Jun 01 (rewatch=false)
933+
934+
// August 15 - Test Movie second viewing (should be marked as rewatch)
935+
if records[1][0] != "Test Movie" || records[1][2] != "2023-08-15" {
936+
t.Fatalf("First record should be Test Movie 2023-08-15, got %s %s", records[1][0], records[1][2])
937+
}
938+
if records[1][6] != "true" {
939+
t.Fatalf("Second viewing should be marked as rewatch=true, got %s", records[1][6])
940+
}
941+
942+
// July 10 - Another Movie single viewing
943+
if records[2][0] != "Another Movie" || records[2][2] != "2023-07-10" {
944+
t.Fatalf("Second record should be Another Movie 2023-07-10, got %s %s", records[2][0], records[2][2])
945+
}
946+
if records[2][6] != "false" {
947+
t.Fatalf("Single viewing should be marked as rewatch=false, got %s", records[2][6])
948+
}
949+
950+
// June 1 - Test Movie first viewing (should be marked as not a rewatch)
951+
if records[3][0] != "Test Movie" || records[3][2] != "2023-06-01" {
952+
t.Fatalf("Third record should be Test Movie 2023-06-01, got %s %s", records[3][0], records[3][2])
953+
}
954+
if records[3][6] != "false" {
955+
t.Fatalf("First viewing should be marked as rewatch=false, got %s", records[3][6])
956+
}
838957
}

pkg/web/handlers/exports.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ type ExportCache struct {
6969
cacheTTL time.Duration
7070
}
7171

72+
// invalidateCache clears the cache to force a refresh
73+
func (h *ExportsHandler) invalidateCache() {
74+
h.cache.mu.Lock()
75+
defer h.cache.mu.Unlock()
76+
h.cache.exports = nil
77+
h.cache.lastScan = time.Time{}
78+
}
79+
7280
type ExportsHandler struct {
7381
config *config.Config
7482
logger logger.Logger
@@ -1013,6 +1021,12 @@ func (h *ExportsHandler) runExportAsync(exportID, exportType, historyMode string
10131021
"output": string(output),
10141022
})
10151023
}
1024+
1025+
// Invalidate cache to ensure new export appears in the list
1026+
h.invalidateCache()
1027+
h.logger.Info("web.export_cache_invalidated", map[string]interface{}{
1028+
"export_id": exportID,
1029+
})
10161030
}
10171031

10181032
// DownloadHandler handles file downloads

0 commit comments

Comments
 (0)