Implement remote library metadata sync for iPhone

This commit is contained in:
diyaa
2026-05-29 08:54:09 +02:00
parent ebc187f4a1
commit 6e73c1878e
20 changed files with 1379 additions and 48 deletions
@@ -0,0 +1,54 @@
import Foundation
import VelodyDomain
public struct RemoteTrackDTO: Codable, Hashable, Sendable {
public var trackId: String
public var title: String
public var artist: String
public var durationSeconds: Int
public var sha256: String
public var assetId: String
public var createdAt: String
public var updatedAt: String
public init(
trackId: String,
title: String,
artist: String,
durationSeconds: Int,
sha256: String,
assetId: String,
createdAt: String,
updatedAt: String
) {
self.trackId = trackId
self.title = title
self.artist = artist
self.durationSeconds = durationSeconds
self.sha256 = sha256
self.assetId = assetId
self.createdAt = createdAt
self.updatedAt = updatedAt
}
public var remoteTrack: RemoteTrack {
RemoteTrack(
trackId: trackId,
title: title,
artist: artist,
durationSeconds: durationSeconds,
sha256: sha256,
assetId: assetId,
createdAt: createdAt,
updatedAt: updatedAt
)
}
}
public struct RemoteLibraryResponseDTO: Codable, Hashable, Sendable {
public var tracks: [RemoteTrackDTO]
public init(tracks: [RemoteTrackDTO]) {
self.tracks = tracks
}
}
@@ -39,6 +39,10 @@ public protocol VelodyAPIClient: Sendable {
func fetchSyncBootstrap() async throws -> SyncBootstrapResponse
func fetchRemoteLibrary(
deviceId: String
) async throws -> RemoteLibraryResponseDTO
func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse
@@ -106,6 +110,19 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
)
}
public func fetchRemoteLibrary(
deviceId: String
) async throws -> RemoteLibraryResponseDTO {
try await sendRequest(
method: "GET",
pathComponents: ["api", "v1", "library", "tracks"],
queryItems: [
URLQueryItem(name: "deviceId", value: deviceId),
],
responseType: RemoteLibraryResponseDTO.self
)
}
public func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse {
@@ -139,6 +156,7 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
let request = try buildRequest(
method: "PUT",
pathComponents: ["api", "v1", "uploads", uploadId, "file"],
queryItems: [],
bodyData: nil,
contentType: mimeType
)
@@ -174,11 +192,13 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
private func sendRequest<Response: Decodable>(
method: String,
pathComponents: [String],
queryItems: [URLQueryItem] = [],
responseType: Response.Type
) async throws -> Response {
let request = try buildRequest(
method: method,
pathComponents: pathComponents,
queryItems: queryItems,
bodyData: nil
)
@@ -188,6 +208,7 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
private func sendRequest<Body: Encodable, Response: Decodable>(
method: String,
pathComponents: [String],
queryItems: [URLQueryItem] = [],
body: Body,
responseType: Response.Type
) async throws -> Response {
@@ -202,6 +223,7 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
let request = try buildRequest(
method: method,
pathComponents: pathComponents,
queryItems: queryItems,
bodyData: bodyData,
contentType: "application/json"
)
@@ -212,10 +234,14 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
private func buildRequest(
method: String,
pathComponents: [String],
queryItems: [URLQueryItem],
bodyData: Data?,
contentType: String? = nil
) throws -> URLRequest {
guard let url = endpointURL(pathComponents: pathComponents) else {
guard let url = endpointURL(
pathComponents: pathComponents,
queryItems: queryItems
) else {
throw VelodyAPIError.invalidServerURL(environment.baseURL.absoluteString)
}
@@ -284,10 +310,24 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
}
}
private func endpointURL(pathComponents: [String]) -> URL? {
pathComponents.reduce(environment.baseURL) { partialURL, component in
private func endpointURL(
pathComponents: [String],
queryItems: [URLQueryItem]
) -> URL? {
let baseURL = pathComponents.reduce(environment.baseURL) { partialURL, component in
partialURL.appendingPathComponent(component, isDirectory: false)
}
guard !queryItems.isEmpty else {
return baseURL
}
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
return nil
}
components.queryItems = queryItems
return components.url
}
}
@@ -340,6 +380,27 @@ public struct StubVelodyAPIClient: VelodyAPIClient {
)
}
public func fetchRemoteLibrary(
deviceId: String
) async throws -> RemoteLibraryResponseDTO {
_ = deviceId
return RemoteLibraryResponseDTO(
tracks: [
RemoteTrackDTO(
trackId: UUID().uuidString,
title: "Velody Remote Placeholder",
artist: "Private Library",
durationSeconds: 245,
sha256: String(repeating: "a", count: 64),
assetId: UUID().uuidString,
createdAt: ISO8601DateFormatter().string(from: .now),
updatedAt: ISO8601DateFormatter().string(from: .now)
),
]
)
}
public func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse {