117 lines
4.6 KiB
Swift
117 lines
4.6 KiB
Swift
import Foundation
|
|
|
|
/// One music server the app reads from. The first one ever added has an empty [id], so its
|
|
/// keys stay bare and everything saved before multi-server support (downloads, mixes, stem
|
|
/// playlists) still matches. Every other server's keys carry "<id>~" in front.
|
|
struct MediaServer: Codable, Hashable, Identifiable {
|
|
enum Kind: String, Codable { case plex, jellyfin }
|
|
|
|
var id = ""
|
|
var kind = Kind.plex
|
|
var name = ""
|
|
var url = ""
|
|
var token = ""
|
|
var user = ""
|
|
/// Jellyfin user id; Plex leaves it empty.
|
|
var userId = ""
|
|
var machineId = ""
|
|
var sections: [String] = []
|
|
var sectionTitles = ""
|
|
|
|
var signedIn: Bool { !token.isEmpty }
|
|
var ready: Bool { signedIn && !sections.isEmpty }
|
|
var base: String { url.trimmingCharacters(in: CharacterSet(charactersIn: "/")) }
|
|
var label: String { name.isEmpty ? (kind == .plex ? "Plex" : "Jellyfin") : name }
|
|
var kindLabel: String { kind == .plex ? "Plex" : "Jellyfin" }
|
|
|
|
static func newId() -> String { "s" + String(UUID().uuidString.prefix(6)).lowercased() }
|
|
}
|
|
|
|
/// What a single server can do. [PlexClient] pools these and tags their keys.
|
|
protocol MediaSource: AnyObject, Sendable {
|
|
var server: MediaServer { get }
|
|
func artists(sort: String) async throws -> [Artist]
|
|
func albums(sort: String) async throws -> [Album]
|
|
func recentlyAdded(count: Int) async throws -> [Album]
|
|
func recentlyPlayed(count: Int) async throws -> [Album]
|
|
func recentlyAddedTracks(count: Int) async throws -> [Track]
|
|
func randomAlbums(_ count: Int) async throws -> [Album]
|
|
func tracksPage(start: Int, size: Int, sort: String) async throws -> ([Track], Int)
|
|
func randomTracks(_ count: Int) async throws -> [Track]
|
|
func album(_ key: String) async throws -> Album?
|
|
func albumTracks(_ key: String) async throws -> [Track]
|
|
func artistAlbums(_ key: String) async throws -> [Album]
|
|
func artistTracks(_ key: String) async throws -> [Track]
|
|
func searchTracks(_ title: String) async throws -> [Track]
|
|
func loudness(_ trackKey: String, samples: Int) async throws -> [Float]?
|
|
func search(_ query: String) async throws -> SearchResults
|
|
func playlists(sort: String) async throws -> [Playlist]
|
|
func playlistTracks(_ key: String) async throws -> [Track]
|
|
func createPlaylist(_ title: String, trackKeys: [String]) async throws -> Playlist?
|
|
func addToPlaylist(_ key: String, trackKeys: [String]) async throws
|
|
func removeFromPlaylist(_ key: String, playlistItemId: Int64) async throws
|
|
func movePlaylistItem(_ key: String, playlistItemId: Int64, afterItemId: Int64?) async throws
|
|
func renamePlaylist(_ key: String, title: String) async throws
|
|
func deletePlaylist(_ key: String) async throws
|
|
func scrobble(_ trackKey: String) async
|
|
func timeline(_ track: Track, state: String, positionMs: Int64) async
|
|
func artUrl(_ thumb: String?, px: Int) -> URL?
|
|
func partUrl(_ partKey: String) -> URL?
|
|
/// Music libraries on the server, for the picker in Settings.
|
|
func musicSections() async throws -> [MusicSection]
|
|
}
|
|
|
|
// ---- key tagging -----------------------------------------------------------------
|
|
|
|
extension String {
|
|
func tagged(_ id: String) -> String { id.isEmpty ? self : id + "~" + self }
|
|
}
|
|
|
|
extension Optional where Wrapped == String {
|
|
func tagged(_ id: String) -> String? { map { $0.tagged(id) } }
|
|
}
|
|
|
|
extension Track {
|
|
func tagged(_ id: String) -> Track {
|
|
guard !id.isEmpty else { return self }
|
|
var t = self
|
|
t.ratingKey = ratingKey.tagged(id); t.albumKey = albumKey.tagged(id); t.artistKey = artistKey.tagged(id)
|
|
t.thumb = thumb.tagged(id); t.partKey = partKey.tagged(id)
|
|
return t
|
|
}
|
|
}
|
|
|
|
extension Album {
|
|
func tagged(_ id: String) -> Album {
|
|
guard !id.isEmpty else { return self }
|
|
var a = self
|
|
a.ratingKey = ratingKey.tagged(id); a.artistKey = artistKey.tagged(id); a.thumb = thumb.tagged(id)
|
|
return a
|
|
}
|
|
}
|
|
|
|
extension Artist {
|
|
func tagged(_ id: String) -> Artist {
|
|
guard !id.isEmpty else { return self }
|
|
var a = self
|
|
a.ratingKey = ratingKey.tagged(id); a.thumb = thumb.tagged(id)
|
|
return a
|
|
}
|
|
}
|
|
|
|
extension Playlist {
|
|
func tagged(_ id: String) -> Playlist {
|
|
guard !id.isEmpty else { return self }
|
|
var p = self
|
|
p.ratingKey = ratingKey.tagged(id); p.thumb = thumb.tagged(id)
|
|
return p
|
|
}
|
|
}
|
|
|
|
extension SearchResults {
|
|
func tagged(_ id: String) -> SearchResults {
|
|
SearchResults(artists: artists.map { $0.tagged(id) }, albums: albums.map { $0.tagged(id) },
|
|
tracks: tracks.map { $0.tagged(id) }, playlists: playlists.map { $0.tagged(id) })
|
|
}
|
|
}
|