Plex API for Music Radio & Playlists

SkillSecurity

Plex API endpoints, authentication, filtering, music radio, and video content. Use when working on Plex integration, library browsing, track queries, radio features, or video playback.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Plex API for Music Radio & Playlists skill

What this skill tells your AI

The instructions your AI receives, as published by ad-repo/nullplayer in skills/plex-integration/SKILL.md and read by ahel’s review.

This guide describes the Plex API endpoints used for querying tracks and building radio/playlist features in NullPlayer.

Authentication

All requests require authentication headers:

X-Plex-Token: {token}
X-Plex-Client-Identifier: {unique-client-id}
X-Plex-Product: NullPlayer
X-Plex-Version: 1.0
X-Plex-Platform: macOS
X-Plex-Device: Mac
Accept: application/json

Core Endpoints

Library Sections

GET /library/sections

Returns all libraries. Music libraries have type: "artist".

Music Library Rule For Radio

Plex radio must target a music library section. currentLibrary can point at movies or shows because Plex browsing is cross-media, so radio code must not blindly reuse it.

Actual app behavior:

  • Use currentLibrary only when currentLibrary.isMusicLibrary == true
  • Otherwise fall back to the first available music library from availableLibraries
  • If no music library exists, fail early and log that no music library is available

If a track-style Plex radio query is sent to a non-music section, Plex can return 200 OK with an empty metadata list, which looks like a radio failure even though the query shape is valid.

Query Items by Type

GET /library/sections/{libraryID}/all?type={typeID}

Type values:

Type IDContent
8Artists
9Albums
10Tracks
1Movies
2TV Shows

Library Browser Artist Grouping

Plex can expose multiple artist records with the same display name but different ratingKey values. In the Library Browser, normal Plex artist browsing shows one visible row per normalized artist name while keeping every underlying Plex artist record reachable.

Current behavior in both classic PlexBrowserView and modern ModernLibraryBrowserView:

  • Normalize artist names with trimmed whitespace, case-insensitive + diacritic-insensitive folding, and lowercasing (so e.g. "Motörhead" groups with "Motorhead"), then use plex-artist-{normalizedName} as the grouped display key.
  • Pick the visible representative from the same-name group by highest albumCount.
  • When expanding, playing, or queueing a grouped row, fan out across every artist record in the group and then deduplicate albums/tracks by stable metadata keys.
  • Search mode intentionally treats the selected Plex artist as a single record instead of expanding all same-name records.

Performance requirement: build group indexes once when artist/album counts are rebuilt. buildArtistAlbumCounts() should populate plexArtistGroupsByName, plexAlbumsByArtistGroupKey, and plexAlbumCountsByArtistGroupKey from cachedArtists and cachedAlbums. Expanding a grouped artist row should first use plexAlbumsByArtistGroupKey[groupKey] and avoid rescanning the full album cache on the main actor, because large same-name groups can otherwise beachball the UI.

Popular Tracks (Last.fm Integration)

Plex identifies "hit" tracks using the ratingCount field, which contains global popularity data from Last.fm - the number of unique listeners who have scrobbled the track worldwide.

Example: "Purple Haze" by Jimi Hendrix would have a high ratingCount (millions of scrobbles), while a deep cut might have significantly lower count or none.

Radio API

Sonically Similar Tracks (Primary Radio API)

GET /library/sections/{libraryID}/all?type=10&track.sonicallySimilar={trackID}&sort=random&limit=100

Returns tracks that are sonically similar to the seed track. Use sort=random for diverse results.

Sonically Similar Artists

GET /library/sections/{libraryID}/all?type=8&artist.sonicallySimilar={artistID}&limit=15

Sonically Similar Albums

GET /library/sections/{libraryID}/all?type=9&album.sonicallySimilar={albumID}&limit=10

Only the Hits Radio

Plays only popular/hit tracks based on Last.fm scrobble data.

Threshold: 1,000,000+ scrobbles

Sonic Version:

GET /library/sections/{libID}/all?type=10&track.sonicallySimilar={trackID}&ratingCount>=1000000&sort=random&limit=100

Non-Sonic Version:

GET /library/sections/{libID}/all?type=10&ratingCount>=1000000&sort=random&limit=100

Deep Cuts Radio

Plays lesser-known tracks.

Threshold: Under 1,000 scrobbles

Sonic Version:

GET /library/sections/{libID}/all?type=10&track.sonicallySimilar={trackID}&ratingCount<=999&sort=random&limit=100

Non-Sonic Version:

GET /library/sections/{libID}/all?type=10&ratingCount<=999&sort=random&limit=100

Rating Radio (My Ratings)

Plays tracks based on user's personal star ratings:

StationMin RatingDescription
5 Stars Radio10 (5★)Only tracks rated exactly 5 stars
4+ Stars Radio8 (4★+)Highly rated tracks (4-5 stars)
3+ Stars Radio6 (3★+)Good tracks (3-5 stars)
2+ Stars Radio4 (2★+)Any track rated 2+ stars
All Rated Radio0.1Any track with a rating

Note: Plex stores ratings on a 0-10 scale internally (10 = 5 stars).

Sonic Version:

GET /library/sections/{libID}/all?type=10&track.sonicallySimilar={trackID}&userRating>=8&sort=random&limit=100

Non-Sonic Version:

GET /library/sections/{libID}/all?type=10&userRating>=8&sort=random&limit=100

URL Encoding Warning

IMPORTANT: Plex filter operators (>=, <=, =, !=) must NOT be URL-encoded.

  • WRONG: userRating%3E%3D=8 (URLQueryItem encodes >= as %3E%3D)
  • CORRECT: userRating>=8 (literal >= in the URL)

Note: Plex only supports >=, <=, =, !=. The < and > operators (without equals) are NOT supported and return HTTP 400. Use <= with value-1 instead of < (e.g., ratingCount<=999 instead of ratingCount<1000).

When using Swift's URLQueryItem, build URLs manually for filter parameters:

// WRONG - URLQueryItem encodes >=
URLQueryItem(name: "userRating>=", value: "8")  // produces userRating%3E%3D=8

// CORRECT - manual URL construction
let urlString = "\(baseURL)/library/sections/\(id)/all?type=10&userRating>=8&..."

Filter Parameters

By Artist

?artist.id={artistID}

By Genre

?genre={genreID}

By Year/Decade

?year={year}
?year>=1980&year<=1989

Sorting Options

?sort=random            # Random order (great for radio)
?sort=titleSort         # Alphabetical
?sort=lastViewedAt:desc # Recently played
?sort=addedAt:desc      # Recently added
?sort=year:desc         # By year

Video Content (Movies & TV Shows)

Fetching Movies

GET /library/sections/{libraryID}/all?type=1

Fetching TV Shows

GET /library/sections/{libraryID}/all?type=2

Multi-Version Movies

Movies may contain multiple media versions:

Important: The top-level duration field may point to bonus content, not the main movie. NullPlayer uses the longest duration from the Media array to identify primary content.

External IDs (IMDB, TMDB, TVDB)

Movies, TV shows, and episodes include external service IDs in the Guid array:

{
  "title": "The Matrix",
  "Guid": [
    {"id": "imdb://tt0133093"},
    {"id": "tmdb://603"},
    {"id": "tvdb://12345"}
  ]
}
ServiceURL Pattern
IMDBhttps://www.imdb.com/title/{id}/
TMDBhttps://www.themoviedb.org/movie/{id} (movies)
TMDBhttps://www.themoviedb.org/tv/{id} (TV shows)
TVDBhttps://www.thetvdb.com/series/{id}

NullPlayer uses these IDs to provide "View Online" context menu links.

Setting User Ratings

PUT /:/rate?key={ratingKey}&identifier=com.plexapp.plugins.library&rating={rating}
ParameterDescription
keyThe item's ratingKey
identifierAlways com.plexapp.plugins.library
rating0-10 scale (2 per star), or -1 to clear

Rating Scale:

  • 2 = 1 star, 4 = 2 stars, 6 = 3 stars, 8 = 4 stars, 10 = 5 stars, -1 = Clear

Building Streaming URLs

{baseURL}{Media.Part.key}?X-Plex-Token={token}

Example:

http://192.168.0.102:32400/library/parts/653835/1723508508/file.flac?X-Plex-Token=TOKEN

Sonos Casting Metadata

Plex stream URLs can be extensionless or otherwise not reliable for format detection. When converting PlexTrack to Track, preserve Plex audio format metadata in Track.contentType using codec/container fields (audioCodec, Media.container, then Part.container). This keeps the existing Sonos compatibility path working for extensionless streams.

Important cases:

  • flac -> audio/flac; extensionless 24/96 or 24/192 FLAC must be identified as FLAC so CastManager.isSonosCompatible applies the 48 kHz Sonos limit.
  • alac -> audio/mp4, matching the shared cast content-type mapping used for Apple Lossless in MP4 containers.
  • Restored Plex tracks from StreamingTrackResolver must populate contentType the same way as PlexManager.convertToTrack.

For lossless Plex tracks with nil sampleRate, castCurrentTrack and castNewTrack fetch the sample rate from Plex by rating key before making the final strict Sonos compatibility decision. That fetch must be triggered from URL extension or Track.contentType, because Plex stream URLs may not have .flac or .wav extensions.

Requirements

  • Plex Pass subscription (for sonic analysis)
  • Plex Media Server v1.24.0+ (64-bit)
  • Sonic analysis enabled on the music library
  • Tracks must be analyzed (check for musicAnalysisVersion attribute)

Plex Radio History

Tracks played during Plex Radio sessions are recorded in a local SQLite database so they can be filtered out of future radio queues (preventing repeats) and viewed by the user.

Storage

  • Database: ~/Library/Application Support/NullPlayer/plex_radio_history.db
  • Table: plex_radio_history
  • Unique constraint: (plex_rating_key, plex_server_id) — one record per track per server, upserted on each play (updates played_at)
  • Indexes: played_at (for retention cutoff queries), normalized_key (for fallback matching)

Deduplication / Normalized Key

Radio library rescans can change a track's ratingKey. To still suppress known tracks, a normalized_key ("artist|title" lowercased, trimmed) is stored alongside the rating key. filterOutHistoryTracks does a two-pass filter:

  1. Match by plexRatingKey (exact)
  2. Match by normalized_key (fuzzy fallback)

Retention

Configurable via PlexRadioHistoryInterval enum:

ValueDuration
offDisabled (no filtering)
twoWeeks2 weeks
oneMonth1 month (default)
threeMonths3 months
sixMonths6 months

Stored in UserDefaults key plexRadioHistoryInterval. When off, filterOutHistoryTracks returns the input unchanged.

Recording

PlexRadioHistory.shared.recordTrackPlayed(_:) is called from AudioEngine when a Plex track finishes — at natural track completion and during gapless crossfade. Uses track.plexServerId (set by PlexManager.convertToTrack), falling back to PlexManager.shared.currentServer?.id.

Filtering

PlexRadioHistory.shared.filterOutHistoryTracks(_:) is called inside PlexManager when building radio queues — after fetching tracks from the Plex API, before returning them to the engine.

Context Menu (User UI)

Under Playback Options → Radio History:

  • Plex History Retention submenu — sets retentionInterval (Off / 2 Weeks / 1 Month / 3 Months / 6 Months)
  • View Plex Radio History — opens http://127.0.0.1:8765/radio-history in the default browser
  • Clear Plex Radio History — confirmation dialog, then clearHistory()

Web History Page

Served by LocalMediaServer on port 8765. Routes:

  • GET /radio-history — returns HTML from generateHistoryHTML()
  • POST /radio-history/delete/:id — removes a single entry by rowid

The page is a dark-themed sortable table (Track / Artist / Album / Track ID / Played). Each row has a Remove button that calls the delete endpoint via fetch(). No page reload needed.

Key Source Files

FilePurpose
Plex/PlexManager.swiftSingleton managing connections, caching, track conversion
Plex/PlexServerClient.swiftHTTP client for Plex REST API
Plex/PlexModels.swiftDomain models (Server, Artist, Album, Track, etc.)
Plex/PlexPlaybackReporter.swiftAudio scrobbling and "now playing" reporting
Plex/PlexVideoPlaybackReporter.swiftVideo scrobbling with periodic timeline updates
Plex/PlexRadioHistory.swiftSQLite-backed play history for Plex Radio — recording, filtering, web UI

Implementation Gotchas

API Filter Operators — Build URLs Manually

URLQueryItem will URL-encode operators like >=, <= which breaks Plex filtering. Build URLs manually for filter params. Note: Plex only supports >=, <=, =, != operators — NOT < or > (use <= with value-1 instead):

// WRONG - URLQueryItem encodes >= as %3E%3D, Plex ignores the filter:
URLQueryItem(name: "userRating>=", value: "8")

// WRONG - Plex doesn't support < operator (returns 400 Bad Request):
let url = "...&ratingCount<1000&..."

// CORRECT - manual URL with literal operators:
let url = "\(baseURL)/library/sections/\(id)/all?type=10&userRating>=8&..."

// CORRECT - use <= with threshold-1 instead of <:
let url = "...&ratingCount<=999&..."  // equivalent to <1000

References

Signals

GitHub stars
120
Forks
9
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
plex-integration
Source
github.com/ad-repo/nullplayer