importer chnges

This commit is contained in:
Mark Gregory
2026-11-05 10:18:41 +00:00
parent 1c509b2dad
commit 38a99d94ba
7 changed files with 479 additions and 31 deletions
+124
View File
@@ -118,6 +118,130 @@ final class KJamsCloneTests: XCTestCase {
let song = try XCTUnwrap(try db.song(ids[0]));XCTAssertEqual(URL(fileURLWithPath:song.audio).deletingLastPathComponent().standardizedFileURL,media.standardizedFileURL)
XCTAssertTrue(FileManager.default.fileExists(atPath:song.audio));XCTAssertNotNil(song.cdg)
}
func testLegacyEncodedZipNamesImportMatchedAudioAndGraphics() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
// A valid stored ZIP with an unflagged 0xE9 filename byte, matching older
// Windows karaoke archives. No commercial media is part of this test.
func number(_ value:UInt32,_ count:Int) -> Data { Data((0..<count).map { UInt8(truncatingIfNeeded:value >> (8*$0)) }) }
func crc(_ data:Data) -> UInt32 {
var value = UInt32.max
for byte in data { value ^= UInt32(byte);for _ in 0..<8 { value = value & 1 == 0 ? value >> 1 : (value >> 1) ^ 0xEDB88320 } }
return ~value
}
var zip = Data(),central = Data()
for ext in ["mp3","cdg"] {
let name = Data("Original - Caf".utf8)+Data([0xE9])+Data(("."+ext).utf8)
let contents = try Data(contentsOf:fixtures.appendingPathComponent("Demo."+ext)),size = UInt32(contents.count),checksum = crc(contents),offset = UInt32(zip.count)
zip += number(0x04034b50,4)+number(20,2)+number(0,2)+number(0,2)+number(0,2)+number(0,2)
zip += number(checksum,4)+number(size,4)+number(size,4)+number(UInt32(name.count),2)+number(0,2)+name+contents
central += number(0x02014b50,4)+number(20,2)+number(20,2)+number(0,2)+number(0,2)+number(0,2)+number(0,2)
central += number(checksum,4)+number(size,4)+number(size,4)
central += number(UInt32(name.count),2)+number(0,2)+number(0,2)
central += number(0,2)+number(0,2)+number(0,4)+number(offset,4)
central += name
}
let centralOffset = UInt32(zip.count);zip += central
zip += number(0x06054b50,4)+number(0,2)+number(0,2)+number(2,2)+number(2,2)+number(UInt32(central.count),4)+number(centralOffset,4)+number(0,2)
let archive = root.appendingPathComponent("Legacy.zip");try zip.write(to:archive)
let db = try Library(url:root.appendingPathComponent("Library.sqlite")),importer = Importer(media:root.appendingPathComponent("Media"))
let ids = try importer.run([archive],library:db);XCTAssertEqual(ids.count,1)
let song = try XCTUnwrap(try db.song(ids[0]));let graphics = try XCTUnwrap(song.cdg)
XCTAssertTrue(song.title.contains("Café"),"Legacy filenames must be renamed to Unicode before import")
XCTAssertEqual(try Data(contentsOf:URL(fileURLWithPath:song.audio)),try Data(contentsOf:fixtures.appendingPathComponent("Demo.mp3")))
XCTAssertEqual(try Data(contentsOf:URL(fileURLWithPath:graphics)),try Data(contentsOf:fixtures.appendingPathComponent("Demo.cdg")))
var progress = ImportProgress();_ = try importer.run([archive],library:db,progress:{progress = $0})
XCTAssertEqual(progress.skipped,1);XCTAssertEqual(try db.songs().count,1)
}
@MainActor func testMediaFolderChoicePersistsAndFutureImportsUseIt() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
let model = AppModel(storageURL:root.appendingPathComponent("Library")),external = root.appendingPathComponent("External Media")
try model.setMediaDirectory(external)
let reopened = AppModel(storageURL:root.appendingPathComponent("Library"))
XCTAssertEqual(reopened.managedMediaDirectory.resolvingSymlinksInPath().path,external.resolvingSymlinksInPath().path)
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.zip"))
reopened.importFiles([fixture])
for _ in 0..<300 { if !reopened.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertFalse(reopened.isImporting);XCTAssertNil(reopened.importProgress?.failure)
let song = try XCTUnwrap(reopened.songs.first)
XCTAssertTrue(URL(fileURLWithPath:song.audio).resolvingSymlinksInPath().path.hasPrefix(external.resolvingSymlinksInPath().path+"/"));XCTAssertTrue(URL(fileURLWithPath:try XCTUnwrap(song.cdg)).resolvingSymlinksInPath().path.hasPrefix(external.resolvingSymlinksInPath().path+"/"))
XCTAssertFalse(FileManager.default.fileExists(atPath:root.appendingPathComponent("Library/Media").path))
}
func testVerifiedMoveUpdatesLinksPreservesSingerPreferencesAndFreesOriginals() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let database = root.appendingPathComponent("Library.sqlite"),source = root.appendingPathComponent("Media"),target = root.appendingPathComponent("External")
let db = try Library(url:database),fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.zip"))
let ids = try Importer(media:source).run([fixture],library:db),id = try XCTUnwrap(ids.first)
try db.addEntity("singers",name:"Alex");let singer = try XCTUnwrap(try db.entities("singers").first?.id);try db.assignSong(id,singer:singer)
try db.savePreference(song:id,singer:singer,value:.init(pitch:3,tempo:1.1,rating:4));try db.rate(id,5)
let before = try XCTUnwrap(try db.song(id)),audio = try Data(contentsOf:URL(fileURLWithPath:before.audio))
var studio = StudioSettings();studio.backgroundPath = before.audio
try JSONEncoder().encode(studio).write(to:root.appendingPathComponent("StudioSettings.json"))
let result = try ManagedMediaStorage.move(database:database,roots:[source],destination:target)
XCTAssertTrue(result.finished);XCTAssertNil(result.failure);XCTAssertEqual(result.completed,2)
let after = try XCTUnwrap(try db.song(id));XCTAssertEqual(after.id,id);XCTAssertEqual(after.rating,5)
XCTAssertTrue(after.audio.hasPrefix(target.resolvingSymlinksInPath().path+"/"))
let movedSettings = try JSONDecoder().decode(StudioSettings.self,from:Data(contentsOf:root.appendingPathComponent("StudioSettings.json")))
XCTAssertEqual(movedSettings.backgroundPath,after.audio)
XCTAssertEqual(try Data(contentsOf:URL(fileURLWithPath:after.audio)),audio)
XCTAssertFalse(FileManager.default.fileExists(atPath:before.audio));XCTAssertFalse(FileManager.default.fileExists(atPath:try XCTUnwrap(before.cdg)))
XCTAssertEqual(try db.preference(song:id,singer:singer).pitch,3)
var progress = ImportProgress();_ = try Importer(media:target).run([fixture],library:db,progress:{progress = $0})
XCTAssertEqual(progress.skipped,1)
}
func testCancelledMoveKeepsOriginalMediaAndDatabaseLinks() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let database = root.appendingPathComponent("Library.sqlite"),source = root.appendingPathComponent("Media"),target = root.appendingPathComponent("External")
let db = try Library(url:database),fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.zip"))
_ = try Importer(media:source).run([fixture],library:db);let before = try XCTUnwrap(try db.songs().first)
var cancel = false
XCTAssertThrowsError(try ManagedMediaStorage.move(database:database,roots:[source],destination:target,cancelled:{cancel},progress:{if $0.completed == 1 { cancel = true }})) { XCTAssertTrue($0 is CancellationError) }
XCTAssertEqual(try db.song(before.id)?.audio,before.audio);XCTAssertTrue(FileManager.default.fileExists(atPath:before.audio))
XCTAssertTrue(FileManager.default.fileExists(atPath:try XCTUnwrap(before.cdg)));XCTAssertNil(try db.setting(MediaStorageLocation.key))
let retried = try ManagedMediaStorage.move(database:database,roots:[source],destination:target);XCTAssertNil(retried.failure)
XCTAssertFalse(FileManager.default.fileExists(atPath:before.audio))
}
func testMoveConflictDoesNotOverwriteDestinationOrDeleteOriginal() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
defer { try? FileManager.default.removeItem(at:root) }
let database = root.appendingPathComponent("Library.sqlite"),source = root.appendingPathComponent("Media"),target = root.appendingPathComponent("External")
let db = try Library(url:database),fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
_ = try Importer(media:source).run([fixture],library:db);let song = try XCTUnwrap(try db.songs().first)
try FileManager.default.createDirectory(at:target,withIntermediateDirectories:true)
let conflict = target.appendingPathComponent(URL(fileURLWithPath:song.audio).lastPathComponent),different = Data("Other media".utf8);try different.write(to:conflict)
XCTAssertThrowsError(try ManagedMediaStorage.move(database:database,roots:[source],destination:target))
XCTAssertEqual(try Data(contentsOf:conflict),different);XCTAssertTrue(FileManager.default.fileExists(atPath:song.audio));XCTAssertEqual(try db.song(song.id)?.audio,song.audio)
}
func testUnavailableExternalDriveDoesNotCreateALocalFallbackFolder() {
let path = URL(fileURLWithPath:"/Volumes/KJMix-Offline-"+UUID().uuidString+"/Media")
XCTAssertThrowsError(try ManagedMediaStorage.prepare(path))
XCTAssertFalse(FileManager.default.fileExists(atPath:path.path))
}
@MainActor func testMediaMoveCollectsPreviousImportFolders() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
let model = AppModel(storageURL:root.appendingPathComponent("Library")),a = root.appendingPathComponent("External A"),b = root.appendingPathComponent("External B")
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
model.importFiles([fixtures.appendingPathComponent("Demo.mp3")])
for _ in 0..<300 { if !model.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
model.dismissImport();try model.setMediaDirectory(a)
model.importFiles([fixtures.appendingPathComponent("TestTrack.mp3")])
for _ in 0..<300 { if !model.isImporting { break };try await Task.sleep(nanoseconds:10_000_000) }
model.dismissImport();let ids = Set(model.songs.map(\.id));model.moveMedia(to:b)
for _ in 0..<300 { if !model.movingMedia { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertFalse(model.movingMedia);XCTAssertNil(model.storageProgress?.failure)
XCTAssertEqual(Set(model.songs.map(\.id)),ids);XCTAssertEqual(model.songs.count,2)
XCTAssertTrue(model.songs.allSatisfy{$0.audio.hasPrefix(b.resolvingSymlinksInPath().path+"/")})
XCTAssertEqual(model.managedMediaDirectory.resolvingSymlinksInPath().path,b.resolvingSymlinksInPath().path)
}
func packet(_ instruction: UInt8, _ data: [UInt8]) -> [UInt8] { [9,instruction,0,0] + Array((data + Array(repeating: 0,count:16)).prefix(16)) + [0,0,0,0] }
func testPresetTileXORAndInvalidCoordinates() {
var d = CDGDecoder()