feat(*): subsonic client with avfoundation playback and apple music ui
This commit is contained in:
commit
e5bb559d7f
93 files changed
+7517
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import Domain
|
||||
import Foundation
|
||||
@testable import SourceSubsonic
|
||||
|
||||
struct FixtureTransport: Sendable {
|
||||
var droppedAlbum: String?
|
||||
|
||||
static func fixture(_ name: String) throws -> Data {
|
||||
let url = try unwrap(Bundle.module.url(forResource: name, withExtension: "json", subdirectory: "Fixtures"))
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
|
||||
static func empty(_ key: String) -> Data {
|
||||
Data(#"{"subsonic-response":{"status":"ok","version":"1.16.1","\#(key)":{}}}"#.utf8)
|
||||
}
|
||||
|
||||
static func method(of request: URLRequest) -> (String, [String: String]) {
|
||||
let url = request.url ?? URL(filePath: "/")
|
||||
let method = url.lastPathComponent.replacingOccurrences(of: ".view", with: "")
|
||||
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
return (method, Dictionary(items.map { ($0.name, $0.value ?? "") }) { $1 })
|
||||
}
|
||||
|
||||
func callAsFunction(_ request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
let (method, params) = Self.method(of: request)
|
||||
let response = HTTPURLResponse(url: request.url ?? URL(filePath: "/"), statusCode: 200, httpVersion: nil, headerFields: nil)
|
||||
return try (data(for: method, params: params), response ?? URLResponse())
|
||||
}
|
||||
|
||||
private func data(for method: String, params: [String: String]) throws -> Data {
|
||||
switch method {
|
||||
case "getArtists": try Self.fixture("artists")
|
||||
case "search3" where params["songOffset"] == "0": try Self.fixture("search3All")
|
||||
case "search3": Self.empty("searchResult3")
|
||||
case "getAlbumList2" where params["offset"] == "0": try albums()
|
||||
case "getAlbumList2": Self.empty("albumList2")
|
||||
case "getPlaylists": try Self.fixture("playlists")
|
||||
case "getPlaylist": try Self.fixture("playlist")
|
||||
case "getInternetRadioStations": try Self.fixture("radioStations")
|
||||
case "getStarred2": try Self.fixture("starred2")
|
||||
case "getLyricsBySongId": try Self.fixture("lyricsEmpty")
|
||||
default: try Self.fixture("ping")
|
||||
}
|
||||
}
|
||||
|
||||
private func albums() throws -> Data {
|
||||
let data = try Self.fixture("albumList2All")
|
||||
guard let droppedAlbum else { return data }
|
||||
var root = try unwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
var response = try unwrap(root["subsonic-response"] as? [String: Any])
|
||||
var list = try unwrap(response["albumList2"] as? [String: Any])
|
||||
let albums = try unwrap(list["album"] as? [[String: Any]])
|
||||
list["album"] = albums.filter { $0["id"] as? String != droppedAlbum }
|
||||
response["albumList2"] = list
|
||||
root["subsonic-response"] = response
|
||||
return try JSONSerialization.data(withJSONObject: root)
|
||||
}
|
||||
}
|
||||
|
||||
extension SubsonicSource {
|
||||
static func fixture(transport: FixtureTransport = FixtureTransport(), id: SourceID = SourceID()) -> SubsonicSource {
|
||||
let client = SubsonicClient(
|
||||
baseURL: URL(string: "https://music.example.test")!,
|
||||
username: "h",
|
||||
password: "secret",
|
||||
transport: { try await transport($0) }
|
||||
)
|
||||
return SubsonicSource(id: id, name: "Test", client: client)
|
||||
}
|
||||
|
||||
func collect() async throws -> [Change] {
|
||||
var collected: [Change] = []
|
||||
for try await change in changes(since: nil) {
|
||||
collected.append(change)
|
||||
}
|
||||
return collected
|
||||
}
|
||||
}
|
||||
|
||||
private func unwrap<T>(_ value: T?) throws -> T {
|
||||
guard let value else { throw FixtureError.missing }
|
||||
return value
|
||||
}
|
||||
|
||||
private enum FixtureError: Error {
|
||||
case missing
|
||||
}
|
||||
Reference in new issue
Block a user