removed unwanted bar now with waveforms, major updates

This commit is contained in:
Mark Gregory
2026-11-06 13:19:04 +00:00
parent f6368e4ae7
commit e67acb8210
6 changed files with 127 additions and 33 deletions
+47 -17
View File
@@ -27,7 +27,7 @@ struct Importer {
var includeGraphics = true
var waveformCache: URL? = nil
@discardableResult func run(_ urls: [URL],library: Library,copyMedia: Bool = true,cancelled: () -> Bool = { false },progress: (ImportProgress) -> Void = { _ in }) throws -> [Int64] {
try ManagedMediaStorage.prepare(media)
if copyMedia { try ManagedMediaStorage.prepare(media) }
var state = ImportProgress()
var lastReport = Date.distantPast
func report(_ current: String,force: Bool = false) {
@@ -213,6 +213,7 @@ struct Importer {
if includeGraphics { cdg = try companions(of:audio)["cdg"] } else { cdg = nil }
let archive = archiveForFile[audio.resolvingSymlinksInPath().path]
let copy = copyMedia || archive != nil
if copy { try ManagedMediaStorage.prepare(media) }
let signature = try importSignature(audio,cdg:cdg)
if let existing = try library.importedSongs(path:audio.standardizedFileURL.path,signature:signature) {
songIDs.formUnion(existing)
@@ -360,13 +361,20 @@ enum ManagedMediaStorage {
static func save(_ location:MediaStorageLocation,to library:Library) throws {
try library.setSetting(MediaStorageLocation.key,String(decoding:try JSONEncoder().encode(location),as:UTF8.self))
}
static func hash(_ url:URL,cancelled:()->Bool) throws -> SHA256.Digest {
static func hash(_ url:URL,cancelled:()->Bool,bytesRead:(Int64)->Void = {_ in}) throws -> SHA256.Digest {
let file = try FileHandle(forReadingFrom:url);defer { try? file.close() }
var result = SHA256()
while let data = try file.read(upToCount:1_048_576),!data.isEmpty { if cancelled() { throw CancellationError() };result.update(data:data) }
var result = SHA256(),total: Int64 = 0
while let data = try file.read(upToCount:1_048_576),!data.isEmpty { if cancelled() { throw CancellationError() };result.update(data:data);total += Int64(data.count);bytesRead(total) }
return result.finalize()
}
static func move(database:URL,roots:[URL],destination:URL,cancelled:()->Bool = {false},progress:(ImportProgress)->Void = {_ in}) throws -> ImportProgress {
var state = ImportProgress(current:"Checking media locations…"),lastReport = Date.distantPast
func scan(_ text: String) throws {
if cancelled() { throw CancellationError() }
if Date().timeIntervalSince(lastReport) >= 0.1 { state.current = text;progress(state);lastReport = Date() }
if cancelled() { throw CancellationError() }
}
try scan("Checking media locations…")
try prepare(destination)
let targetRoot = destination.resolvingSymlinksInPath()
let sources = Set(roots.map{$0.resolvingSymlinksInPath()}).filter{$0 != targetRoot}
@@ -374,6 +382,7 @@ enum ManagedMediaStorage {
let db = try Library(url:database),songs = try db.songs()
var files = Set<URL>()
for song in songs {
try scan("Finding media: "+song.title)
for path in [song.audio,song.cdg].compactMap({$0}) {
let file = URL(fileURLWithPath:path).resolvingSymlinksInPath()
if sources.contains(file.deletingLastPathComponent()) { files.insert(file) }
@@ -381,7 +390,9 @@ enum ManagedMediaStorage {
}
// Include managed hash-named remnants of previous cancelled imports.
for root in sources where FileManager.default.fileExists(atPath:root.path) {
try scan("Checking folder: "+root.path)
for file in try FileManager.default.contentsOfDirectory(at:root,includingPropertiesForKeys:[.isRegularFileKey,.isSymbolicLinkKey]) {
try scan("Checking media: "+file.lastPathComponent)
let stem = file.deletingPathExtension().lastPathComponent
if [64,129].contains(stem.count),stem.allSatisfy({$0.isHexDigit || $0 == "-"}),["mp3","wav","m4a","mp4","m4v","mov","cdg"].contains(file.pathExtension.lowercased()) {
let values = try file.resourceValues(forKeys:[.isRegularFileKey,.isSymbolicLinkKey])
@@ -389,7 +400,8 @@ enum ManagedMediaStorage {
}
}
}
var state = ImportProgress(total:files.count),verified = [(URL,URL)](),lastReport = Date.distantPast
state.total = files.count
var verified = [(URL,URL)]()
defer { progress(state) }
var sourceStamps = [URL:String]()
func stamp(_ url:URL) throws -> String { let info = try FileManager.default.attributesOfItem(atPath:url.path);return "\(info[.size] ?? 0)|\(info[.modificationDate] ?? "")|\(info[.systemFileNumber] ?? 0)" }
@@ -400,16 +412,16 @@ enum ManagedMediaStorage {
if Date().timeIntervalSince(lastReport) >= 0.1 { progress(state);lastReport = Date() }
let target = targetRoot.appendingPathComponent(source.lastPathComponent)
sourceStamps[source] = try stamp(source)
let expected = try hash(source,cancelled:cancelled)
let expected = try hash(source,cancelled:cancelled,bytesRead:{ bytes in
if Date().timeIntervalSince(lastReport) >= 0.1 { state.current = "Verifying "+source.lastPathComponent+" · "+ByteCountFormatter.string(fromByteCount:bytes,countStyle:.file);progress(state);lastReport = Date() }
})
if FileManager.default.fileExists(atPath:target.path) {
guard try hash(target,cancelled:cancelled) == expected else { throw LibraryError.message("A different file already exists at \(target.path). Choose an empty folder or resolve the conflict first.") }
state.skipped += 1
} else {
let temporary = targetRoot.appendingPathComponent(".kjmix-moving-"+UUID().uuidString)
defer { try? FileManager.default.removeItem(at:temporary) }
try FileManager.default.copyItem(at:source,to:temporary)
guard try hash(temporary,cancelled:cancelled) == expected else { throw LibraryError.message("Verification failed for \(source.lastPathComponent). The original has been kept.") }
try FileManager.default.moveItem(at:temporary,to:target)
try MediaByteCopy.copy(source,to:target,expectedHash:expected.map{String(format:"%02x",$0)}.joined(),cancelled:cancelled,bytesCopied:{ bytes in
if Date().timeIntervalSince(lastReport) >= 0.1 { state.current = "Copying "+source.lastPathComponent+" · "+ByteCountFormatter.string(fromByteCount:bytes,countStyle:.file);progress(state);lastReport = Date() }
})
}
verified.append((source,target));state.completed += 1;state.logLines.append("[COPIED] "+source.path)
if Date().timeIntervalSince(lastReport) >= 0.1 || state.completed == 1 || state.completed == state.total { progress(state);lastReport = Date() }
@@ -463,7 +475,7 @@ enum MediaByteCopy {
}
return hash.finalize().map{String(format:"%02x",$0)}.joined()
}
static func copy(_ source: URL,to destination: URL,expectedHash: String,cancelled: () -> Bool = { false }) throws {
static func copy(_ source: URL,to destination: URL,expectedHash: String,cancelled: () -> Bool = { false },bytesCopied: (Int64) -> Void = {_ in}) throws {
if cancelled() { throw CancellationError() }
if nonempty(destination),try digest(destination,cancelled:cancelled) == expectedHash { return }
let temporary = destination.deletingLastPathComponent().appendingPathComponent(".kjmix-import-"+UUID().uuidString)
@@ -476,8 +488,9 @@ enum MediaByteCopy {
while true {
if cancelled() { throw CancellationError() }
guard let data = try input.read(upToCount:1_048_576),!data.isEmpty else { break }
try output.write(contentsOf:data);bytesWritten += data.count
try output.write(contentsOf:data);bytesWritten += data.count;bytesCopied(Int64(bytesWritten))
}
if cancelled() { throw CancellationError() }
try output.synchronize();try output.close()
guard bytesWritten > 0,try digest(temporary,cancelled:cancelled) == expectedHash else { throw LibraryError.message("Audio data was empty or changed during copying. Retry the import.") }
if cancelled() { throw CancellationError() }
@@ -497,27 +510,44 @@ enum DJStorageMove {
return try JSONDecoder().decode(MediaStorageLocation.self,from:Data(text.utf8))
}
static func run(database: URL,defaultRoot: URL,destination: URL,cancelled: () -> Bool = {false},progress: (ImportProgress) -> Void = {_ in}) throws -> ImportProgress {
var discovery = ImportProgress(current:"Checking DJ storage…"),lastDiscovery = Date.distantPast
func reportDiscovery(_ text: String,force: Bool = false) throws {
if cancelled() { throw CancellationError() }
if force || Date().timeIntervalSince(lastDiscovery) >= 0.1 { discovery.current = text;progress(discovery);lastDiscovery = Date() }
if cancelled() { throw CancellationError() }
}
try reportDiscovery("Checking DJ storage…",force:true)
let db = try Library(url:database),old = try location(in:db)
let oldRoots = Array(Set((old?.roots ?? [])+[defaultRoot.path,old?.path ?? defaultRoot.path])).map{URL(fileURLWithPath:$0)}
let target = destination.resolvingSymlinksInPath(),media = target.appendingPathComponent("Media"),waves = target.appendingPathComponent("Waveforms")
try ManagedMediaStorage.prepare(media);try ManagedMediaStorage.prepare(waves)
let oldSongs = try db.songs()
var cacheSources = [Int64:URL](),allCaches = Set<URL>()
for folder in oldRoots.map({$0.appendingPathComponent("Waveforms")}) {
var cacheFolders = Set<URL>()
for folder in Set(oldRoots.map({$0.appendingPathComponent("Waveforms").resolvingSymlinksInPath()})) {
try reportDiscovery("Scanning saved waveforms: "+folder.path)
if let files = try? FileManager.default.contentsOfDirectory(at:folder,includingPropertiesForKeys:[.isRegularFileKey,.isSymbolicLinkKey]) {
for file in files where file.pathExtension == "waveform" {
try reportDiscovery("Checking saved waveforms: \(allCaches.count.formatted()) found")
let attributes = try file.resourceValues(forKeys:[.isRegularFileKey,.isSymbolicLinkKey])
if attributes.isRegularFile == true,attributes.isSymbolicLink != true { allCaches.insert(file.resolvingSymlinksInPath()) }
if attributes.isRegularFile == true,attributes.isSymbolicLink != true { allCaches.insert(file.resolvingSymlinksInPath());cacheFolders.insert(folder) }
}
}
for song in oldSongs where cacheSources[song.id] == nil {
if let file = try? WaveformCache.location(audio:URL(fileURLWithPath:song.audio),directory:folder),FileManager.default.fileExists(atPath:file.path) { cacheSources[song.id] = file.resolvingSymlinksInPath() }
}
if !cacheFolders.isEmpty {
let filesByName = Dictionary(grouping:allCaches,by: \.lastPathComponent)
for (index,song) in oldSongs.enumerated() {
try reportDiscovery("Matching waveform \(index+1) of \(oldSongs.count): "+song.title)
// The key filename does not depend on its cache directory.
if let key = try? WaveformCache.location(audio:URL(fileURLWithPath:song.audio),directory:waves).lastPathComponent,
let file = filesByName[key]?.first { cacheSources[song.id] = file }
}
}
let manifestKey = "dj.storage.waveform.sources.v1"
if let json = try db.setting(manifestKey),let saved = try? JSONDecoder().decode([String:String].self,from:Data(json.utf8)) {
let folders = Set(oldRoots.map{$0.appendingPathComponent("Waveforms").resolvingSymlinksInPath()})
for song in oldSongs where cacheSources[song.id] == nil {
try reportDiscovery("Checking interrupted transfers: "+song.title)
if let path = saved[String(song.id)] {
let file = URL(fileURLWithPath:path).resolvingSymlinksInPath()
if folders.contains(file.deletingLastPathComponent()),file.pathExtension == "waveform",FileManager.default.fileExists(atPath:file.path) { cacheSources[song.id] = file;allCaches.insert(file) }