Add persistent local music catalog
This commit is contained in:
+273
@@ -0,0 +1,273 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import VelodyPersistence
|
||||
import VelodyDomain
|
||||
|
||||
final class LocalCatalogServiceTests: XCTestCase {
|
||||
func testScanPersistsTracksAndSkipsDuplicateSHA() async throws {
|
||||
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
|
||||
let service = DefaultLocalCatalogService(repository: repository)
|
||||
let modifiedAt = Date(timeIntervalSince1970: 1_000)
|
||||
|
||||
let result = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Alpha",
|
||||
path: "/Music/Alpha.mp3",
|
||||
sha256: "sha-alpha",
|
||||
modifiedAt: modifiedAt
|
||||
),
|
||||
makeScannedTrack(
|
||||
title: "Alpha Copy",
|
||||
path: "/Music/Copies/Alpha.mp3",
|
||||
sha256: "sha-alpha",
|
||||
modifiedAt: modifiedAt
|
||||
),
|
||||
],
|
||||
in: URL(fileURLWithPath: "/Music"),
|
||||
scannedAt: Date(timeIntervalSince1970: 1_100)
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.insertedTrackCount, 1)
|
||||
XCTAssertEqual(result.skippedDuplicateTrackCount, 1)
|
||||
XCTAssertEqual(result.deletedTrackCount, 0)
|
||||
XCTAssertEqual(result.tracks.count, 1)
|
||||
XCTAssertEqual(result.tracks.first?.localFilePath, "/Music/Alpha.mp3")
|
||||
|
||||
let storedTracks = try await repository.loadLocalTracks(
|
||||
origin: .localScan,
|
||||
includeDeleted: true
|
||||
)
|
||||
XCTAssertEqual(storedTracks.count, 1)
|
||||
XCTAssertEqual(storedTracks.first?.sha256, "sha-alpha")
|
||||
XCTAssertEqual(storedTracks.first?.fileModifiedAt, modifiedAt)
|
||||
}
|
||||
|
||||
func testModifiedFileAtSamePathUpdatesExistingTrackInsteadOfDuplicating() async throws {
|
||||
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
|
||||
let service = DefaultLocalCatalogService(repository: repository)
|
||||
let folderURL = URL(fileURLWithPath: "/Music")
|
||||
|
||||
_ = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Original",
|
||||
path: "/Music/Track.mp3",
|
||||
sha256: "sha-original",
|
||||
modifiedAt: Date(timeIntervalSince1970: 2_000)
|
||||
),
|
||||
],
|
||||
in: folderURL,
|
||||
scannedAt: Date(timeIntervalSince1970: 2_100)
|
||||
)
|
||||
|
||||
let result = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Updated Title",
|
||||
path: "/Music/Track.mp3",
|
||||
sha256: "sha-updated",
|
||||
modifiedAt: Date(timeIntervalSince1970: 2_200)
|
||||
),
|
||||
],
|
||||
in: folderURL,
|
||||
scannedAt: Date(timeIntervalSince1970: 2_300)
|
||||
)
|
||||
|
||||
XCTAssertEqual(result.insertedTrackCount, 0)
|
||||
XCTAssertEqual(result.updatedTrackCount, 1)
|
||||
XCTAssertEqual(result.deletedTrackCount, 0)
|
||||
XCTAssertEqual(result.tracks.count, 1)
|
||||
XCTAssertEqual(result.tracks.first?.title, "Updated Title")
|
||||
XCTAssertEqual(result.tracks.first?.sha256, "sha-updated")
|
||||
|
||||
let storedTracks = try await repository.loadLocalTracks(
|
||||
origin: .localScan,
|
||||
includeDeleted: true
|
||||
)
|
||||
XCTAssertEqual(storedTracks.count, 1)
|
||||
XCTAssertEqual(storedTracks.first?.title, "Updated Title")
|
||||
XCTAssertEqual(storedTracks.first?.sha256, "sha-updated")
|
||||
}
|
||||
|
||||
func testRescanMarksDeletedTracksAndReactivatesExistingSHA() async throws {
|
||||
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
|
||||
let service = DefaultLocalCatalogService(repository: repository)
|
||||
let folderURL = URL(fileURLWithPath: "/Music")
|
||||
|
||||
_ = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Alpha",
|
||||
path: "/Music/Alpha.mp3",
|
||||
sha256: "sha-alpha",
|
||||
modifiedAt: Date(timeIntervalSince1970: 3_000)
|
||||
),
|
||||
makeScannedTrack(
|
||||
title: "Beta",
|
||||
path: "/Music/Beta.mp3",
|
||||
sha256: "sha-beta",
|
||||
modifiedAt: Date(timeIntervalSince1970: 3_000)
|
||||
),
|
||||
],
|
||||
in: folderURL,
|
||||
scannedAt: Date(timeIntervalSince1970: 3_100)
|
||||
)
|
||||
|
||||
let deleteResult = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Alpha",
|
||||
path: "/Music/Alpha.mp3",
|
||||
sha256: "sha-alpha",
|
||||
modifiedAt: Date(timeIntervalSince1970: 3_200)
|
||||
),
|
||||
],
|
||||
in: folderURL,
|
||||
scannedAt: Date(timeIntervalSince1970: 3_300)
|
||||
)
|
||||
|
||||
XCTAssertEqual(deleteResult.deletedTrackCount, 1)
|
||||
XCTAssertEqual(deleteResult.tracks.count, 1)
|
||||
|
||||
let reactivationResult = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Alpha",
|
||||
path: "/Music/Alpha.mp3",
|
||||
sha256: "sha-alpha",
|
||||
modifiedAt: Date(timeIntervalSince1970: 3_400)
|
||||
),
|
||||
makeScannedTrack(
|
||||
title: "Beta",
|
||||
path: "/Music/Recovered/Beta.mp3",
|
||||
sha256: "sha-beta",
|
||||
modifiedAt: Date(timeIntervalSince1970: 3_400)
|
||||
),
|
||||
],
|
||||
in: folderURL,
|
||||
scannedAt: Date(timeIntervalSince1970: 3_500)
|
||||
)
|
||||
|
||||
XCTAssertEqual(reactivationResult.reactivatedTrackCount, 1)
|
||||
XCTAssertEqual(reactivationResult.deletedTrackCount, 0)
|
||||
XCTAssertEqual(reactivationResult.tracks.count, 2)
|
||||
|
||||
let storedTracks = try await repository.loadLocalTracks(
|
||||
origin: .localScan,
|
||||
includeDeleted: true
|
||||
)
|
||||
XCTAssertEqual(storedTracks.count, 2)
|
||||
XCTAssertEqual(
|
||||
storedTracks.first(where: { $0.sha256 == "sha-beta" })?.localFilePath,
|
||||
"/Music/Recovered/Beta.mp3"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
storedTracks.first(where: { $0.sha256 == "sha-beta" })?.isDeleted,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
func testRepositoryPersistsTracksAcrossInstances() async throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
UUID().uuidString,
|
||||
isDirectory: true
|
||||
)
|
||||
let databaseURL = tempDirectory.appendingPathComponent("catalog.store")
|
||||
let folderURL = URL(fileURLWithPath: "/Music")
|
||||
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
}
|
||||
|
||||
let firstRepository = try SwiftDataTrackRepository(databaseURL: databaseURL)
|
||||
let firstService = DefaultLocalCatalogService(repository: firstRepository)
|
||||
|
||||
_ = try await firstService.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Persisted",
|
||||
path: "/Music/Persisted.mp3",
|
||||
sha256: "sha-persisted",
|
||||
modifiedAt: Date(timeIntervalSince1970: 4_000)
|
||||
),
|
||||
],
|
||||
in: folderURL,
|
||||
scannedAt: Date(timeIntervalSince1970: 4_100)
|
||||
)
|
||||
|
||||
let secondRepository = try SwiftDataTrackRepository(databaseURL: databaseURL)
|
||||
let storedTracks = try await secondRepository.loadLocalTracks(
|
||||
origin: .localScan,
|
||||
includeDeleted: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(storedTracks.count, 1)
|
||||
XCTAssertEqual(storedTracks.first?.title, "Persisted")
|
||||
XCTAssertEqual(storedTracks.first?.sha256, "sha-persisted")
|
||||
}
|
||||
|
||||
func testSyncBootstrapReplacementLeavesLocalCatalogIntact() async throws {
|
||||
let repository = try SwiftDataTrackRepository(isStoredInMemoryOnly: true)
|
||||
let service = DefaultLocalCatalogService(repository: repository)
|
||||
|
||||
_ = try await service.reconcileScanResults(
|
||||
[
|
||||
makeScannedTrack(
|
||||
title: "Local Track",
|
||||
path: "/Music/Local.mp3",
|
||||
sha256: "sha-local",
|
||||
modifiedAt: Date(timeIntervalSince1970: 5_000)
|
||||
),
|
||||
],
|
||||
in: URL(fileURLWithPath: "/Music"),
|
||||
scannedAt: Date(timeIntervalSince1970: 5_100)
|
||||
)
|
||||
|
||||
try await repository.replaceTracks(
|
||||
[
|
||||
LibraryTrack(
|
||||
id: "remote-track-1",
|
||||
title: "Remote Track",
|
||||
artist: "Remote Artist",
|
||||
album: "Remote Album",
|
||||
durationSeconds: 180,
|
||||
localFilePath: "",
|
||||
sha256: nil
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
let localTracks = try await repository.loadLocalTracks(
|
||||
origin: .localScan,
|
||||
includeDeleted: false
|
||||
)
|
||||
let remoteTracks = try await repository.loadLocalTracks(
|
||||
origin: .syncBootstrap,
|
||||
includeDeleted: false
|
||||
)
|
||||
let allTracks = try await repository.loadTracks()
|
||||
|
||||
XCTAssertEqual(localTracks.count, 1)
|
||||
XCTAssertEqual(remoteTracks.count, 1)
|
||||
XCTAssertEqual(allTracks.count, 2)
|
||||
}
|
||||
|
||||
private func makeScannedTrack(
|
||||
title: String,
|
||||
path: String,
|
||||
sha256: String,
|
||||
modifiedAt: Date
|
||||
) -> ScannedLocalTrack {
|
||||
ScannedLocalTrack(
|
||||
title: title,
|
||||
artist: "Artist",
|
||||
album: "Album",
|
||||
durationSeconds: 180,
|
||||
localFilePath: path,
|
||||
sha256: sha256,
|
||||
fileModifiedAt: modifiedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user