178 lines
5.3 KiB
Swift
178 lines
5.3 KiB
Swift
import Foundation
|
|
|
|
struct UserResultSummary: Identifiable, Decodable {
|
|
let attemptId: String
|
|
let videoHash: String
|
|
let courseTitle: String
|
|
let sentenceIndex: Int
|
|
let sentenceText: String
|
|
let overallScore: Float
|
|
let passed: Bool
|
|
let createdAt: String
|
|
|
|
var id: String { attemptId }
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case overallScore = "overall_score"
|
|
case courseTitle = "course_title"
|
|
case sentenceIndex = "sentence_index"
|
|
case sentenceText = "sentence_text"
|
|
case createdAt = "created_at"
|
|
case attemptId = "attempt_id"
|
|
case videoHash = "video_hash"
|
|
case passed
|
|
}
|
|
}
|
|
|
|
struct UserDubShareSummary: Identifiable, Decodable {
|
|
let shareId: String
|
|
let videoHash: String
|
|
let courseTitle: String
|
|
let title: String
|
|
let segmentCount: Int
|
|
let averageScore: Float
|
|
let createdAt: String
|
|
|
|
var id: String { shareId }
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case averageScore = "average_score"
|
|
case courseTitle = "course_title"
|
|
case segmentCount = "segment_count"
|
|
case createdAt = "created_at"
|
|
case shareId = "share_id"
|
|
case videoHash = "video_hash"
|
|
case title
|
|
}
|
|
}
|
|
|
|
struct DubShareSegmentDetail: Decodable {
|
|
let index: Int
|
|
let overallScore: Float
|
|
let recognizedText: String
|
|
let scoreDetails: [String: Float]
|
|
let audioURL: String
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case overallScore = "overall_score"
|
|
case recognizedText = "recognized_text"
|
|
case scoreDetails = "score_details"
|
|
case audioURL = "audio_url"
|
|
case index
|
|
}
|
|
}
|
|
|
|
struct DubShareDetail: Decodable {
|
|
let shareId: String
|
|
let videoHash: String
|
|
let title: String
|
|
let segments: [DubShareSegmentDetail]
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case shareId = "share_id"
|
|
case videoHash = "video_hash"
|
|
case title
|
|
case segments
|
|
}
|
|
}
|
|
|
|
enum UserHistoryError: LocalizedError {
|
|
case badStatus(Int, String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .badStatus(let code, let body):
|
|
return "HTTP \(code): \(body.prefix(200))"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum UserHistoryAPI {
|
|
static func results(token: String) async throws -> [UserResultSummary] {
|
|
try await getJSON([UserResultSummary].self, path: "api/v1/me/results", token: token)
|
|
}
|
|
|
|
static func dubShares(token: String) async throws -> [UserDubShareSummary] {
|
|
try await getJSON([UserDubShareSummary].self, path: "api/v1/me/dub-shares", token: token)
|
|
}
|
|
|
|
static func dubShareDetail(token: String, shareId: String) async throws -> DubShareDetail {
|
|
try await getJSON(
|
|
DubShareDetail.self,
|
|
path: "api/v1/dub-shares/\(shareId)",
|
|
token: token
|
|
)
|
|
}
|
|
|
|
static func deleteResult(token: String, attemptId: String) async throws {
|
|
try await delete(path: "api/v1/me/results/\(attemptId)", token: token)
|
|
}
|
|
|
|
static func deleteDubShare(token: String, shareId: String) async throws {
|
|
try await delete(path: "api/v1/me/dub-shares/\(shareId)", token: token)
|
|
}
|
|
|
|
static func downloadAudio(
|
|
token: String,
|
|
audioPath: String,
|
|
destination: URL
|
|
) async throws {
|
|
let result = try await HTTPClient.execute(
|
|
baseURL: AppConfig.serverBaseURL,
|
|
path: audioPath,
|
|
method: "GET",
|
|
timeout: AppConfig.readTimeout,
|
|
headers: authorizationHeaders(token: token)
|
|
)
|
|
guard (200...299).contains(result.statusCode) else {
|
|
throw UserHistoryError.badStatus(
|
|
result.statusCode,
|
|
String(data: result.body, encoding: .utf8) ?? ""
|
|
)
|
|
}
|
|
try? FileManager.default.removeItem(at: destination)
|
|
try result.body.write(to: destination, options: .atomic)
|
|
}
|
|
|
|
private static func getJSON<T: Decodable>(
|
|
_ type: T.Type,
|
|
path: String,
|
|
token: String
|
|
) async throws -> T {
|
|
let result = try await HTTPClient.execute(
|
|
baseURL: AppConfig.serverBaseURL,
|
|
path: path,
|
|
method: "GET",
|
|
timeout: AppConfig.readTimeout,
|
|
headers: authorizationHeaders(token: token)
|
|
)
|
|
guard (200...299).contains(result.statusCode) else {
|
|
throw UserHistoryError.badStatus(
|
|
result.statusCode,
|
|
String(data: result.body, encoding: .utf8) ?? ""
|
|
)
|
|
}
|
|
return try JSONDecoder().decode(T.self, from: result.body)
|
|
}
|
|
|
|
private static func delete(path: String, token: String) async throws {
|
|
let result = try await HTTPClient.execute(
|
|
baseURL: AppConfig.serverBaseURL,
|
|
path: path,
|
|
method: "DELETE",
|
|
timeout: AppConfig.readTimeout,
|
|
headers: authorizationHeaders(token: token)
|
|
)
|
|
guard (200...299).contains(result.statusCode) else {
|
|
throw UserHistoryError.badStatus(
|
|
result.statusCode,
|
|
String(data: result.body, encoding: .utf8) ?? ""
|
|
)
|
|
}
|
|
}
|
|
|
|
private static func authorizationHeaders(token: String) -> [String: String] {
|
|
token.isEmpty ? [:] : ["Authorization": "Bearer \(token)"]
|
|
}
|
|
}
|