From 53078bf6ebf97b5dd00a6d1bcf4b8434c4d2adbf Mon Sep 17 00:00:00 2001 From: nasvidia Date: Sun, 2 Jun 2024 10:40:58 +0100 Subject: [PATCH 1/2] (feat) add playlist reading to movies page --- README.MD | 2 + plex/plex.go | 151 +++++++++++++++++++++++++++++++++++------ plex/plex_test.go | 16 ++++- web/movies/movies.go | 58 ++++++++-------- web/movies/movies.html | 35 +++------- web/server.go | 1 + 6 files changed, 188 insertions(+), 75 deletions(-) diff --git a/README.MD b/README.MD index e604d7b..f182e71 100644 --- a/README.MD +++ b/README.MD @@ -20,6 +20,7 @@ A tool to compare your plex libraries with other physical media rental / purchas - [x] amazon via blu-ray.com (customisable region) - [x] cinema paradiso - [x] filter by resolution, audio language or new releases + - [x] use playlists to filter what you search for - [x] TV - [x] amazon via blu-ray.com (customisable region) - [x] cinema paradiso @@ -28,6 +29,7 @@ A tool to compare your plex libraries with other physical media rental / purchas - [x] spotify (requires a client id and secret) - [x] musicbrainz (can use a local copy of the database) - [x] find new releases, or find similar new artists + - [x] use playlists to filter what you search for - [x] Runs locally - [x] no data is stored - [x] no ads diff --git a/plex/plex.go b/plex/plex.go index 4e671ef..afd3bd1 100644 --- a/plex/plex.go +++ b/plex/plex.go @@ -726,6 +726,101 @@ type MusicPlayList struct { } `xml:"Track"` } +type MoviePlaylist struct { + XMLName xml.Name `xml:"MediaContainer"` + Text string `xml:",chardata"` + Size string `xml:"size,attr"` + Composite string `xml:"composite,attr"` + Duration string `xml:"duration,attr"` + LeafCount string `xml:"leafCount,attr"` + PlaylistType string `xml:"playlistType,attr"` + RatingKey string `xml:"ratingKey,attr"` + Smart string `xml:"smart,attr"` + Title string `xml:"title,attr"` + Video []struct { + Text string `xml:",chardata"` + RatingKey string `xml:"ratingKey,attr"` + Key string `xml:"key,attr"` + GUID string `xml:"guid,attr"` + Studio string `xml:"studio,attr"` + Type string `xml:"type,attr"` + Title string `xml:"title,attr"` + LibrarySectionTitle string `xml:"librarySectionTitle,attr"` + LibrarySectionID string `xml:"librarySectionID,attr"` + LibrarySectionKey string `xml:"librarySectionKey,attr"` + ContentRating string `xml:"contentRating,attr"` + Summary string `xml:"summary,attr"` + Rating string `xml:"rating,attr"` + AudienceRating string `xml:"audienceRating,attr"` + ViewCount string `xml:"viewCount,attr"` + LastViewedAt string `xml:"lastViewedAt,attr"` + Year string `xml:"year,attr"` + Tagline string `xml:"tagline,attr"` + Thumb string `xml:"thumb,attr"` + Art string `xml:"art,attr"` + Duration string `xml:"duration,attr"` + OriginallyAvailableAt string `xml:"originallyAvailableAt,attr"` + AddedAt string `xml:"addedAt,attr"` + UpdatedAt string `xml:"updatedAt,attr"` + AudienceRatingImage string `xml:"audienceRatingImage,attr"` + ChapterSource string `xml:"chapterSource,attr"` + PrimaryExtraKey string `xml:"primaryExtraKey,attr"` + RatingImage string `xml:"ratingImage,attr"` + TitleSort string `xml:"titleSort,attr"` + OriginalTitle string `xml:"originalTitle,attr"` + Slug string `xml:"slug,attr"` + SkipCount string `xml:"skipCount,attr"` + Media []struct { + Text string `xml:",chardata"` + ID string `xml:"id,attr"` + Duration string `xml:"duration,attr"` + Bitrate string `xml:"bitrate,attr"` + Width string `xml:"width,attr"` + Height string `xml:"height,attr"` + AspectRatio string `xml:"aspectRatio,attr"` + AudioChannels string `xml:"audioChannels,attr"` + AudioCodec string `xml:"audioCodec,attr"` + VideoCodec string `xml:"videoCodec,attr"` + VideoResolution string `xml:"videoResolution,attr"` + Container string `xml:"container,attr"` + VideoFrameRate string `xml:"videoFrameRate,attr"` + VideoProfile string `xml:"videoProfile,attr"` + AudioProfile string `xml:"audioProfile,attr"` + Part struct { + Text string `xml:",chardata"` + ID string `xml:"id,attr"` + Key string `xml:"key,attr"` + Duration string `xml:"duration,attr"` + File string `xml:"file,attr"` + Size string `xml:"size,attr"` + Container string `xml:"container,attr"` + VideoProfile string `xml:"videoProfile,attr"` + AudioProfile string `xml:"audioProfile,attr"` + } `xml:"Part"` + } `xml:"Media"` + Genre []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Genre"` + Country []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Country"` + Director []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Director"` + Writer []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Writer"` + Role []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Role"` + } `xml:"Video"` +} + type Filter struct { Name string Value string @@ -734,22 +829,6 @@ type Filter struct { func GetPlexMovies(ipAddress, libraryID, plexToken string, filters []Filter) (movieList []types.PlexMovie) { url := fmt.Sprintf("http://%s:32400/library/sections/%s/all", ipAddress, libraryID) - //nolint:gocritic - // EG - // filter = []plex.Filter{ - // // does not have german audio - // { - // Name: "audioLanguage", - // Value: "de", - // Modifier: "\u0021=", - // }, - // // has german audio - // { - // Name: "audioLanguage", - // Value: "de", - // Modifier: "=", - // }, - // } for i := range filters { if i == 0 { @@ -1144,18 +1223,33 @@ func GetArtistsFromPlaylist(ipAddress, plexToken, ratingKey string) (playlistIte url := fmt.Sprintf("http://%s:32400/playlists/%s/items", ipAddress, ratingKey) response, err := makePlexAPIRequest(url, plexToken) if err != nil { - fmt.Println("GetPlaylistItems: Error making request:", err) + fmt.Println("GetArtistsFromPlaylist: Error making request:", err) + return playlistItems + } + + playlistItems, err = extractArtistsFromPlaylist(response) + if err != nil { + fmt.Println("Error extracting artists from playlist:", err) + } + return playlistItems +} + +func GetMoviesFromPlaylist(ipAddress, plexToken, ratingKey string) (playlistItems []types.PlexMovie) { + url := fmt.Sprintf("http://%s:32400/playlists/%s/items", ipAddress, ratingKey) + response, err := makePlexAPIRequest(url, plexToken) + if err != nil { + fmt.Println("GetMoviesromPlaylist: Error making request:", err) return playlistItems } - playlistItems, err = extractPlaylistItems(response) + playlistItems, err = extractMoviesFromPlaylist(response) if err != nil { fmt.Println("Error extracting playlist items:", err) } return playlistItems } -func extractPlaylistItems(xmlString string) (playlistItems []types.PlexMusicArtist, err error) { +func extractArtistsFromPlaylist(xmlString string) (playlistItems []types.PlexMusicArtist, err error) { var container MusicPlayList err = xml.Unmarshal([]byte(xmlString), &container) if err != nil { @@ -1192,6 +1286,25 @@ func extractPlaylistItems(xmlString string) (playlistItems []types.PlexMusicArti return playlistItems, nil } +func extractMoviesFromPlaylist(xmlString string) (playlistItems []types.PlexMovie, err error) { + var container MoviePlaylist + err = xml.Unmarshal([]byte(xmlString), &container) + if err != nil { + fmt.Println("Error parsing XML:", err) + return playlistItems, err + } + + for i := range container.Video { + playlistItems = append(playlistItems, types.PlexMovie{ + Title: container.Video[i].Title, + RatingKey: container.Video[i].RatingKey, + Resolution: container.Video[i].Media[0].VideoResolution, + Year: container.Video[i].Year, + DateAdded: parsePlexDate(container.Video[i].AddedAt)}) + } + return playlistItems, nil +} + // ================================================================================================= func makePlexAPIRequest(inputURL, plexToken string) (response string, err error) { diff --git a/plex/plex_test.go b/plex/plex_test.go index 91a0ad0..28d363c 100644 --- a/plex/plex_test.go +++ b/plex/plex_test.go @@ -121,7 +121,7 @@ func TestGetPlaylists(t *testing.T) { if plexIP == "" || plexToken == "" { t.Skip("ACCEPTANCE TEST: PLEX environment variables not set") } - playlists, err := GetPlaylists(plexIP, plexToken, "1") + playlists, err := GetPlaylists(plexIP, plexToken, "3") if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -131,7 +131,7 @@ func TestGetPlaylists(t *testing.T) { } } -func TestGetPlaylistItems(t *testing.T) { +func TestGetArtistsFromPlaylist(t *testing.T) { if plexIP == "" || plexToken == "" { t.Skip("ACCEPTANCE TEST: PLEX environment variables not set") } @@ -141,6 +141,18 @@ func TestGetPlaylistItems(t *testing.T) { t.Errorf("Expected at least one item, but got %d", len(items)) } } + +func TestGetMoviesFromPlaylist(t *testing.T) { + if plexIP == "" || plexToken == "" { + t.Skip("ACCEPTANCE TEST: PLEX environment variables not set") + } + items := GetMoviesFromPlaylist(plexIP, plexToken, "111907") + // Check the number of items + if len(items) == 0 { + t.Errorf("Expected at least one item, but got %d", len(items)) + } +} + func Test_findLowestResolution(t *testing.T) { tests := []struct { name string diff --git a/web/movies/movies.go b/web/movies/movies.go index cea43ae..651fa2e 100644 --- a/web/movies/movies.go +++ b/web/movies/movies.go @@ -23,7 +23,6 @@ var ( searchResults []types.SearchResults plexMovies []types.PlexMovie lookup string - plexFilters types.PlexLookupFilters lookupFilters types.MovieLookupFilters ) @@ -41,40 +40,25 @@ func MoviesHandler(w http.ResponseWriter, _ *http.Request) { } func (c MoviesConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { + playlist := r.FormValue("playlist") lookup = r.FormValue("lookup") - // plex filters - sd := r.FormValue("sd") - r240 := r.FormValue("240") - r480 := r.FormValue("480") - r576 := r.FormValue("576") - r720 := r.FormValue("720") - r1080 := r.FormValue("1080") - r4k := r.FormValue("4k") - plexResolutions := []string{sd, r240, r480, r576, r720, r1080, r4k} - // remove empty resolutions - var filteredResolutions []string - for _, resolution := range plexResolutions { - if resolution != "" { - filteredResolutions = append(filteredResolutions, resolution) - } - } - plexFilters.MatchesResolutions = filteredResolutions // lookup filters lookupFilters.AudioLanguage = r.FormValue("language") lookupFilters.NewerVersion = r.FormValue("newerVersion") == types.StringTrue // fetch from plex - if len(plexMovies) == 0 { - plexMovies = plex.GetPlexMovies(c.Config.PlexIP, c.Config.PlexMovieLibraryID, c.Config.PlexToken, nil) + if playlist == "all" { + if len(plexMovies) == 0 { + plexMovies = plex.GetPlexMovies(c.Config.PlexIP, c.Config.PlexMovieLibraryID, c.Config.PlexToken, nil) + } + } else { + plexMovies = plex.GetMoviesFromPlaylist(c.Config.PlexIP, c.Config.PlexToken, playlist) } - // filter plex movies based on preferences, eg. only movies with a certain resolution - filteredPlexMovies := plex.FilterPlexMovies(plexMovies, plexFilters) //nolint: gocritic - // filteredPlexMovies = filteredPlexMovies[:30] + // plexMovies = plexMovies[:30] //lint: gocritic jobRunning = true numberOfMoviesProcessed = 0 - totalMovies = len(filteredPlexMovies) - 1 - + totalMovies = len(plexMovies) - 1 // write progress bar fmt.Fprintf(w, `
`, numberOfMoviesProcessed, totalMovies) @@ -82,12 +66,12 @@ func (c MoviesConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { go func() { startTime := time.Now() if lookup == "cinemaParadiso" { - searchResults = cinemaparadiso.MoviesInParallel(filteredPlexMovies) + searchResults = cinemaparadiso.MoviesInParallel(plexMovies) if lookupFilters.NewerVersion { searchResults = cinemaparadiso.ScrapeMoviesParallel(searchResults) } } else { - searchResults = amazon.MoviesInParallel(filteredPlexMovies, lookupFilters.AudioLanguage, c.Config.AmazonRegion) + searchResults = amazon.MoviesInParallel(plexMovies, lookupFilters.AudioLanguage, c.Config.AmazonRegion) // if we are filtering by newer version, we need to search again if lookupFilters.NewerVersion { searchResults = amazon.ScrapeTitlesParallel(searchResults, c.Config.AmazonRegion, false) @@ -99,6 +83,26 @@ func (c MoviesConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { }() } +func (c MoviesConfig) PlaylistHTML(w http.ResponseWriter, _ *http.Request) { + playlistHTML := `
+ ` + playlists, _ := plex.GetPlaylists(c.Config.PlexIP, c.Config.PlexToken, c.Config.PlexMovieLibraryID) + fmt.Println("Playlists:", len(playlists)) + for i := range playlists { + playlistHTML += fmt.Sprintf( + ``, + playlists[i].Title, playlists[i].RatingKey, playlists[i].Title) + } + + playlistHTML += `
` + fmt.Fprint(w, playlistHTML) +} + func ProgressBarHTML(w http.ResponseWriter, _ *http.Request) { if lookup == "cinemaParadiso" { // check job status diff --git a/web/movies/movies.html b/web/movies/movies.html index 3cef3eb..4ca2d2b 100644 --- a/web/movies/movies.html +++ b/web/movies/movies.html @@ -27,6 +27,14 @@

Movies

+ Plex: filter by playlist +
+ +
Lookup:
-
- Plex Filter: only compare movies that match the following criteria. - - - - - - -
Lookup Filters: