Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) get plex playlists, and playlist items #41

Merged
merged 2 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

## features

- allow the use of playlists for finding music / TV / movies `https://www.plexopedia.com/plex-media-server/api/playlists/view/`

## bugs

- mandalorian is not showing up on amazon tv search
Expand Down Expand Up @@ -45,3 +43,4 @@
- allow amazon tv search for indivdual series
- improve best match for tv series
- vikings, Once Upon a Time in Wonderland, What We Do in the Shadows, The Peter Serafinowicz Show is not showing up on cinema-paradiso tv search
- allow the use of playlists for finding music / TV / movies `https://www.plexopedia.com/plex-media-server/api/playlists/view/`
193 changes: 192 additions & 1 deletion plex/plex.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,108 @@ type AlbumContainer struct {
} `xml:"Directory"`
}

type PlaylistContainer struct {
XMLName xml.Name `xml:"MediaContainer"`
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
Playlist []struct {
Text string `xml:",chardata"`
RatingKey string `xml:"ratingKey,attr"`
Key string `xml:"key,attr"`
GUID string `xml:"guid,attr"`
Type string `xml:"type,attr"`
Title string `xml:"title,attr"`
TitleSort string `xml:"titleSort,attr"`
Summary string `xml:"summary,attr"`
Smart string `xml:"smart,attr"`
PlaylistType string `xml:"playlistType,attr"`
Composite string `xml:"composite,attr"`
Icon string `xml:"icon,attr"`
ViewCount string `xml:"viewCount,attr"`
LastViewedAt string `xml:"lastViewedAt,attr"`
Duration string `xml:"duration,attr"`
LeafCount string `xml:"leafCount,attr"`
AddedAt string `xml:"addedAt,attr"`
UpdatedAt string `xml:"updatedAt,attr"`
} `xml:"Playlist"`
}

type MusicPlayList 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"`
Track []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"`
ParentStudio string `xml:"parentStudio,attr"`
Type string `xml:"type,attr"`
Title string `xml:"title,attr"`
TitleSort string `xml:"titleSort,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"`
Summary string `xml:"summary,attr"`
Index string `xml:"index,attr"`
ParentIndex string `xml:"parentIndex,attr"`
RatingCount string `xml:"ratingCount,attr"`
ViewCount string `xml:"viewCount,attr"`
LastViewedAt string `xml:"lastViewedAt,attr"`
ParentYear string `xml:"parentYear,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"`
Duration string `xml:"duration,attr"`
AddedAt string `xml:"addedAt,attr"`
UpdatedAt string `xml:"updatedAt,attr"`
SkipCount string `xml:"skipCount,attr"`
OriginalTitle string `xml:"originalTitle,attr"`
UserRating string `xml:"userRating,attr"`
LastRatedAt string `xml:"lastRatedAt,attr"`
Media struct {
Text string `xml:",chardata"`
ID string `xml:"id,attr"`
Duration string `xml:"duration,attr"`
Bitrate string `xml:"bitrate,attr"`
AudioChannels string `xml:"audioChannels,attr"`
AudioCodec string `xml:"audioCodec,attr"`
Container string `xml:"container,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"`
HasThumbnail string `xml:"hasThumbnail,attr"`
} `xml:"Part"`
} `xml:"Media"`
Genre []struct {
Text string `xml:",chardata"`
Tag string `xml:"tag,attr"`
} `xml:"Genre"`
} `xml:"Track"`
}

type Filter struct {
Name string
Value string
Expand Down Expand Up @@ -901,7 +1003,7 @@ func extractTVEpisodes(xmlString string) (episodeList []types.PlexTVEpisode) {
}

// =================================================================================================
func GetPlexMusicArtists(ipAddress, libraryID, plexToken string) (artists []types.PlexMusicArtist) {
func GetPlexMusicArtists(ipAddress, plexToken, libraryID string) (artists []types.PlexMusicArtist) {
url := fmt.Sprintf("http://%s:32400/library/sections/%s/all", ipAddress, libraryID)

response, err := makePlexAPIRequest(url, plexToken)
Expand Down Expand Up @@ -973,6 +1075,7 @@ func extractMusicAlbums(xmlString string) (albums []types.PlexMusicAlbum, err er
return albums, nil
}

// =================================================================================================
func GetPlexLibraries(ipAddress, plexToken string) (libraryList []types.PlexLibrary, err error) {
url := fmt.Sprintf("http://%s:32400/library/sections", ipAddress)

Expand Down Expand Up @@ -1003,6 +1106,94 @@ func extractLibraries(xmlString string) (libraryList []types.PlexLibrary, err er

// =================================================================================================

func GetPlaylists(ipAddress, plexToken, libraryID string) (playlists []types.PlexPlaylist, err error) {
start := time.Now()
url := fmt.Sprintf("http://%s:32400/playlists?sectionID=%s", ipAddress, libraryID)

response, err := makePlexAPIRequest(url, plexToken)
if err != nil {
fmt.Println("GetPlaylists: Error making request:", err)
return playlists, err
}

playlists, err = extractPlaylists(response)
fmt.Printf("Plex playlists: %d. Duration: %v\n", len(playlists), time.Since(start))
return playlists, err
}

func extractPlaylists(xmlString string) (playlistList []types.PlexPlaylist, err error) {
var container PlaylistContainer
err = xml.Unmarshal([]byte(xmlString), &container)
if err != nil {
fmt.Println("Error parsing XML:", err)
return playlistList, err
}

for i := range container.Playlist {
playlistList = append(playlistList, types.PlexPlaylist{
Title: container.Playlist[i].Title,
RatingKey: container.Playlist[i].RatingKey,
Type: container.Playlist[i].PlaylistType,
})
}

return playlistList, nil
}

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("GetPlaylistItems: Error making request:", err)
return playlistItems
}

playlistItems, err = extractPlaylistItems(response)
if err != nil {
fmt.Println("Error extracting playlist items:", err)
}
return playlistItems
}

func extractPlaylistItems(xmlString string) (playlistItems []types.PlexMusicArtist, err error) {
var container MusicPlayList
err = xml.Unmarshal([]byte(xmlString), &container)
if err != nil {
fmt.Println("Error parsing XML:", err)
return playlistItems, err
}

// verify the library ID matches
artists := make(map[string]types.PlexMusicArtist)
for i := range container.Track {
album := types.PlexMusicAlbum{
Title: container.Track[i].ParentTitle,
RatingKey: container.Track[i].ParentRatingKey,
Year: container.Track[i].ParentYear,
DateAdded: parsePlexDate(container.Track[i].AddedAt),
}
foundArtist, ok := artists[container.Track[i].GrandparentTitle]
if !ok {
artists[container.Track[i].GrandparentTitle] = types.PlexMusicArtist{
Name: container.Track[i].GrandparentTitle,
RatingKey: container.Track[i].GrandparentRatingKey,
Albums: []types.PlexMusicAlbum{album},
}
} else if !slices.Contains(artists[container.Track[i].GrandparentTitle].Albums, album) {
foundArtist.Albums = append(artists[container.Track[i].GrandparentTitle].Albums, album) //nolint:gocritic
// replace the artist in the map with the updated artist
artists[container.Track[i].GrandparentTitle] = foundArtist
}
}
// convert map to slice
for _, value := range artists {
playlistItems = append(playlistItems, value)
}
return playlistItems, nil
}

// =================================================================================================

func makePlexAPIRequest(inputURL, plexToken string) (response string, err error) {
req, err := http.NewRequestWithContext(context.Background(), "GET", inputURL, http.NoBody)
if err != nil {
Expand Down
26 changes: 25 additions & 1 deletion plex/plex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestGetPlexMusic(t *testing.T) {
if plexIP == "" || plexMusicLibraryID == "" || plexToken == "" {
t.Skip("ACCEPTANCE TEST: PLEX environment variables not set")
}
result := GetPlexMusicArtists(plexIP, plexMusicLibraryID, plexToken)
result := GetPlexMusicArtists(plexIP, plexToken, plexMusicLibraryID)

if len(result) == 0 {
t.Errorf("Expected at least one album, but got %d", len(result))
Expand All @@ -117,6 +117,30 @@ func TestGetPlexMusic(t *testing.T) {
}
}

func TestGetPlaylists(t *testing.T) {
if plexIP == "" || plexToken == "" {
t.Skip("ACCEPTANCE TEST: PLEX environment variables not set")
}
playlists, err := GetPlaylists(plexIP, plexToken, "1")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Check the number of playlists
if len(playlists) == 0 {
t.Errorf("Expected at least one playlist, but got %d", len(playlists))
}
}

func TestGetPlaylistItems(t *testing.T) {
if plexIP == "" || plexToken == "" {
t.Skip("ACCEPTANCE TEST: PLEX environment variables not set")
}
items := GetArtistsFromPlaylist(plexIP, plexToken, "111897")
// 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
Expand Down
6 changes: 6 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,9 @@ type PlexLibrary struct {
Type string
ID string
}

type PlexPlaylist struct {
Title string
Type string
RatingKey string
}
29 changes: 26 additions & 3 deletions web/music/music.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func MusicHandler(w http.ResponseWriter, _ *http.Request) {

// nolint: lll, nolintlint
func (c MusicConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) {
playlist := r.FormValue("playlist")
lookup = r.FormValue("lookup")
if lookup == "musicbrainz" {
if c.Config.MusicBrainzURL == "" {
Expand All @@ -75,8 +76,10 @@ func (c MusicConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) {
}
lookupType = r.FormValue("lookuptype")
// only get the artists from plex once
if len(plexMusic) == 0 {
plexMusic = plex.GetPlexMusicArtists(c.Config.PlexIP, c.Config.PlexMusicLibraryID, c.Config.PlexToken)
if playlist == "all" {
plexMusic = plex.GetPlexMusicArtists(c.Config.PlexIP, c.Config.PlexToken, c.Config.PlexMusicLibraryID)
} else {
plexMusic = plex.GetArtistsFromPlaylist(c.Config.PlexIP, c.Config.PlexToken, playlist)
}
//nolint: gocritic
// plexMusic = plexMusic[:30]
Expand Down Expand Up @@ -131,6 +134,26 @@ func (c MusicConfig) ProcessHTML(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Processed %d artists in %v\n", totalArtists, time.Since(startTime))
}

func (c MusicConfig) PlaylistHTML(w http.ResponseWriter, _ *http.Request) {
playlistHTML := `<fieldset id="playlist">
<label for="All">
<input type="radio" id="playlist" name="playlist" value="all" checked />
All: dont use a playlist. (SLOW, only use for small libraries)
</label>`
playlists, _ := plex.GetPlaylists(c.Config.PlexIP, c.Config.PlexToken, c.Config.PlexMusicLibraryID)
fmt.Println("Playlists:", len(playlists))
for i := range playlists {
playlistHTML += fmt.Sprintf(
`<label for=%q>
<input type="radio" id="playlist" name="playlist" value=%q/>
%s</label>`,
playlists[i].Title, playlists[i].RatingKey, playlists[i].Title)
}

playlistHTML += `</fieldset>`
fmt.Fprint(w, playlistHTML)
}

func ProgressBarHTML(w http.ResponseWriter, _ *http.Request) {
if lookup == spotifyString {
numberOfArtistsProcessed = spotify.GetJobProgress()
Expand All @@ -146,7 +169,7 @@ func ProgressBarHTML(w http.ResponseWriter, _ *http.Request) {
tableContents = renderSimilarArtistsTable()
}
fmt.Fprintf(w,
`<table class="table-sortable">%s</tbody></table>
`<table class="table-sortable" hx-boost="true">%s</tbody></table>
</script><script>document.querySelector('.table-sortable').tsortable()</script>`,
tableContents)
// reset variables
Expand Down
9 changes: 7 additions & 2 deletions web/music/music.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@
<h1 class="container">Music</h1>
<div hx-get="/settings/plexinfook" class="container" hx-trigger="load"></div>
<form hx-post="/musicprocess" class="container" hx-target="#progress" hx-boost="true" hx-indicator="#indicator">
<fieldset>
<legend><strong>Plex Filter:</strong> only lookup music that matches the following criteria.</legend>
<legend><strong>Plex:</strong> filter by playlist</legend>
<fieldset id="playlist" hx-get="/musicplaylists" class="container" name="playlist" hx-trigger="load once"
hx-swap="outerHTML" hx-boost="true" hx-target="this">
<label for="All">
<input type="radio" id="playlist" name="playlist" value="all" checked />
All: dont use a playlist.
</label>
</fieldset>
<fieldset>
<legend><strong>Lookup:</strong></legend>
Expand Down
1 change: 1 addition & 0 deletions web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func StartServer(startingConfig *types.Configuration) {

mux.HandleFunc("/music", music.MusicHandler)
mux.HandleFunc("/musicprocess", music.MusicConfig{Config: config}.ProcessHTML)
mux.HandleFunc("/musicplaylists", music.MusicConfig{Config: config}.PlaylistHTML)
mux.HandleFunc("/musicprogress", music.ProgressBarHTML)

mux.HandleFunc("/", indexHandler)
Expand Down
Loading