From d7e81b1d318d5cc0883e8f75dfa3d6f3d5d065e9 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 26 May 2026 23:18:53 +0200 Subject: [PATCH] refactor: remove artworkData BLOB from database, load artwork from files on demand Artwork was stored inline in the tracks table (385MB / 98% of DB size for 8K tracks). Now read directly from audio file metadata via AVAsset when needed, shrinking the DB to ~8MB. --- Music/Models/Track.swift | 4 +--- Music/Services/DatabaseService.swift | 6 ++++++ Music/Services/ScannerService.swift | 5 ----- Music/Views/PlayerControlsView.swift | 30 +++++++++++++++++++++++++--- MusicTests/TrackTests.swift | 1 - 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Music/Models/Track.swift b/Music/Models/Track.swift index 78c15da..3a7cb55 100644 --- a/Music/Models/Track.swift +++ b/Music/Models/Track.swift @@ -22,7 +22,6 @@ nonisolated struct Track: Codable, Identifiable, Equatable, Hashable, Sendable { var bitrate: Int? var sampleRate: Int? var fileSize: Int64 - var artworkData: Data? var playCount: Int var lastPlayedAt: Date? var rating: Int @@ -66,7 +65,6 @@ extension Track { bitrate: Int? = 320, sampleRate: Int? = 44100, fileSize: Int64 = 5_000_000, - artworkData: Data? = nil, playCount: Int = 0, lastPlayedAt: Date? = nil, rating: Int = 0, @@ -79,7 +77,7 @@ extension Track { albumArtist: albumArtist, album: album, genre: genre, year: year, trackNumber: trackNumber, discNumber: discNumber, duration: duration, bpm: bpm, composer: composer, fileFormat: fileFormat, bitrate: bitrate, - sampleRate: sampleRate, fileSize: fileSize, artworkData: artworkData, + sampleRate: sampleRate, fileSize: fileSize, playCount: playCount, lastPlayedAt: lastPlayedAt, rating: rating, dateAdded: dateAdded, dateModified: dateModified, fileHash: fileHash ) diff --git a/Music/Services/DatabaseService.swift b/Music/Services/DatabaseService.swift index a83c22f..df26eb4 100644 --- a/Music/Services/DatabaseService.swift +++ b/Music/Services/DatabaseService.swift @@ -110,6 +110,12 @@ nonisolated final class DatabaseService: Sendable { } } + migrator.registerMigration("v4-drop-artworkData") { db in + try db.alter(table: "tracks") { t in + t.drop(column: "artworkData") + } + } + try migrator.migrate(db) } diff --git a/Music/Services/ScannerService.swift b/Music/Services/ScannerService.swift index 76c8b0a..4c0eb23 100644 --- a/Music/Services/ScannerService.swift +++ b/Music/Services/ScannerService.swift @@ -90,8 +90,6 @@ final class ScannerService { var discNumber: Int? var bpm: Int? var composer = "" - var artworkData: Data? - do { let metadata = try await asset.load(.metadata) @@ -106,8 +104,6 @@ final class ScannerService { if albumArtist == "Unknown" { albumArtist = val } case .commonKeyAlbumName: album = (try? await item.load(.stringValue)) ?? "Unknown" - case .commonKeyArtwork: - artworkData = try? await item.load(.dataValue) case .commonKeyCreator: composer = (try? await item.load(.stringValue)) ?? "" default: @@ -184,7 +180,6 @@ final class ScannerService { bitrate: bitrate, sampleRate: sampleRate, fileSize: fileSize, - artworkData: artworkData, playCount: 0, lastPlayedAt: nil, rating: 0, diff --git a/Music/Views/PlayerControlsView.swift b/Music/Views/PlayerControlsView.swift index 2fb488c..b7c04e4 100644 --- a/Music/Views/PlayerControlsView.swift +++ b/Music/Views/PlayerControlsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AVFoundation struct PlayerControlsView: View { let currentTrack: Track? @@ -20,6 +21,7 @@ struct PlayerControlsView: View { @State private var isDragging = false @State private var dragValue: Double = 0 + @State private var artworkImage: NSImage? var body: some View { VStack(spacing: 0) { @@ -38,6 +40,29 @@ struct PlayerControlsView: View { .padding(.vertical, 8) } .background(.bar) + .onChange(of: currentTrack?.id) { + loadArtwork() + } + .onAppear { + loadArtwork() + } + } + + private func loadArtwork() { + guard let urlString = currentTrack?.fileURL, + let url = URL(string: urlString) else { + artworkImage = nil + return + } + Task.detached { + let asset = AVURLAsset(url: url) + let metadata = try? await asset.load(.metadata) + let data = try? await metadata? + .first { $0.commonKey == .commonKeyArtwork }? + .load(.dataValue) + let image = data.flatMap { NSImage(data: $0) } + await MainActor.run { artworkImage = image } + } } private var nowPlayingSection: some View { @@ -46,9 +71,8 @@ struct PlayerControlsView: View { .fill(.quaternary) .frame(width: 44, height: 44) .overlay { - if let data = currentTrack?.artworkData, - let nsImage = NSImage(data: data) { - Image(nsImage: nsImage) + if let artworkImage { + Image(nsImage: artworkImage) .resizable() .aspectRatio(contentMode: .fill) } else { diff --git a/MusicTests/TrackTests.swift b/MusicTests/TrackTests.swift index 170852b..ddaa1a0 100644 --- a/MusicTests/TrackTests.swift +++ b/MusicTests/TrackTests.swift @@ -27,7 +27,6 @@ struct TrackTests { t.column("bitrate", .integer) t.column("sampleRate", .integer) t.column("fileSize", .integer).notNull() - t.column("artworkData", .blob) t.column("playCount", .integer).notNull().defaults(to: 0) t.column("lastPlayedAt", .datetime) t.column("rating", .integer).notNull().defaults(to: 0)