@@ -361,4 +361,101 @@ func (e *LetterboxdExporter) ExportWatchlist(watchlist []api.WatchlistMovie) err
361361 "path" : filePath ,
362362 })
363363 return nil
364+ }
365+
366+ // ExportLetterboxdFormat exports the given movies to a CSV file in Letterboxd import format
367+ // The format matches the official Letterboxd import format with columns:
368+ // Title, Year, imdbID, tmdbID, WatchedDate, Rating10, Rewatch
369+ func (e * LetterboxdExporter ) ExportLetterboxdFormat (movies []api.Movie , ratings []api.Rating ) error {
370+ if err := os .MkdirAll (e .config .Letterboxd .ExportDir , 0755 ); err != nil {
371+ e .log .Error ("errors.export_dir_create_failed" , map [string ]interface {}{
372+ "error" : err .Error (),
373+ })
374+ return fmt .Errorf ("failed to create export directory: %w" , err )
375+ }
376+
377+ // Use configured filename, or generate one with timestamp if not specified
378+ filename := "letterboxd_import.csv"
379+ filePath := filepath .Join (e .config .Letterboxd .ExportDir , filename )
380+
381+ file , err := os .Create (filePath )
382+ if err != nil {
383+ e .log .Error ("errors.file_create_failed" , map [string ]interface {}{
384+ "error" : err .Error (),
385+ })
386+ return fmt .Errorf ("failed to create export file: %w" , err )
387+ }
388+ defer file .Close ()
389+
390+ writer := csv .NewWriter (file )
391+ defer writer .Flush ()
392+
393+ // Write header
394+ header := []string {"Title" , "Year" , "imdbID" , "tmdbID" , "WatchedDate" , "Rating10" , "Rewatch" }
395+ if err := writer .Write (header ); err != nil {
396+ return fmt .Errorf ("failed to write header: %w" , err )
397+ }
398+
399+ // Create a map of movie ratings for quick lookup
400+ movieRatings := make (map [string ]float64 )
401+ for _ , rating := range ratings {
402+ // Use IMDB ID as key for the ratings map
403+ if rating .Movie .IDs .IMDB != "" {
404+ movieRatings [rating .Movie .IDs .IMDB ] = rating .Rating
405+ }
406+ }
407+
408+ // Create a map to track plays for determining rewatches
409+ moviePlays := make (map [string ]int )
410+ for _ , movie := range movies {
411+ if movie .Movie .IDs .IMDB != "" {
412+ moviePlays [movie .Movie .IDs .IMDB ] += movie .Plays
413+ }
414+ }
415+
416+ // Write movies
417+ for _ , movie := range movies {
418+ // Parse watched date
419+ watchedDate := ""
420+ if movie .LastWatchedAt != "" {
421+ if parsedTime , err := time .Parse (time .RFC3339 , movie .LastWatchedAt ); err == nil {
422+ watchedDate = parsedTime .Format (e .config .Export .DateFormat )
423+ }
424+ }
425+
426+ // Get rating (scale is already 1-10 in Trakt)
427+ rating := ""
428+ if r , exists := movieRatings [movie .Movie .IDs .IMDB ]; exists {
429+ rating = strconv .FormatFloat (r , 'f' , 0 , 64 )
430+ }
431+
432+ // Determine if this is a rewatch
433+ rewatch := "false"
434+ if movie .Plays > 1 {
435+ rewatch = "true"
436+ }
437+
438+ // Convert TMDB ID to string
439+ tmdbID := strconv .Itoa (movie .Movie .IDs .TMDB )
440+
441+ record := []string {
442+ movie .Movie .Title ,
443+ strconv .Itoa (movie .Movie .Year ),
444+ movie .Movie .IDs .IMDB ,
445+ tmdbID ,
446+ watchedDate ,
447+ rating ,
448+ rewatch ,
449+ }
450+
451+ if err := writer .Write (record ); err != nil {
452+ return fmt .Errorf ("failed to write movie record: %w" , err )
453+ }
454+ }
455+
456+ e .log .Info ("export.letterboxd_export_complete" , map [string ]interface {}{
457+ "count" : len (movies ),
458+ "path" : filePath ,
459+ })
460+ return nil
364461}
0 commit comments