Skip to content

Commit

Permalink
Merge pull request #41 from tphoney/music_playlists
Browse files Browse the repository at this point in the history
(feat) get plex playlists, and playlist items
  • Loading branch information
tphoney committed Jun 1, 2024
2 parents aefe581 + 07d29a4 commit 0609f5b
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 9 deletions.
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

0 comments on commit 0609f5b

Please sign in to comment.