1
1
mirror of https://github.com/danbee/persephone synced 2025-03-04 08:39:11 +00:00
persephone/Persephone/Services/AlbumArtService.swift
Dan Barber e8b58b7686
Scale down cover images
This brings memory usage (for my music library) down from 2+GB to less
than 300MB. 👍🏼
2019-03-20 20:06:23 -04:00

153 lines
4.9 KiB
Swift

//
// AlbumArtService.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/23.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
import SwiftyJSON
import PromiseKit
import PMKFoundation
class AlbumArtService: NSObject {
static var shared = AlbumArtService()
var preferences = Preferences()
var session = URLSession(configuration: .default)
let cacheQueue = DispatchQueue(label: "albumArtCacheQueue", attributes: .concurrent)
func fetchAlbumArt(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) {
cacheQueue.async { [unowned self] in
//print("Trying cache")
if !self.getCachedArtwork(for: album, callback: callback) {
self.getArtworkFromFilesystem(for: album, callback: callback)
// if !self.getArtworkFromFilesystem(for: album, callback: callback) {
// // self.getRemoteArtwork(for: album, callback: callback)
// }
}
}
}
func getCachedArtwork(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) -> Bool {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(bundleIdentifier)
else { return false }
let cacheFilePath = cacheDir.appendingPathComponent(album.hash).path
if FileManager.default.fileExists(atPath: cacheFilePath) {
guard let data = FileManager.default.contents(atPath: cacheFilePath),
let image = NSImage(data: data)
else { return true }
callback(image)
return true
} else {
return false
}
}
func getArtworkFromFilesystem(
for album: AlbumItem,
callback: @escaping (_ image: NSImage) -> Void
) {
let coverArtFilenames = [
"folder.jpg",
"cover.jpg",
"\(album.artist) - \(album.title).jpg"
]
AppDelegate.mpdClient.getAlbumURI(
for: album.album,
callback: { (_ albumURI: String?) in
guard let albumURI = albumURI
else { return }
let musicDir = self.preferences.expandedMpdLibraryDir
let fullAlbumURI = "\(musicDir)/\(albumURI)"
for coverArtFilename in coverArtFilenames {
let coverArtURI = "\(fullAlbumURI)/\(coverArtFilename)"
if FileManager.default.fileExists(atPath: coverArtURI),
let data = FileManager.default.contents(atPath: coverArtURI),
let image = NSImage(data: data) {
let imageThumb = image.toFitBox(
size: NSSize(width: 180, height: 180)
)
self.cacheArtwork(
for: album,
data: imageThumb.jpegData(compressionQuality: 0.5)
)
callback(imageThumb)
break
}
}
}
)
}
func cacheArtwork(for album: AlbumItem, data: Data?) {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(bundleIdentifier)
else { return }
let cacheFilePath = cacheDir.appendingPathComponent(album.hash).path
FileManager.default.createFile(atPath: cacheFilePath, contents: data, attributes: nil)
}
func getRemoteArtwork(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) {
let albumArtWorkItem = DispatchWorkItem() {
self.getArtworkFromMusicBrainz(for: album, callback: callback)
}
AlbumArtQueue.shared.addToQueue(workItem: albumArtWorkItem)
}
func getArtworkFromMusicBrainz(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) {
guard var urlComponents = URLComponents(string: "https://musicbrainz.org/ws/2/release/")
else { return }
urlComponents.query = "query=artist:\(album.artist) AND release:\(album.title) AND country:US&limit=1&fmt=json"
guard let searchURL = urlComponents.url
else { return }
URLSession.shared.dataTask(.promise, with: searchURL).validate()
.compactMap {
JSON($0.data)
}.compactMap {
$0["releases"][0]["id"].string
}.compactMap {
URLComponents(string: "https://coverartarchive.org/release/\($0)/front-500")
}.then { (urlComponents: URLComponents?) -> Promise<(data: Data, response: URLResponse)> in
let url = urlComponents!.url
return URLSession.shared.dataTask(.promise, with: url!).validate()
}.compactMap {
self.cacheArtwork(for: album, data: $0.data)
return NSImage(data: $0.data)
}.done {
callback($0)
}.catch {
if let httpError = $0 as? PMKHTTPError {
switch httpError {
case let .badStatusCode(statusCode, _, _):
switch statusCode {
case 404:
self.cacheArtwork(for: album, data: Data())
default:
self.getRemoteArtwork(for: album, callback: callback)
}
}
}
}
}
}