iOS 1.0 RC 2: Plex streams under upload caps, shuffle and album radio stations, Recently Added list, photo covers, crossfade
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0e33b5e3e
commit
2fb8040388
17 files changed
+412
-74
No files matched your search
@@ -440,6 +440,8 @@ final class JellyfinClient: MediaSource, @unchecked Sendable {
|
||||
URL(string: base + "/Audio/\(partKey)/stream?static=true&api_key=" + urlEncode(server.token))
|
||||
}
|
||||
|
||||
func streamUrl(_ ratingKey: String, partKey: String) async -> URL? { partUrl(partKey) }
|
||||
|
||||
/// True if [url] answers as a Jellyfin server.
|
||||
func probe() async -> Bool {
|
||||
guard let u = URL(string: base + "/System/Info/Public") else { return false }
|
||||
|
||||
@@ -57,6 +57,8 @@ protocol MediaSource: AnyObject, Sendable {
|
||||
func timeline(_ track: Track, state: String, positionMs: Int64) async
|
||||
func artUrl(_ thumb: String?, px: Int) -> URL?
|
||||
func partUrl(_ partKey: String) -> URL?
|
||||
/// The URL to stream a track's file from, after any setup the server needs for playback.
|
||||
func streamUrl(_ ratingKey: String, partKey: String) async -> URL?
|
||||
/// Music libraries on the server, for the picker in Settings.
|
||||
func musicSections() async throws -> [MusicSection]
|
||||
}
|
||||
|
||||
@@ -141,18 +141,6 @@ final class PlexClient: @unchecked Sendable {
|
||||
return (out, total)
|
||||
}
|
||||
|
||||
/// Every track in the library, in pages, shuffled.
|
||||
func allTracksShuffled() async throws -> [Track] {
|
||||
let size = 2000
|
||||
var (out, total) = try await tracksPage(start: 0, size: size)
|
||||
while out.count < total {
|
||||
let (page, _) = try await tracksPage(start: out.count, size: size)
|
||||
if page.isEmpty { break }
|
||||
out += page
|
||||
}
|
||||
return out.shuffled()
|
||||
}
|
||||
|
||||
func randomTracks(_ count: Int) async throws -> [Track] {
|
||||
let each = count / max(sources.count, 1) + 1
|
||||
return Array(try await pool { s in try await s.randomTracks(each).map { $0.tagged(s.server.id) } }.shuffled().prefix(count))
|
||||
@@ -265,6 +253,7 @@ final class PlexClient: @unchecked Sendable {
|
||||
// ---- URLs -----------------------------------------------------------------------
|
||||
|
||||
func artUrl(_ thumb: String?, px: Int) -> URL? {
|
||||
if let thumb, thumb.hasPrefix(StemPlaylists.localPrefix) { return StemPlaylists.localArt(thumb) }
|
||||
guard let thumb, !thumb.isEmpty, let r = route(thumb) else { return nil }
|
||||
let (s, k) = r
|
||||
return s.artUrl(k, px: px)
|
||||
@@ -276,6 +265,26 @@ final class PlexClient: @unchecked Sendable {
|
||||
return s.partUrl(k)
|
||||
}
|
||||
|
||||
/// [track]'s file, set up for playback on its server (see PlexServer.streamUrl).
|
||||
func streamUrl(_ track: Track) async -> URL? {
|
||||
guard let part = track.partKey, let r = route(part) else { return nil }
|
||||
let (s, k) = r
|
||||
let rk = route(track.ratingKey)?.1 ?? track.ratingKey
|
||||
return await s.streamUrl(rk, partKey: k)
|
||||
}
|
||||
|
||||
/// Blocking form of [streamUrl] for worker threads.
|
||||
func streamUrlNow(_ track: Track) -> URL? {
|
||||
let box = UrlBox()
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
Task {
|
||||
box.url = await streamUrl(track)
|
||||
sem.signal()
|
||||
}
|
||||
sem.wait()
|
||||
return box.url
|
||||
}
|
||||
|
||||
// ---- sign-in helpers --------------------------------------------------------------
|
||||
|
||||
/// A Plex client for plex.tv calls and for a server that isn't saved yet.
|
||||
@@ -333,3 +342,5 @@ final class PlexClient: @unchecked Sendable {
|
||||
|
||||
func jellyfin(_ s: MediaServer) -> JellyfinClient { JellyfinClient(http: http, settings: settings, server: s) }
|
||||
}
|
||||
|
||||
private final class UrlBox: @unchecked Sendable { var url: URL? }
|
||||
@@ -317,6 +317,32 @@ final class PlexServer: MediaSource, @unchecked Sendable {
|
||||
URL(string: base + partKey + "?X-Plex-Token=" + urlEncode(token))
|
||||
}
|
||||
|
||||
/// The part's URL the way Plexamp plays it: first a playback decision for this session, then
|
||||
/// the file fetched under that session and client. A bare part request has no bitrate, so Plex
|
||||
/// reads it as unlimited ("Bandwidth exceeded: 2147483647 kbps") and answers 500 whenever
|
||||
/// Remote Access has an upload limit set.
|
||||
func streamUrl(_ ratingKey: String, partKey: String) async -> URL? {
|
||||
let session = UUID().uuidString.lowercased()
|
||||
if signedIn, let u = try? url("/music/:/transcode/universal/decision", [
|
||||
("path", "/library/metadata/\(ratingKey)"), ("mediaIndex", 0), ("partIndex", 0),
|
||||
("protocol", "http"), ("directPlay", 1), ("directStream", 1), ("directStreamAudio", 1),
|
||||
("hasMDE", 1), ("session", session), ("X-Plex-Session-Identifier", session),
|
||||
]) {
|
||||
var req = request(u)
|
||||
req.setValue(session, forHTTPHeaderField: "X-Plex-Session-Identifier")
|
||||
req.timeoutInterval = 8
|
||||
// Best effort: the part URL below still carries the session if this fails.
|
||||
if let (_, resp) = try? await http.session.data(for: req), let code = (resp as? HTTPURLResponse)?.statusCode, code >= 300 {
|
||||
Log.shared.error("plex", "stream decision for \(ratingKey): HTTP \(code)")
|
||||
}
|
||||
}
|
||||
guard let u = URL(string: base + partKey) else { return nil }
|
||||
return u.with([
|
||||
("X-Plex-Token", token), ("X-Plex-Client-Identifier", settings.clientId), ("X-Plex-Product", Self.product),
|
||||
("X-Plex-Platform", "iOS"), ("X-Plex-Device", model), ("X-Plex-Session-Identifier", session), ("session", session),
|
||||
])
|
||||
}
|
||||
|
||||
// ---- plex.tv sign-in ------------------------------------------------------------
|
||||
|
||||
func createPin() async throws -> PlexPin {
|
||||
|
||||
Reference in new issue
Block a user