Implement remote library metadata sync for iPhone
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import VelodyDomain
|
||||
|
||||
public protocol RemoteLibraryStore: Actor {
|
||||
func loadRemoteTracks() async throws -> [RemoteTrack]
|
||||
func replaceRemoteTracks(_ tracks: [RemoteTrack]) async throws
|
||||
}
|
||||
|
||||
public actor FileRemoteLibraryStore: RemoteLibraryStore {
|
||||
private let fileURL: URL
|
||||
private let fileManager: FileManager
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
public init(
|
||||
fileURL: URL? = nil,
|
||||
fileManager: FileManager = .default
|
||||
) throws {
|
||||
self.fileManager = fileManager
|
||||
if let fileURL {
|
||||
self.fileURL = fileURL
|
||||
} else {
|
||||
self.fileURL = try Self.defaultFileURL(fileManager: fileManager)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadRemoteTracks() async throws -> [RemoteTrack] {
|
||||
guard fileManager.fileExists(atPath: fileURL.path) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return try decoder.decode([RemoteTrack].self, from: data)
|
||||
}
|
||||
|
||||
public func replaceRemoteTracks(_ tracks: [RemoteTrack]) async throws {
|
||||
try fileManager.createDirectory(
|
||||
at: fileURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let data = try encoder.encode(tracks)
|
||||
try data.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
|
||||
private static func defaultFileURL(fileManager: FileManager) throws -> URL {
|
||||
guard let applicationSupportURL = fileManager.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first else {
|
||||
throw CocoaError(.fileNoSuchFile)
|
||||
}
|
||||
|
||||
return applicationSupportURL
|
||||
.appendingPathComponent("Velody", isDirectory: true)
|
||||
.appendingPathComponent("remote-library.json")
|
||||
}
|
||||
}
|
||||
|
||||
public actor InMemoryRemoteLibraryStore: RemoteLibraryStore {
|
||||
private var tracks: [RemoteTrack]
|
||||
|
||||
public init(tracks: [RemoteTrack] = []) {
|
||||
self.tracks = tracks
|
||||
}
|
||||
|
||||
public func loadRemoteTracks() async throws -> [RemoteTrack] {
|
||||
tracks
|
||||
}
|
||||
|
||||
public func replaceRemoteTracks(_ tracks: [RemoteTrack]) async throws {
|
||||
self.tracks = tracks
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import VelodyDomain
|
||||
@testable import VelodyPersistence
|
||||
|
||||
final class RemoteLibraryStoreTests: XCTestCase {
|
||||
func testFileRemoteLibraryStorePersistsTracksAcrossInstances() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
let fileURL = tempDirectory.appendingPathComponent("remote-library.json")
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let firstStore = try FileRemoteLibraryStore(fileURL: fileURL)
|
||||
let tracks = [
|
||||
RemoteTrack(
|
||||
trackId: "track-123",
|
||||
title: "Remote Title",
|
||||
artist: "Remote Artist",
|
||||
durationSeconds: 245,
|
||||
sha256: String(repeating: "a", count: 64),
|
||||
assetId: "asset-456",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z"
|
||||
),
|
||||
]
|
||||
|
||||
try await firstStore.replaceRemoteTracks(tracks)
|
||||
|
||||
let secondStore = try FileRemoteLibraryStore(fileURL: fileURL)
|
||||
let restoredTracks = try await secondStore.loadRemoteTracks()
|
||||
|
||||
XCTAssertEqual(restoredTracks, tracks)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user