Files

84 lines
3.2 KiB
Swift

import Domain
import Foundation
import Synchronization
public struct SubsonicAccount: Codable, Hashable, Sendable {
public var baseURL: URL
public var username: String
public init(baseURL: URL, username: String) {
self.baseURL = baseURL
self.username = username
}
}
public final class SubsonicSource: LibrarySource, Starring, LyricsSource, Scrobbling {
public let id: SourceID
public let name: String
let client: SubsonicClient
private let mapper: SubsonicMapper
private let memory = Mutex(SubsonicMemory())
public convenience init(id: SourceID, account: SubsonicAccount, password: String, name: String, session: URLSession = .shared) {
let client = SubsonicClient(baseURL: account.baseURL, username: account.username, password: password, session: session)
self.init(id: id, name: name, client: client)
}
init(id: SourceID, name: String, client: SubsonicClient) {
self.id = id
self.name = name
self.client = client
mapper = SubsonicMapper(source: id)
}
public func verify() async throws {
try await client.call("ping")
}
public func changes(since _: Cursor?) -> AsyncThrowingStream<Change, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
let start = Date.now
var crawl = SubsonicCrawl(client: client, mapper: mapper) { continuation.yield($0) }
let result = try await crawl.run(previous: memory.withLock(\.seen))
memory.withLock { $0 = result }
continuation.yield(.checkpoint(Cursor(start.ISO8601Format())))
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
public func stream(for track: EntityID) async throws -> StreamRequest {
let url = client.url("stream", ["id": track.remote, "format": "raw"])
return StreamRequest(resource: ResourceRequest(url: url), format: memory.withLock { $0.formats[track.remote] })
}
public func artwork(_ id: EntityID, pixelSize: Int) async throws -> ResourceRequest {
ResourceRequest(url: client.url("getCoverArt", ["id": id.remote, "size": String(pixelSize)]))
}
public func setFavorite(_ isFavorite: Bool, for id: EntityID, kind: EntityKind) async throws {
let param = switch kind {
case .album: "albumId"
case .artist: "artistId"
default: "id"
}
try await client.call(isFavorite ? "star" : "unstar", [param: id.remote])
}
public func lyrics(for track: EntityID) async throws -> Lyrics? {
let payload = try await client.call("getLyricsBySongId", ["id": track.remote], as: LyricsPayload.self)
return mapper.lyrics(payload.lyricsList.structuredLyrics ?? [])
}
public func scrobble(_ track: EntityID, at date: Date, submission: Bool) async throws {
let time = String(Int(date.timeIntervalSince1970 * 1000))
try await client.call("scrobble", ["id": track.remote, "time": time, "submission": submission ? "true" : "false"])
}
}