Skip to content

Commit cee85d3

Browse files
committed
feat: add Letterboxd import format export option
This commit adds: new extended_info option 'letterboxd', ExportLetterboxdFormat function, Docker support, and titles without double quotes for cleaner import.
1 parent 94dee2f commit cee85d3

File tree

7 files changed

+174
-9
lines changed

7 files changed

+174
-9
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,38 @@ The original work by u2pitchjami is also licensed under the MIT License. This fo
253253
- Twitter: [@0xUta](https://twitter.com/0xUta)
254254
- Github: [@JohanDevl](https://github.com/JohanDevl)
255255
- LinkedIn: [@johan-devlaminck](https://linkedin.com/in/johan-devlaminck)
256+
257+
## Letterboxd Import Export Format
258+
259+
A new export format has been added to generate files compatible with Letterboxd's import functionality. To use this feature:
260+
261+
1. Set `extended_info = "letterboxd"` in your `config.toml` file
262+
2. Run the application normally or with Docker (see below)
263+
264+
The format includes the following fields:
265+
266+
- Title: Movie title (quoted)
267+
- Year: Release year
268+
- imdbID: IMDB ID for the movie
269+
- tmdbID: TMDB ID for the movie
270+
- WatchedDate: Date the movie was watched
271+
- Rating10: Rating on a scale of 1-10
272+
- Rewatch: Whether the movie has been watched multiple times (true/false)
273+
274+
### Using with Docker
275+
276+
To use the Letterboxd export format with Docker:
277+
278+
```bash
279+
# Create directories for the Docker volumes
280+
mkdir -p config logs exports
281+
282+
# Copy the example config file and edit it
283+
cp config.example.toml config/config.toml
284+
285+
# Edit the config file to set extended_info = "letterboxd"
286+
# Then run:
287+
docker run --rm -v $(pwd)/config:/app/config -v $(pwd)/logs:/app/logs -v $(pwd)/exports:/app/exports johandevl/export-trakt-4-letterboxd:latest
288+
```
289+
290+
The output file will be saved as `letterboxd_import.csv` in your exports directory.

cmd/export_trakt/main.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,28 @@ func exportWatchedMovies(client *api.Client, exporter *export.LetterboxdExporter
9595

9696
log.Info("export.movies_retrieved", map[string]interface{}{"count": len(movies)})
9797

98-
// Export movies
98+
// If extended_info is set to "letterboxd", export in Letterboxd format
99+
if client.GetConfig().Trakt.ExtendedInfo == "letterboxd" {
100+
// Get ratings for Letterboxd format
101+
log.Info("export.retrieving_ratings", nil)
102+
ratings, err := client.GetRatings()
103+
if err != nil {
104+
log.Error("errors.api_request_failed", map[string]interface{}{"error": err.Error()})
105+
os.Exit(1)
106+
}
107+
108+
log.Info("export.ratings_retrieved", map[string]interface{}{"count": len(ratings)})
109+
110+
// Export in Letterboxd format
111+
log.Info("export.exporting_letterboxd_format", nil)
112+
if err := exporter.ExportLetterboxdFormat(movies, ratings); err != nil {
113+
log.Error("export.export_failed", map[string]interface{}{"error": err.Error()})
114+
os.Exit(1)
115+
}
116+
return
117+
}
118+
119+
// Export movies in standard format
99120
log.Info("export.exporting_watched_movies", nil)
100121
if err := exporter.ExportMovies(movies); err != nil {
101122
log.Error("export.export_failed", map[string]interface{}{"error": err.Error()})

config/config.example.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
# Trakt.tv API Configuration
22
[trakt]
3-
# Obtenir ces identifiants sur https://trakt.tv/oauth/applications
4-
# NOTE: Vous devez configurer ces valeurs pour que l'application fonctionne
3+
# Get these credentials at https://trakt.tv/oauth/applications
4+
# NOTE: You must configure these values for the application to work
55
client_id = "YOUR_CLIENT_ID"
66
client_secret = "YOUR_CLIENT_SECRET"
77
access_token = "YOUR_ACCESS_TOKEN"
88
api_base_url = "https://api.trakt.tv"
9+
# Options: "min", "full", "metadata", "letterboxd"
10+
# Use "letterboxd" to enable export in Letterboxd import CSV format
911
extended_info = "full"
1012

1113
# Letterboxd Export Configuration
1214
[letterboxd]
1315
export_dir = "exports"
14-
watched_filename = "trakt-watched-films.csv"
15-
collection_filename = "trakt-collection-films.csv"
16-
shows_filename = "trakt-shows.csv"
16+
# Optional: specify filenames for exports
17+
# watched_filename = "watched.csv"
18+
# collection_filename = "collection.csv"
19+
# shows_filename = "shows.csv"
1720

1821
# Export Settings
1922
[export]

locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
"retrieving_watchlist": "Retrieving movie watchlist from Trakt.tv",
3232
"watchlist_retrieved": "Retrieved {{.count}} movies from your Trakt.tv watchlist",
3333
"exporting_watchlist": "Exporting movie watchlist to Letterboxd format",
34-
"watchlist_export_complete": "Successfully exported {{.count}} watchlist movies to {{.path}}"
34+
"watchlist_export_complete": "Successfully exported {{.count}} watchlist movies to {{.path}}",
35+
"exporting_letterboxd_format": "Exporting in Letterboxd import format",
36+
"letterboxd_export_complete": "Successfully exported {{.count}} movies to Letterboxd import format at {{.path}}"
3537
},
3638
"errors": {
3739
"config_load_failed": "Failed to load configuration: {{.error}}",

locales/fr.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
"exporting_ratings": "Exportation des évaluations de films au format Letterboxd",
3030
"ratings_export_complete": "{{.count}} évaluations de films exportées avec succès vers {{.path}}",
3131
"retrieving_watchlist": "Récupération de la liste de surveillance depuis Trakt.tv",
32-
"watchlist_retrieved": "{{.count}} films récupérés depuis votre liste de surveillance Trakt.tv",
32+
"watchlist_retrieved": "Récupération de {{.count}} films de votre liste de surveillance Trakt.tv",
3333
"exporting_watchlist": "Exportation de la liste de surveillance au format Letterboxd",
34-
"watchlist_export_complete": "{{.count}} films de la liste de surveillance exportés avec succès vers {{.path}}"
34+
"watchlist_export_complete": "Exportation réussie de {{.count}} films de votre liste de surveillance vers {{.path}}",
35+
"exporting_letterboxd_format": "Exportation au format d'importation Letterboxd",
36+
"letterboxd_export_complete": "Exportation réussie de {{.count}} films au format d'importation Letterboxd vers {{.path}}"
3537
},
3638
"errors": {
3739
"config_load_failed": "Échec du chargement de la configuration : {{.error}}",

pkg/api/trakt.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,4 +519,9 @@ func (c *Client) GetWatchlist() ([]WatchlistMovie, error) {
519519
"count": len(watchlist),
520520
})
521521
return watchlist, nil
522+
}
523+
524+
// GetConfig returns the client's configuration
525+
func (c *Client) GetConfig() *config.Config {
526+
return c.config
522527
}

pkg/export/letterboxd.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)