diff --git a/musicbrainz/musicbrainz_test.go b/musicbrainz/musicbrainz_test.go index 895d7c6..09949a2 100644 --- a/musicbrainz/musicbrainz_test.go +++ b/musicbrainz/musicbrainz_test.go @@ -42,7 +42,7 @@ func TestSearchMusicBrainzArtist(t *testing.T) { { Name: "AC/DC", ID: "66c662b6-6e2f-4930-8610-912e24c63ed1", - Albums: make([]types.MusicAlbumSearchResult, 17), + Albums: make([]types.MusicAlbumSearchResult, 18), }, }, }, diff --git a/plex/plex.go b/plex/plex.go index 59aa0f2..4ad923d 100644 --- a/plex/plex.go +++ b/plex/plex.go @@ -821,6 +821,204 @@ type MoviePlaylist struct { } `xml:"Video"` } +type TVPlaylist 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"` + ParentRatingKey string `xml:"parentRatingKey,attr"` + GrandparentRatingKey string `xml:"grandparentRatingKey,attr"` + GUID string `xml:"guid,attr"` + ParentGUID string `xml:"parentGuid,attr"` + GrandparentGUID string `xml:"grandparentGuid,attr"` + Type string `xml:"type,attr"` + Title string `xml:"title,attr"` + GrandparentKey string `xml:"grandparentKey,attr"` + ParentKey string `xml:"parentKey,attr"` + LibrarySectionTitle string `xml:"librarySectionTitle,attr"` + LibrarySectionID string `xml:"librarySectionID,attr"` + LibrarySectionKey string `xml:"librarySectionKey,attr"` + GrandparentTitle string `xml:"grandparentTitle,attr"` + ParentTitle string `xml:"parentTitle,attr"` + ContentRating string `xml:"contentRating,attr"` + Summary string `xml:"summary,attr"` + Index string `xml:"index,attr"` + ParentIndex string `xml:"parentIndex,attr"` + AudienceRating string `xml:"audienceRating,attr"` + ViewCount string `xml:"viewCount,attr"` + LastViewedAt string `xml:"lastViewedAt,attr"` + Year string `xml:"year,attr"` + Thumb string `xml:"thumb,attr"` + Art string `xml:"art,attr"` + ParentThumb string `xml:"parentThumb,attr"` + GrandparentThumb string `xml:"grandparentThumb,attr"` + GrandparentArt string `xml:"grandparentArt,attr"` + GrandparentTheme string `xml:"grandparentTheme,attr"` + Duration string `xml:"duration,attr"` + OriginallyAvailableAt string `xml:"originallyAvailableAt,attr"` + AddedAt string `xml:"addedAt,attr"` + AudienceRatingImage string `xml:"audienceRatingImage,attr"` + TitleSort string `xml:"titleSort,attr"` + UpdatedAt string `xml:"updatedAt,attr"` + ChapterSource string `xml:"chapterSource,attr"` + GrandparentSlug string `xml:"grandparentSlug,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"` + OptimizedForStreaming string `xml:"optimizedForStreaming,attr"` + AudioProfile string `xml:"audioProfile,attr"` + Has64bitOffsets string `xml:"has64bitOffsets,attr"` + VideoProfile string `xml:"videoProfile,attr"` + DisplayOffset string `xml:"displayOffset,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"` + AudioProfile string `xml:"audioProfile,attr"` + Container string `xml:"container,attr"` + Has64bitOffsets string `xml:"has64bitOffsets,attr"` + OptimizedForStreaming string `xml:"optimizedForStreaming,attr"` + VideoProfile string `xml:"videoProfile,attr"` + } `xml:"Part"` + } `xml:"Media"` + Role []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Role"` + 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"` + MediaContainer struct { + 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"` + ParentRatingKey string `xml:"parentRatingKey,attr"` + GrandparentRatingKey string `xml:"grandparentRatingKey,attr"` + GUID string `xml:"guid,attr"` + ParentGUID string `xml:"parentGuid,attr"` + GrandparentGUID string `xml:"grandparentGuid,attr"` + Type string `xml:"type,attr"` + Title string `xml:"title,attr"` + GrandparentKey string `xml:"grandparentKey,attr"` + ParentKey string `xml:"parentKey,attr"` + LibrarySectionTitle string `xml:"librarySectionTitle,attr"` + LibrarySectionID string `xml:"librarySectionID,attr"` + LibrarySectionKey string `xml:"librarySectionKey,attr"` + GrandparentTitle string `xml:"grandparentTitle,attr"` + ParentTitle string `xml:"parentTitle,attr"` + ContentRating string `xml:"contentRating,attr"` + Summary string `xml:"summary,attr"` + Index string `xml:"index,attr"` + ParentIndex string `xml:"parentIndex,attr"` + AudienceRating string `xml:"audienceRating,attr"` + ViewCount string `xml:"viewCount,attr"` + LastViewedAt string `xml:"lastViewedAt,attr"` + Year string `xml:"year,attr"` + Thumb string `xml:"thumb,attr"` + Art string `xml:"art,attr"` + ParentThumb string `xml:"parentThumb,attr"` + GrandparentThumb string `xml:"grandparentThumb,attr"` + GrandparentArt string `xml:"grandparentArt,attr"` + GrandparentTheme string `xml:"grandparentTheme,attr"` + Duration string `xml:"duration,attr"` + OriginallyAvailableAt string `xml:"originallyAvailableAt,attr"` + AddedAt string `xml:"addedAt,attr"` + AudienceRatingImage string `xml:"audienceRatingImage,attr"` + TitleSort string `xml:"titleSort,attr"` + UpdatedAt string `xml:"updatedAt,attr"` + ChapterSource string `xml:"chapterSource,attr"` + GrandparentSlug string `xml:"grandparentSlug,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"` + OptimizedForStreaming string `xml:"optimizedForStreaming,attr"` + AudioProfile string `xml:"audioProfile,attr"` + Has64bitOffsets string `xml:"has64bitOffsets,attr"` + VideoProfile string `xml:"videoProfile,attr"` + DisplayOffset string `xml:"displayOffset,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"` + AudioProfile string `xml:"audioProfile,attr"` + Container string `xml:"container,attr"` + Has64bitOffsets string `xml:"has64bitOffsets,attr"` + OptimizedForStreaming string `xml:"optimizedForStreaming,attr"` + VideoProfile string `xml:"videoProfile,attr"` + } `xml:"Part"` + } `xml:"Media"` + Role []struct { + Text string `xml:",chardata"` + Tag string `xml:"tag,attr"` + } `xml:"Role"` + 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"` + } `xml:"Video"` + } `xml:"MediaContainer"` + } `xml:"Video"` +} + type Filter struct { Name string Value string @@ -1033,7 +1231,7 @@ func extractTVSeasons(xmlString string) (seasonList []types.PlexTVSeason) { if strings.HasPrefix(container.Directory[i].Title, "Season") { seasonNumber, _ := strconv.Atoi(container.Directory[i].Index) seasonList = append(seasonList, types.PlexTVSeason{ - Title: container.Directory[i].Title, RatingKey: container.Directory[i].RatingKey, Number: seasonNumber}) + RatingKey: container.Directory[i].RatingKey, Number: seasonNumber}) } } return seasonList @@ -1199,36 +1397,135 @@ func extractPlaylists(xmlString string) (playlistList []types.PlexPlaylist, err return playlistList, nil } -func GetArtistsFromPlaylist(ipAddress, plexToken, ratingKey string) (playlistItems []types.PlexMusicArtist) { +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("GetArtistsFromPlaylist: Error making request:", err) + fmt.Println("GetMoviesromPlaylist: Error making request:", err) return playlistItems } - playlistItems, err = extractArtistsFromPlaylist(response) + playlistItems, err = extractMoviesFromPlaylist(response) if err != nil { - fmt.Println("Error extracting artists from playlist:", err) + fmt.Println("Error extracting playlist items:", err) } return playlistItems } -func GetMoviesFromPlaylist(ipAddress, plexToken, ratingKey string) (playlistItems []types.PlexMovie) { +func GetTVFromPlaylist(ipAddress, plexToken, ratingKey string) (playlistItems []types.PlexTVShow) { 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) + fmt.Println("getTVFromPlaylist: Error making request:", err) return playlistItems } - playlistItems, err = extractMoviesFromPlaylist(response) + playlistItems, err = extractTVFromPlaylist(response) if err != nil { fmt.Println("Error extracting playlist items:", err) } return playlistItems } +func GetArtistsFromPlaylist(ipAddress, plexToken, ratingKey string) (playlistItems []types.PlexMusicArtist) { + url := fmt.Sprintf("http://%s:32400/playlists/%s/items", ipAddress, ratingKey) + response, err := makePlexAPIRequest(url, plexToken) + if err != nil { + 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 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 extractTVFromPlaylist(xmlString string) (playlistItems []types.PlexTVShow, err error) { + var container TVPlaylist + err = xml.Unmarshal([]byte(xmlString), &container) + if err != nil { + fmt.Println("Error parsing XML:", err) + return playlistItems, err + } + + tvShows := make(map[string]types.PlexTVShow) + for i := range container.Video { + episode := types.PlexTVEpisode{ + Index: container.Video[i].Index, + Title: container.Video[i].Title, + Resolution: container.Video[i].Media[0].VideoResolution, + OriginallyAired: parsePlexDate(container.Video[i].OriginallyAvailableAt), + DateAdded: parsePlexDate(container.Video[i].AddedAt), + } + + seasonNumber, _ := strconv.Atoi(container.Video[i].ParentIndex) + season := types.PlexTVSeason{ + Number: seasonNumber, + RatingKey: container.Video[i].ParentRatingKey, + FirstEpisodeAired: episode.OriginallyAired, + LastEpisodeAired: episode.OriginallyAired, + } + + foundTVShow, ok := tvShows[container.Video[i].GrandparentTitle] + if !ok { + season.Episodes = append(season.Episodes, episode) + tvShows[container.Video[i].GrandparentTitle] = types.PlexTVShow{ + Title: container.Video[i].GrandparentTitle, + RatingKey: container.Video[i].GrandparentRatingKey, + Year: container.Video[i].Year, + DateAdded: parsePlexDate(container.Video[i].AddedAt), + Seasons: []types.PlexTVSeason{season}, + } + } else { + seasonFound := false + for j := range foundTVShow.Seasons { + if foundTVShow.Seasons[j].Number == season.Number { + foundTVShow.Seasons[j].Episodes = append(foundTVShow.Seasons[j].Episodes, episode) + seasonFound = true + break + } + } + if !seasonFound { + fmt.Println("Adding season:", season.Number, "to TV show:", foundTVShow.Title) + season.Episodes = append(season.Episodes, episode) + foundTVShow.Seasons = append(foundTVShow.Seasons, season) + // replace the TV show in the map with the updated TV show + tvShows[container.Video[i].GrandparentTitle] = foundTVShow + } + } + } + // need to sort the episodes + // find the lowest resolution for each season and the start and end dates + // find the show start and end dates + + // convert map to slice + for i := range tvShows { + playlistItems = append(playlistItems, tvShows[i]) + } + return playlistItems, nil +} + func extractArtistsFromPlaylist(xmlString string) (playlistItems []types.PlexMusicArtist, err error) { var container MusicPlayList err = xml.Unmarshal([]byte(xmlString), &container) @@ -1266,25 +1563,6 @@ func extractArtistsFromPlaylist(xmlString string) (playlistItems []types.PlexMus 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 d61a98f..cc9222c 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, "3") + playlists, err := GetPlaylists(plexIP, plexToken, "2") if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -153,6 +153,16 @@ func TestGetMoviesFromPlaylist(t *testing.T) { } } +func TestGetTVFromPlaylist(t *testing.T) { + if plexIP == "" || plexToken == "" { + t.Skip("ACCEPTANCE TEST: PLEX environment variables not set") + } + items := GetTVFromPlaylist(plexIP, plexToken, "111908") + // 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/types/types.go b/types/types.go index 6981601..a6336a0 100644 --- a/types/types.go +++ b/types/types.go @@ -86,7 +86,6 @@ type PlexTVShow struct { } type PlexTVSeason struct { - Title string Number int RatingKey string LowestResolution string diff --git a/web/movies/movies.go b/web/movies/movies.go index f1b1e0d..627c6e3 100644 --- a/web/movies/movies.go +++ b/web/movies/movies.go @@ -39,6 +39,26 @@ func MoviesHandler(w http.ResponseWriter, _ *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 (c MoviesConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { playlist := r.FormValue("playlist") lookup = r.FormValue("lookup") @@ -47,9 +67,7 @@ func (c MoviesConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) { lookupFilters.NewerVersion = r.FormValue("newerVersion") == types.StringTrue // fetch from plex if playlist == "all" { - if len(plexMovies) == 0 { - plexMovies = plex.AllMovies(c.Config.PlexIP, c.Config.PlexMovieLibraryID, c.Config.PlexToken) - } + plexMovies = plex.AllMovies(c.Config.PlexIP, c.Config.PlexMovieLibraryID, c.Config.PlexToken) } else { plexMovies = plex.GetMoviesFromPlaylist(c.Config.PlexIP, c.Config.PlexToken, playlist) } @@ -83,26 +101,6 @@ 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 4ca2d2b..53633ba 100644 --- a/web/movies/movies.html +++ b/web/movies/movies.html @@ -28,7 +28,7 @@

Movies

Plex: filter by playlist -