Add MP3 upload pipeline foundation

This commit is contained in:
diyaa
2026-05-28 17:43:00 +02:00
parent 799c07b068
commit 98edfa0faf
24 changed files with 2469 additions and 93 deletions
@@ -38,6 +38,25 @@ public protocol VelodyAPIClient: Sendable {
) async throws -> DeviceHeartbeatResponse
func fetchSyncBootstrap() async throws -> SyncBootstrapResponse
func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse
func fetchUploadStatus(
uploadId: String
) async throws -> UploadSessionStatusResponse
func uploadFile(
uploadId: String,
fileURL: URL,
mimeType: String
) async throws -> UploadSessionStatusResponse
func finalizeUpload(
uploadId: String,
payload: UploadFinalizeRequest
) async throws -> UploadFinalizeResponse
}
public struct URLSessionVelodyAPIClient: VelodyAPIClient {
@@ -87,6 +106,71 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
)
}
public func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse {
try await sendRequest(
method: "POST",
pathComponents: ["api", "v1", "uploads", "prepare"],
body: payload,
responseType: UploadPrepareResponse.self
)
}
public func fetchUploadStatus(
uploadId: String
) async throws -> UploadSessionStatusResponse {
try await sendRequest(
method: "GET",
pathComponents: ["api", "v1", "uploads", uploadId],
responseType: UploadSessionStatusResponse.self
)
}
public func uploadFile(
uploadId: String,
fileURL: URL,
mimeType: String = "audio/mpeg"
) async throws -> UploadSessionStatusResponse {
guard FileManager.default.fileExists(atPath: fileURL.path) else {
throw VelodyAPIError.requestFailed("The selected file could not be found.")
}
let request = try buildRequest(
method: "PUT",
pathComponents: ["api", "v1", "uploads", uploadId, "file"],
bodyData: nil,
contentType: mimeType
)
let data: Data
let response: URLResponse
do {
(data, response) = try await session.upload(for: request, fromFile: fileURL)
} catch {
throw VelodyAPIError.requestFailed(error.localizedDescription)
}
return try decodeResponse(
data: data,
response: response,
responseType: UploadSessionStatusResponse.self
)
}
public func finalizeUpload(
uploadId: String,
payload: UploadFinalizeRequest
) async throws -> UploadFinalizeResponse {
try await sendRequest(
method: "POST",
pathComponents: ["api", "v1", "uploads", uploadId, "finalize"],
body: payload,
responseType: UploadFinalizeResponse.self
)
}
private func sendRequest<Response: Decodable>(
method: String,
pathComponents: [String],
@@ -118,7 +202,8 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
let request = try buildRequest(
method: method,
pathComponents: pathComponents,
bodyData: bodyData
bodyData: bodyData,
contentType: "application/json"
)
return try await execute(request, responseType: responseType)
@@ -127,7 +212,8 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
private func buildRequest(
method: String,
pathComponents: [String],
bodyData: Data?
bodyData: Data?,
contentType: String? = nil
) throws -> URLRequest {
guard let url = endpointURL(pathComponents: pathComponents) else {
throw VelodyAPIError.invalidServerURL(environment.baseURL.absoluteString)
@@ -139,7 +225,10 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
if let bodyData {
request.httpBody = bodyData
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
if let contentType {
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
}
return request
@@ -158,6 +247,28 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
throw VelodyAPIError.requestFailed(error.localizedDescription)
}
return try decodeResponse(
data: data,
response: response,
responseType: responseType
)
}
private func decodeResponse<Response: Decodable>(
data: Data,
response: URLResponse,
responseType: Response.Type
) throws -> Response {
try validate(response: response, data: data)
do {
return try decoder.decode(responseType, from: data)
} catch {
throw VelodyAPIError.decodingFailed(error.localizedDescription)
}
}
private func validate(response: URLResponse, data: Data) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw VelodyAPIError.invalidResponse
}
@@ -171,12 +282,6 @@ public struct URLSessionVelodyAPIClient: VelodyAPIClient {
message: message?.isEmpty == true ? nil : message
)
}
do {
return try decoder.decode(responseType, from: data)
} catch {
throw VelodyAPIError.decodingFailed(error.localizedDescription)
}
}
private func endpointURL(pathComponents: [String]) -> URL? {
@@ -234,4 +339,58 @@ public struct StubVelodyAPIClient: VelodyAPIClient {
serverTime: ISO8601DateFormatter().string(from: .now)
)
}
public func prepareUpload(
_ payload: UploadPrepareRequest
) async throws -> UploadPrepareResponse {
_ = payload
return UploadPrepareResponse(
status: .uploadRequired,
uploadId: UUID().uuidString,
nextOffset: 0
)
}
public func fetchUploadStatus(
uploadId: String
) async throws -> UploadSessionStatusResponse {
UploadSessionStatusResponse(
uploadId: uploadId,
status: .completed,
receivedBytes: "0",
expectedSizeBytes: "0",
nextOffset: "0"
)
}
public func uploadFile(
uploadId: String,
fileURL: URL,
mimeType: String
) async throws -> UploadSessionStatusResponse {
_ = fileURL
_ = mimeType
return UploadSessionStatusResponse(
uploadId: uploadId,
status: .completed,
receivedBytes: "0",
expectedSizeBytes: "0",
nextOffset: "0"
)
}
public func finalizeUpload(
uploadId: String,
payload: UploadFinalizeRequest
) async throws -> UploadFinalizeResponse {
_ = uploadId
_ = payload
return UploadFinalizeResponse(
trackId: UUID().uuidString,
assetId: UUID().uuidString
)
}
}