performance updates now qwith progress on import
This commit is contained in:
@@ -11,6 +11,14 @@ struct ImportProgress: Sendable {
|
||||
var finished = false
|
||||
var cancelled = false
|
||||
var failure: String?
|
||||
var logLines = [String]()
|
||||
var reportLines: [String] {
|
||||
if let failure { return logLines+["[FAILED] "+current+": "+failure] }
|
||||
return logLines
|
||||
}
|
||||
var logText: String {
|
||||
"Import results: \(completed-skipped) imported, \(skipped) skipped\n"+reportLines.joined(separator:"\n")
|
||||
}
|
||||
}
|
||||
|
||||
struct Importer {
|
||||
@@ -73,6 +81,33 @@ struct Importer {
|
||||
var files = discovered.sorted { $0.path < $1.path }
|
||||
var temporary = [URL]()
|
||||
defer { for url in temporary { try? FileManager.default.removeItem(at: url) } }
|
||||
// Older libraries may contain indexed tracks without stored fingerprints.
|
||||
// Compare only same-size candidates, and hash each legacy file at most once.
|
||||
var legacyBySize = [UInt64:[Song]]()
|
||||
for song in try library.songs() where song.fingerprint.isEmpty {
|
||||
if let info = try? FileManager.default.attributesOfItem(atPath:song.audio),let size = info[.size] as? NSNumber,size.uint64Value > 0 {
|
||||
legacyBySize[size.uint64Value,default:[]].append(song)
|
||||
}
|
||||
}
|
||||
var legacyHashes = [Int64:String]()
|
||||
func duplicateWithFingerprint(_ hash: String,audio: URL) throws -> Song? {
|
||||
if let song = try library.songWithFingerprint(hash) { return song }
|
||||
let size = (try FileManager.default.attributesOfItem(atPath:audio.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
||||
for song in legacyBySize[size] ?? [] {
|
||||
if cancelled() { throw CancellationError() }
|
||||
let candidate: String
|
||||
if let known = legacyHashes[song.id] { candidate = known }
|
||||
else {
|
||||
do { candidate = try audioFingerprint(URL(fileURLWithPath:song.audio),cancelled:cancelled) }
|
||||
catch is CancellationError { throw CancellationError() }
|
||||
catch { continue } // An offline legacy entry must not prevent a new import.
|
||||
legacyHashes[song.id] = candidate
|
||||
try library.execute("UPDATE songs SET fingerprint=? WHERE id=? AND fingerprint=''",[candidate,String(song.id)])
|
||||
}
|
||||
if candidate == hash { return song }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var songIDs = Set<Int64>()
|
||||
var archives = [(url:URL,signature:String,folder:URL)]()
|
||||
var archiveForFile = [String:URL]()
|
||||
@@ -85,13 +120,14 @@ struct Importer {
|
||||
}
|
||||
return try signature(audio)+"|"+(cdg.map { try signature($0) } ?? "no-cdg")+"|copy=\(copyMedia)"
|
||||
}
|
||||
func remember(_ audio:URL,_ signature:String,_ id:Int64) throws {
|
||||
var originalPath = audio.path
|
||||
func sourcePath(_ audio: URL) -> String {
|
||||
if let folder = archiveForFile[audio.resolvingSymlinksInPath().path],let zip = archiveOrigins[folder] {
|
||||
let relative = String(audio.resolvingSymlinksInPath().path.dropFirst(folder.resolvingSymlinksInPath().path.count+1))
|
||||
originalPath = zip.path+"/"+relative
|
||||
return zip.path+"/"+String(audio.resolvingSymlinksInPath().path.dropFirst(folder.resolvingSymlinksInPath().path.count+1))
|
||||
}
|
||||
try library.recordOriginalName(song:id,name:audio.deletingPathExtension().lastPathComponent,path:originalPath)
|
||||
return audio.path
|
||||
}
|
||||
func remember(_ audio:URL,_ signature:String,_ id:Int64) throws {
|
||||
try library.recordOriginalName(song:id,name:audio.deletingPathExtension().lastPathComponent,path:sourcePath(audio))
|
||||
if let archive = archiveForFile[audio.resolvingSymlinksInPath().path] { archiveSongIDs[archive,default:[]].append(id) }
|
||||
else { try library.rememberImport(path:audio.standardizedFileURL.path,signature:signature,songs:[id]) }
|
||||
}
|
||||
@@ -100,6 +136,9 @@ struct Importer {
|
||||
let signature = try importSignature(zip)
|
||||
if let existing = try library.importedSongs(path:zip.standardizedFileURL.path,signature:signature),try library.originalNamesRecorded(existing) {
|
||||
songIDs.formUnion(existing);state.completed += existing.count;state.skipped += existing.count
|
||||
for id in existing {
|
||||
state.logLines.append("[SKIPPED] "+zip.path+" → "+((try library.song(id))?.title ?? String(id))+" (already imported)")
|
||||
}
|
||||
report("Already imported: "+zip.lastPathComponent,force:state.completed == existing.count);continue
|
||||
}
|
||||
report("Extracting "+zip.lastPathComponent,force:archives.isEmpty)
|
||||
@@ -178,20 +217,22 @@ struct Importer {
|
||||
songIDs.formUnion(existing)
|
||||
for id in existing { try remember(audio,signature,id) }
|
||||
state.completed += 1;state.skipped += 1
|
||||
state.logLines.append("[SKIPPED] "+sourcePath(audio)+" (already imported)")
|
||||
report("Already imported: "+audio.lastPathComponent,force:state.completed == 1);continue
|
||||
}
|
||||
guard MediaByteCopy.nonempty(audio) else { throw LibraryError.message("Audio file is empty or unreadable: "+audio.path) }
|
||||
let hash = try audioFingerprint(audio,cancelled:cancelled)
|
||||
let graphicsHash = try cdg.map { try audioFingerprint($0,cancelled:cancelled) }
|
||||
let duplicate = try library.songWithFingerprint(hash)
|
||||
let duplicate = try duplicateWithFingerprint(hash,audio:audio)
|
||||
var sameGraphics = cdg == nil
|
||||
if let graphicsHash,let stored = duplicate?.cdg,FileManager.default.fileExists(atPath:stored) {
|
||||
sameGraphics = try audioFingerprint(URL(fileURLWithPath:stored),cancelled:cancelled) == graphicsHash
|
||||
}
|
||||
if let existing = duplicate,sameGraphics,MediaByteCopy.nonempty(URL(fileURLWithPath:existing.audio)),
|
||||
(!copy || URL(fileURLWithPath:existing.audio).deletingLastPathComponent().standardizedFileURL == media.standardizedFileURL) {
|
||||
(archive == nil || !copy || URL(fileURLWithPath:existing.audio).deletingLastPathComponent().standardizedFileURL == media.standardizedFileURL) {
|
||||
try remember(audio,signature,existing.id);songIDs.insert(existing.id)
|
||||
state.completed += 1;state.skipped += 1
|
||||
state.logLines.append("[SKIPPED] "+sourcePath(audio)+" (identical content already in library)")
|
||||
report("Already in library: "+audio.lastPathComponent,force:state.completed == 1);continue
|
||||
}
|
||||
let dest = copy ? media.appendingPathComponent(hash+"."+audio.pathExtension.lowercased()) : audio.standardizedFileURL
|
||||
@@ -210,6 +251,7 @@ struct Importer {
|
||||
songIDs.insert(id)
|
||||
try remember(audio,signature,id)
|
||||
state.completed += 1
|
||||
state.logLines.append("[IMPORTED] "+sourcePath(audio))
|
||||
report(audio.lastPathComponent,force:state.completed == 1)
|
||||
}
|
||||
for archive in archives {
|
||||
|
||||
Reference in New Issue
Block a user