-
Notifications
You must be signed in to change notification settings - Fork 4
Add new APIs: Home for recently played and recently added, Artist, and Album. #1
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
base: master
Are you sure you want to change the base?
Changes from all commits
3b7f80e
f75998d
d0a7c3b
d93c0b4
773be3e
8cd0784
573d577
0f2c1d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package us.berkovitz.plexapi.media | ||
|
|
||
| import io.ktor.client.call.* | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
| import nl.adaptivity.xmlutil.serialization.XmlDefault | ||
| import nl.adaptivity.xmlutil.serialization.XmlElement | ||
| import us.berkovitz.plexapi.config.Http | ||
|
|
||
| /** | ||
| * Represents a music album in the Plex library. | ||
| */ | ||
| @Serializable | ||
| @SerialName("Directory") | ||
| data class Album( | ||
| val ratingKey: Long, | ||
| val key: String, | ||
| val parentRatingKey: Long? = null, | ||
| val guid: String? = null, | ||
| val parentGuid: String? = null, | ||
| val studio: String? = null, | ||
| val type: String = "album", | ||
| val title: String, | ||
| val titleSort: String? = null, | ||
| val parentKey: String? = null, | ||
| val parentTitle: String? = null, | ||
| val summary: String? = null, | ||
| val index: Int? = null, | ||
| val rating: Double? = null, | ||
| val year: Int? = null, | ||
| @XmlDefault("0") val leafCount: Int = 0, | ||
| @XmlDefault("0") val viewedLeafCount: Int = 0, | ||
| val thumb: String? = null, | ||
| val art: String? = null, | ||
| val parentThumb: String? = null, | ||
| val originallyAvailableAt: String? = null, | ||
| val addedAt: Long? = null, | ||
| val updatedAt: Long? = null, | ||
| val loudnessAnalysisVersion: Int? = null, | ||
| @XmlElement(true) @SerialName("Genre") val genres: List<Tag>? = null, | ||
| @XmlElement(true) @SerialName("Style") val styles: List<Tag>? = null, | ||
| @XmlElement(true) @SerialName("Mood") val moods: List<Tag>? = null, | ||
| @XmlElement(true) @SerialName("Director") val directors: List<Tag>? = null | ||
| ) : MediaItem() { | ||
|
|
||
| companion object { | ||
| /** | ||
| * Fetch an album by its rating key. | ||
| */ | ||
| suspend fun fromId(id: Long, server: PlexServer): Album? { | ||
| val url = server.urlFor("/library/metadata/$id") | ||
| val res: MediaContainer<Album> = Http.authenticatedGet(url, null, server.token).body() | ||
| if (res.elements.isEmpty()) return null | ||
| return res.elements[0].also { | ||
| it.setServer(server) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get all tracks in this album. | ||
| */ | ||
| suspend fun tracks(): List<Track> { | ||
| if (_server == null) return emptyList() | ||
| val url = _server!!.urlFor("/library/metadata/$ratingKey/children") | ||
| val res: MediaContainer<Track> = Http.authenticatedGet(url, null, _server!!.token).body() | ||
| return res.elements.map { | ||
| it.also { track -> track.setServer(_server!!) } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get the parent artist for this album. | ||
| */ | ||
| suspend fun artist(): Artist? { | ||
| if (_server == null || parentRatingKey == null) return null | ||
| return Artist.fromId(parentRatingKey, _server!!) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package us.berkovitz.plexapi.media | ||
|
|
||
| import io.ktor.client.call.* | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
| import nl.adaptivity.xmlutil.serialization.XmlElement | ||
| import us.berkovitz.plexapi.config.Http | ||
|
|
||
| /** | ||
| * Represents a music artist in the Plex library. | ||
| */ | ||
| @Serializable | ||
| @SerialName("Directory") | ||
| data class Artist( | ||
| val ratingKey: Long, | ||
| val key: String, | ||
| val guid: String? = null, | ||
| val type: String = "artist", | ||
| val title: String, | ||
| val titleSort: String? = null, | ||
| val summary: String? = null, | ||
| val index: Int? = null, | ||
| val thumb: String? = null, | ||
| val art: String? = null, | ||
| val addedAt: Long? = null, | ||
| val updatedAt: Long? = null, | ||
| @XmlElement(true) @SerialName("Genre") val genres: List<Tag>? = null, | ||
| @XmlElement(true) @SerialName("Country") val countries: List<Tag>? = null, | ||
| @XmlElement(true) @SerialName("Style") val styles: List<Tag>? = null, | ||
| @XmlElement(true) @SerialName("Mood") val moods: List<Tag>? = null | ||
| ) : MediaItem() { | ||
|
|
||
| companion object { | ||
| /** | ||
| * Fetch an artist by its rating key. | ||
| */ | ||
| suspend fun fromId(id: Long, server: PlexServer): Artist? { | ||
| val url = server.urlFor("/library/metadata/$id") | ||
| val res: MediaContainer<Artist> = Http.authenticatedGet(url, null, server.token).body() | ||
| if (res.elements.isEmpty()) return null | ||
| return res.elements[0].also { | ||
| it.setServer(server) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get all albums by this artist. | ||
| */ | ||
| suspend fun albums(): List<Album> { | ||
| if (_server == null) return emptyList() | ||
| val url = _server!!.urlFor("/library/metadata/$ratingKey/children") | ||
| val res: MediaContainer<Album> = Http.authenticatedGet(url, null, _server!!.token).body() | ||
| return res.elements.map { | ||
| it.also { album -> album.setServer(_server!!) } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get all tracks by this artist (across all albums). | ||
| */ | ||
| suspend fun tracks(): List<Track> { | ||
| if (_server == null) return emptyList() | ||
| val url = _server!!.urlFor("/library/metadata/$ratingKey/allLeaves") | ||
| val res: MediaContainer<Track> = Http.authenticatedGet(url, null, _server!!.token).body() | ||
| return res.elements.map { | ||
| it.also { track -> track.setServer(_server!!) } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package us.berkovitz.plexapi.media | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
| import nl.adaptivity.xmlutil.serialization.XmlElement | ||
|
|
||
| /** | ||
| * Represents a Plex library section (e.g., Music, Movies, TV Shows). | ||
| * | ||
| * For music libraries, `type` will be "artist". | ||
| */ | ||
| @Serializable | ||
| @SerialName("Directory") | ||
| data class LibrarySection( | ||
| val key: String, | ||
| val title: String, | ||
| val type: String, | ||
| val uuid: String? = null, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment on everything, I don't get why there's a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a unit test to illustrate that it works without this. See ac59cf6 I intentionally left fields unpopulated in the Playlist XML, such as summary and titleSort. The test still passes |
||
| val agent: String? = null, | ||
| val scanner: String? = null, | ||
| val language: String? = null, | ||
| val art: String? = null, | ||
| val thumb: String? = null, | ||
| val composite: String? = null, | ||
| val createdAt: Long? = null, | ||
| val updatedAt: Long? = null, | ||
| val scannedAt: Long? = null, | ||
| @XmlElement(true) @SerialName("Location") val locations: List<LibrarySectionLocation>? = null | ||
| ) : MediaItem() { | ||
| /** | ||
| * Get artists from this library section with pagination support. | ||
| * @param start Starting index for pagination (default 0) | ||
| * @param size Maximum number of items to return (default 100, use 0 for all) | ||
| */ | ||
| suspend fun artists(start: Int = 0, size: Int = 100): List<Artist> { | ||
| if (_server == null) return emptyList() | ||
| return _server!!.artists(key, start, size) | ||
| } | ||
|
|
||
| /** | ||
| * Get albums from this library section with pagination support. | ||
| * @param start Starting index for pagination (default 0) | ||
| * @param size Maximum number of items to return (default 100, use 0 for all) | ||
| */ | ||
| suspend fun albums(start: Int = 0, size: Int = 100): List<Album> { | ||
| if (_server == null) return emptyList() | ||
| return _server!!.albums(key, start, size) | ||
| } | ||
|
|
||
| /** | ||
| * Get tracks from this library section with pagination support. | ||
| * @param start Starting index for pagination (default 0) | ||
| * @param size Maximum number of items to return (default 100, use 0 for all) | ||
| */ | ||
| suspend fun tracks(start: Int = 0, size: Int = 100): List<Track> { | ||
| if (_server == null) return emptyList() | ||
| return _server!!.tracks(key, start, size) | ||
| } | ||
|
|
||
| /** | ||
| * Get recently added albums from this library section. | ||
| * @param limit Maximum number of items to return (default 50) | ||
| */ | ||
| suspend fun recentlyAddedAlbums(limit: Int = 50): List<Album> { | ||
| if (_server == null) return emptyList() | ||
| return _server!!.recentlyAddedAlbums(key, limit) | ||
| } | ||
|
|
||
| /** | ||
| * Get recently played tracks from this library section. | ||
| * @param limit Maximum number of items to return (default 50) | ||
| */ | ||
| suspend fun recentlyPlayedTracks(limit: Int = 50): List<Track> { | ||
| if (_server == null) return emptyList() | ||
| return _server!!.recentlyPlayedTracks(key, limit) | ||
| } | ||
| } | ||
|
|
||
| @Serializable | ||
| @SerialName("Location") | ||
| data class LibrarySectionLocation( | ||
| val id: Long, | ||
| val path: String | ||
| ) | ||
|
|
||
| /** | ||
| * Response container for library sections endpoint. | ||
| */ | ||
| @Serializable | ||
| @SerialName("MediaContainer") | ||
| data class LibrarySectionsResponse( | ||
| val size: Long, | ||
| @XmlElement(true) @SerialName("Directory") val sections: List<LibrarySection> | ||
| ) | ||
|
Comment on lines
+89
to
+94
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LibrarySectionsResponse is needed because the /library/sections endpoint returns
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's just how MediaContainer always works though? See the newly added Playlist unit test. You get a Ref: ac59cf6 |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too certain about this. kotlinx serialization has pretty advanced polymorphic support. I think it can work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ran into issues with the XML serialization discriminator when Artist and Album were in the polymorphic module. Happy to revisit if you have a working pattern - the current approach works but I agree it's not ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK; Let me take a look
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH - the comment is just what's throwing me off. It's just irrelevant. The only reason why I had the polymorphism here is to support
Playlist::items()which returnsArray<MediaItem>where in theory the members of the playlist aren't strictlyTracks but rather could be other items if the API allowed it.Nothing in this PR uses generics in responses, so this isn't needed