diff --git a/Music.xcodeproj/project.pbxproj b/Music.xcodeproj/project.pbxproj index c67a2cf..7eb30e7 100644 --- a/Music.xcodeproj/project.pbxproj +++ b/Music.xcodeproj/project.pbxproj @@ -443,7 +443,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 24; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 526E96RFNP; ENABLE_APP_SANDBOX = YES; @@ -488,7 +488,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 24; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 526E96RFNP; ENABLE_APP_SANDBOX = YES; diff --git a/Music/Services/ScannerService.swift b/Music/Services/ScannerService.swift index 4c0eb23..6e009ca 100644 --- a/Music/Services/ScannerService.swift +++ b/Music/Services/ScannerService.swift @@ -159,9 +159,7 @@ final class ScannerService { } } - let attrs = try FileManager.default.attributesOfItem(atPath: url.path) - let fileSize = attrs[.size] as? Int64 ?? 0 - let modDate = attrs[.modificationDate] as? Date ?? Date() + let stats = try TrackFileStats.compute(for: url) return Track( fileURL: url.absoluteString, @@ -179,13 +177,13 @@ final class ScannerService { fileFormat: url.pathExtension.lowercased(), bitrate: bitrate, sampleRate: sampleRate, - fileSize: fileSize, + fileSize: stats.fileSize, playCount: 0, lastPlayedAt: nil, rating: 0, dateAdded: Date(), - dateModified: modDate, - fileHash: Track.computeHash(fileSize: fileSize, modificationDate: modDate) + dateModified: stats.dateModified, + fileHash: stats.fileHash ) } catch { print("Failed to extract metadata from \(url.lastPathComponent): \(error)") diff --git a/Music/Services/TrackFileStats.swift b/Music/Services/TrackFileStats.swift new file mode 100644 index 0000000..a289214 --- /dev/null +++ b/Music/Services/TrackFileStats.swift @@ -0,0 +1,22 @@ +import Foundation + +// Reads a file's size + modification date and derives the library fileHash. +// Centralizes the computation so ScannerService (import) and TrackEditService +// (post-writeback refresh) can never drift. Hash uses Track.computeHash so the +// format stays identical to import-time hashes. +nonisolated struct TrackFileStats: Sendable { + let fileSize: Int64 + let dateModified: Date + let fileHash: String + + static func compute(for url: URL) throws -> TrackFileStats { + let attrs = try FileManager.default.attributesOfItem(atPath: url.path) + let fileSize = attrs[.size] as? Int64 ?? 0 + let modDate = attrs[.modificationDate] as? Date ?? Date() + return TrackFileStats( + fileSize: fileSize, + dateModified: modDate, + fileHash: Track.computeHash(fileSize: fileSize, modificationDate: modDate) + ) + } +} diff --git a/MusicTests/TrackFileStatsTests.swift b/MusicTests/TrackFileStatsTests.swift new file mode 100644 index 0000000..1d50852 --- /dev/null +++ b/MusicTests/TrackFileStatsTests.swift @@ -0,0 +1,26 @@ +import Foundation +import Testing +@testable import Music + +// Verifies the shared file-stat helper reads size/mod-date from disk and +// produces a fileHash identical to Track.computeHash (the existing canonical formula). +struct TrackFileStatsTests { + @Test func compute_matchesTrackComputeHash() throws { + // Step 1: write a temp file with known bytes. + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString + ".bin") + try Data(repeating: 0xAB, count: 1234).write(to: url) + defer { try? FileManager.default.removeItem(at: url) } + + // Step 2: compute stats via the helper. + let stats = try TrackFileStats.compute(for: url) + + // Step 3: independently read attrs and assert the helper agrees. + let attrs = try FileManager.default.attributesOfItem(atPath: url.path) + let size = attrs[.size] as? Int64 ?? -1 + let mod = attrs[.modificationDate] as? Date ?? Date.distantPast + #expect(stats.fileSize == size) + #expect(stats.dateModified == mod) + #expect(stats.fileHash == Track.computeHash(fileSize: size, modificationDate: mod)) + } +}