Skip to content

Commit a3117ce

Browse files
committed
update for setting genre
1 parent 409bc25 commit a3117ce

7 files changed

Lines changed: 114 additions & 56 deletions

File tree

README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ Automatically sync TMDb movie keywords as Plex labels - A lightweight Go applica
1616
- ⚙️ **Environment Configuration**: Fully configurable via environment variables
1717
- 🔒 **Protocol Flexibility**: Supports both HTTP and HTTPS Plex connections
1818
- Allows you to have TMBDB keywords as labels in Plex:
19-
![1](https://github.com/user-attachments/assets/914c5d32-1a90-4378-be3c-38679bc6263c)
19+
![1](example/labels.png)
2020
- Create custom dynamic filters for multiple labels that will update automatically when new movies are labeled:
21-
![2](https://github.com/user-attachments/assets/23ab5d2c-9300-4560-a626-31ed836c583c)
21+
![2](example/dynamic_filter.png)
2222
- Filter on the fly by a label:
2323

24-
![3](https://github.com/user-attachments/assets/886df494-83c5-4fff-862d-8f51152bd68c)
24+
![3](example/filter.png)
2525

2626
## 📋 Environment Variables
2727

@@ -35,6 +35,34 @@ Automatically sync TMDb movie keywords as Plex labels - A lightweight Go applica
3535
| `PROCESS_TIMER` | Processing interval (e.g., `5m`, `1h`) | `5m` | No |
3636
| `LIBRARY_ID` | Plex library ID (auto-detected if not set) | - | No |
3737
| `PROCESS_ALL_MOVIE_LIBRARIES` | Process all movie libraries (set to `true` to enable) | `false` | No |
38+
| `UPDATE_FIELD` | Field to update: `labels` (default) or `genre` | `labels` | No |
39+
40+
## 🆕 UPDATE_FIELD: Sync as Labels or Genres
41+
42+
You can control whether TMDb keywords are synced as Plex **labels** (default) or **genres** by setting the `UPDATE_FIELD` environment variable:
43+
44+
- `UPDATE_FIELD=labels` (default): Syncs keywords as Plex labels (original behavior)
45+
- `UPDATE_FIELD=genre`: Syncs keywords as Plex genres
46+
47+
The chosen field will be **locked** after update to prevent Plex from overwriting it.
48+
49+
### Example Usage
50+
51+
```bash
52+
docker run -d --name labelarr \
53+
-e PLEX_SERVER=localhost \
54+
-e PLEX_PORT=32400 \
55+
-e PLEX_TOKEN=your_plex_token_here \
56+
-e TMDB_READ_ACCESS_TOKEN=your_tmdb_read_access_token \
57+
-e UPDATE_FIELD=genre \
58+
nullableeth/labelarr:latest
59+
```
60+
61+
#### Example: Genres Updated and Locked in Plex
62+
63+
![Plex genres updated and locked by Labelarr](example/genre.png)
64+
65+
*Genres updated and locked by Labelarr using `UPDATE_FIELD=genre`. The lock icon indicates the field is protected from automatic changes by Plex.*
3866

3967
## 🔑 Getting API Keys
4068

@@ -50,7 +78,7 @@ Automatically sync TMDb movie keywords as Plex labels - A lightweight Go applica
5078

5179
1. Visit [TMDb API Settings](https://www.themoviedb.org/settings/api)
5280
2. Create account and generate API key
53-
3. Use the **Bearer Token** (not the API key)
81+
3. Use the Read Access Token (not the API key)
5482

5583
## 🐳 Docker Deployment
5684

README_DOCKER.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ docker run -d --name labelarr \
3737
| `PROCESS_TIMER` | How often to scan (e.g., `5m`) | `5m`, `10m`, `1h`, etc. |
3838
| `LIBRARY_ID` | Plex library ID (auto-detected if not set) | See Library Selection Logic below |
3939
| `PROCESS_ALL_MOVIE_LIBRARIES` | Process all movie libraries (set to `true` to enable) | `false` |
40+
| `UPDATE_FIELD` | Field to update: `labels` (default) or `genre` | `labels` | No |
4041

4142
## Docker Compose Example
4243

@@ -151,6 +152,27 @@ The application provides detailed logging including:
151152
- **Issues**: Report bugs and feature requests
152153
- **Logs**: Check container logs for troubleshooting
153154

155+
## 🆕 UPDATE_FIELD: Sync as Labels or Genres
156+
157+
You can control whether TMDb keywords are synced as Plex **labels** (default) or **genres** by setting the `UPDATE_FIELD` environment variable:
158+
159+
- `UPDATE_FIELD=labels` (default): Syncs keywords as Plex labels (original behavior)
160+
- `UPDATE_FIELD=genre`: Syncs keywords as Plex genres
161+
162+
The chosen field will be **locked** after update to prevent Plex from overwriting it.
163+
164+
### Example Usage
165+
166+
```bash
167+
docker run -d --name labelarr \
168+
-e PLEX_SERVER=localhost \
169+
-e PLEX_PORT=32400 \
170+
-e PLEX_TOKEN=your_plex_token_here \
171+
-e TMDB_READ_ACCESS_TOKEN=your_tmdb_read_access_token \
172+
-e UPDATE_FIELD=genre \
173+
nullableeth/labelarr:latest
174+
```
175+
154176
---
155177

156178
**Tags**: plex, tmdb, automation, movies, labels, docker, go, selfhosted

example/dynamic_filter.png

1.56 MB
Loading

example/filter.png

27.7 KB
Loading

example/genre.png

45.4 KB
Loading

example/labels.png

235 KB
Loading

main.go

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Movie struct {
3535
Title string `json:"title"`
3636
Year int `json:"year"`
3737
Label []Label `json:"Label,omitempty"`
38+
Genre []Genre `json:"Genre,omitempty"`
3839
Guid FlexibleGuid `json:"Guid,omitempty"`
3940
Media []Media `json:"Media,omitempty"`
4041
}
@@ -43,6 +44,10 @@ type Label struct {
4344
Tag string `json:"tag"`
4445
}
4546

47+
type Genre struct {
48+
Tag string `json:"tag"`
49+
}
50+
4651
type Guid struct {
4752
ID string `json:"id"`
4853
}
@@ -205,18 +210,25 @@ func main() {
205210
// Start the periodic processing
206211
fmt.Println("\n🔄 Starting periodic movie processing...")
207212

213+
// Add UPDATE_FIELD env variable
214+
updateField := getEnvWithDefault("UPDATE_FIELD", "labels")
215+
if updateField != "labels" && updateField != "genre" {
216+
fmt.Println("❌ UPDATE_FIELD must be 'labels' or 'genre'")
217+
os.Exit(1)
218+
}
219+
208220
processFunc := func() {
209221
if processAllMovieLibraries {
210222
for _, lib := range movieLibraries {
211223
fmt.Printf("\n==============================\n")
212224
fmt.Printf("🎬 Processing library: %s (ID: %s)\n", lib.Title, lib.Key)
213225
libConfig := config
214226
libConfig.LibraryID = lib.Key
215-
processAllMovies(libConfig)
227+
processAllMovies(libConfig, updateField)
216228
}
217229
} else {
218230
config.LibraryID = movieLibraries[0].Key
219-
processAllMovies(config)
231+
processAllMovies(config, updateField)
220232
}
221233
}
222234

@@ -236,7 +248,7 @@ func main() {
236248
}
237249
}
238250

239-
func processAllMovies(config Config) {
251+
func processAllMovies(config Config, updateField string) {
240252
fmt.Println("\n📋 Fetching all movies from library...")
241253
movies, err := getMoviesFromLibrary(config)
242254
if err != nil {
@@ -257,73 +269,71 @@ func processAllMovies(config Config) {
257269
skippedMovies := 0
258270

259271
for i, movie := range movies {
260-
// Check if movie was already processed
261272
processed, exists := processedMovies[movie.RatingKey]
262273
if exists && processed.KeywordsSynced {
263274
skippedMovies++
264275
continue
265276
}
266277

267-
// Extract TMDb ID from movie first (before any output)
268278
tmdbID := extractTMDbID(movie)
269279
if tmdbID == "" {
270280
fmt.Printf("⚠️ No TMDb ID found for movie: %s\n", movie.Title)
271281
continue
272282
}
273283

274-
// Get TMDb keywords
275284
keywords, err := getTMDbKeywords(config, tmdbID)
276285
if err != nil {
277286
fmt.Printf("❌ Error fetching TMDb keywords for %s: %v\n", movie.Title, err)
278287
continue
279288
}
280289

281-
// Fetch detailed movie information to get current labels
282290
movieDetails, err := getMovieDetails(config, movie.RatingKey)
283291
if err != nil {
284292
fmt.Printf("❌ Error fetching movie details for %s: %v\n", movie.Title, err)
285293
continue
286294
}
287295

288-
// Get current movie labels from detailed fetch
289-
currentLabels := make([]string, len(movieDetails.Label))
290-
for j, label := range movieDetails.Label {
291-
currentLabels[j] = label.Tag
296+
var currentValues []string
297+
if updateField == "labels" {
298+
currentValues = make([]string, len(movieDetails.Label))
299+
for j, label := range movieDetails.Label {
300+
currentValues[j] = label.Tag
301+
}
302+
} else {
303+
currentValues = make([]string, len(movieDetails.Genre))
304+
for j, genre := range movieDetails.Genre {
305+
currentValues[j] = genre.Tag
306+
}
292307
}
293308

294-
// Check if all keywords already exist as labels (case-insensitive)
295-
currentLabelsMap := make(map[string]bool)
296-
for _, label := range currentLabels {
297-
currentLabelsMap[strings.ToLower(label)] = true
309+
currentValuesMap := make(map[string]bool)
310+
for _, val := range currentValues {
311+
currentValuesMap[strings.ToLower(val)] = true
298312
}
299313

300314
allKeywordsExist := true
301315
for _, keyword := range keywords {
302-
if !currentLabelsMap[strings.ToLower(keyword)] {
316+
if !currentValuesMap[strings.ToLower(keyword)] {
303317
allKeywordsExist = false
304318
break
305319
}
306320
}
307321

308-
// If all keywords already exist, skip silently
309322
if allKeywordsExist {
310323
skippedMovies++
311324
continue
312325
}
313326

314-
// Only show processing output for movies that need updates
315327
fmt.Printf("\n🎬 Processing movie %d/%d: %s (%d)\n", i+1, len(movies), movie.Title, movie.Year)
316328
fmt.Printf("🔑 TMDb ID: %s (%s)\n", tmdbID, movie.Title)
317329
fmt.Printf("🏷️ Found %d TMDb keywords\n", len(keywords))
318330

319-
// Sync labels with keywords
320-
err = syncMovieLabelsWithKeywords(config, movie.RatingKey, currentLabels, keywords)
331+
err = syncMovieFieldWithKeywords(config, movie.RatingKey, currentValues, keywords, updateField)
321332
if err != nil {
322-
fmt.Printf("❌ Error syncing labels: %v\n", err)
333+
fmt.Printf("❌ Error syncing %s: %v\n", updateField, err)
323334
continue
324335
}
325336

326-
// Update processed movies dictionary
327337
processedMovies[movie.RatingKey] = &ProcessedMovie{
328338
RatingKey: movie.RatingKey,
329339
Title: movie.Title,
@@ -339,8 +349,6 @@ func processAllMovies(config Config) {
339349
}
340350

341351
fmt.Printf("✅ Successfully processed: %s\n", movie.Title)
342-
343-
// Small delay to avoid overwhelming the APIs
344352
time.Sleep(500 * time.Millisecond)
345353
}
346354

@@ -456,50 +464,50 @@ func getTMDbKeywords(config Config, tmdbID string) ([]string, error) {
456464
return keywords, nil
457465
}
458466

459-
func syncMovieLabelsWithKeywords(config Config, movieID string, currentLabels []string, keywords []string) error {
460-
// Convert current labels to a map for easy lookup (case-insensitive)
461-
currentLabelsMap := make(map[string]bool)
462-
for _, label := range currentLabels {
463-
currentLabelsMap[strings.ToLower(label)] = true
467+
func syncMovieFieldWithKeywords(config Config, movieID string, currentValues []string, keywords []string, updateField string) error {
468+
currentValuesMap := make(map[string]bool)
469+
for _, val := range currentValues {
470+
currentValuesMap[strings.ToLower(val)] = true
464471
}
465472

466-
// Find keywords to add (keywords not in current labels, case-insensitive comparison)
467-
labelsToAdd := make([]string, 0)
473+
valuesToAdd := make([]string, 0)
468474
for _, keyword := range keywords {
469-
if !currentLabelsMap[strings.ToLower(keyword)] {
470-
labelsToAdd = append(labelsToAdd, keyword)
475+
if !currentValuesMap[strings.ToLower(keyword)] {
476+
valuesToAdd = append(valuesToAdd, keyword)
471477
}
472478
}
473479

474-
fmt.Printf(" 📝 Labels to add: %v\n", labelsToAdd)
475-
fmt.Printf(" 🏷️ Existing labels: %v\n", currentLabels)
480+
fmt.Printf(" 📝 %s to add: %v\n", strings.Title(updateField), valuesToAdd)
481+
fmt.Printf(" 🏷️ Existing %s: %v\n", updateField, currentValues)
476482

477-
// Merge existing labels with new keywords
478-
allLabels := make([]string, 0, len(currentLabels)+len(labelsToAdd))
479-
allLabels = append(allLabels, currentLabels...)
480-
allLabels = append(allLabels, labelsToAdd...)
483+
allValues := make([]string, 0, len(currentValues)+len(valuesToAdd))
484+
allValues = append(allValues, currentValues...)
485+
allValues = append(allValues, valuesToAdd...)
481486

482-
// Update movie labels with combined list
483-
return updateMovieLabelsWithKeywords(config, movieID, allLabels)
487+
return updateMovieFieldWithKeywords(config, movieID, allValues, updateField)
484488
}
485489

486-
func updateMovieLabelsWithKeywords(config Config, movieID string, keywords []string) error {
487-
// Build the base URL path
490+
func updateMovieFieldWithKeywords(config Config, movieID string, keywords []string, updateField string) error {
488491
basePath := fmt.Sprintf("/library/sections/%s/all?type=1&id=%s&includeExternalMedia=1", config.LibraryID, movieID)
489492

490-
// Add each keyword as a label parameter
491-
for i, keyword := range keywords {
492-
encodedKeyword := url.QueryEscape(keyword)
493-
basePath += fmt.Sprintf("&label%%5B%d%%5D.tag.tag=%s", i, encodedKeyword)
493+
if updateField == "labels" {
494+
for i, keyword := range keywords {
495+
encodedKeyword := url.QueryEscape(keyword)
496+
basePath += fmt.Sprintf("&label%%5B%d%%5D.tag.tag=%s", i, encodedKeyword)
497+
}
498+
basePath += "&label.locked=1"
499+
} else {
500+
for i, keyword := range keywords {
501+
encodedKeyword := url.QueryEscape(keyword)
502+
basePath += fmt.Sprintf("&genre%%5B%d%%5D.tag.tag=%s", i, encodedKeyword)
503+
}
504+
basePath += "&genre.locked=1"
494505
}
506+
basePath += fmt.Sprintf("&X-Plex-Token=%s", config.PlexToken)
495507

496-
// Add label lock and token
497-
basePath += fmt.Sprintf("&label.locked=1&X-Plex-Token=%s", config.PlexToken)
498-
499-
// Build the complete URL
500508
updateURL := buildPlexURL(config, basePath)
501509

502-
fmt.Printf(" 📤 Updating movie labels...\n")
510+
fmt.Printf(" 📤 Updating movie %s...\n", updateField)
503511

504512
req, err := http.NewRequest("PUT", updateURL, nil)
505513
if err != nil {

0 commit comments

Comments
 (0)