diff --git a/Persephone/DataSources/AlbumDataSource.swift b/Persephone/DataSources/AlbumDataSource.swift index 7a5d67d..b672275 100644 --- a/Persephone/DataSources/AlbumDataSource.swift +++ b/Persephone/DataSources/AlbumDataSource.swift @@ -23,7 +23,7 @@ class AlbumDataSource: NSObject, NSCollectionViewDataSource { albumViewItem.setAlbum(albums[indexPath.item]) if albums[indexPath.item].coverArt == nil { - AlbumArtService.shared.fetchAlbumArt(for: albums[indexPath.item]) { image in + AlbumArtService(album: albums[indexPath.item]).fetchAlbumArt { image in self.albums[indexPath.item].coverArt = image DispatchQueue.main.async { diff --git a/Persephone/Services/AlbumArtService.swift b/Persephone/Services/AlbumArtService.swift index ecdb88f..564413d 100644 --- a/Persephone/Services/AlbumArtService.swift +++ b/Persephone/Services/AlbumArtService.swift @@ -7,23 +7,69 @@ // import Cocoa +import PromiseKit -class AlbumArtService: NSObject { +class AlbumArtService { var preferences = Preferences() + let album: AlbumItem let cachedArtworkSize = 180 let cachedArtworkQuality: CGFloat = 0.5 - static var shared = AlbumArtService() - var session = URLSession(configuration: .default) - let cacheQueue = DispatchQueue(label: "albumArtCacheQueue", attributes: .concurrent) + let cacheQueue = DispatchQueue(label: "albumArtCacheQueue") - func fetchAlbumArt(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) { - cacheQueue.async { [unowned self] in - if !self.getCachedArtwork(for: album, callback: callback) { - self.getArtworkFromFilesystem(for: album, callback: callback) + init(album: AlbumItem) { + self.album = album + } + + func fetchAlbumArt(callback: @escaping (_ image: NSImage?) -> Void) { + cacheQueue.async { + firstly { + self.getCachedArtwork() + }.then { artwork -> Promise in + artwork.map(Promise.value) ?? self.cacheIfNecessary(self.getArtworkFromFilesystem()) + }.then { artwork -> Promise in + artwork.map(Promise.value) ?? self.cacheIfNecessary(self.getArtworkFromMusicBrainz().map(Optional.some)) + }.tap { result in + switch result { + case .fulfilled(nil), .rejected(MusicBrainzError.noArtworkAvailable): + self.cacheArtwork(data: Data()) + default: + break + } + }.recover { error in + .value(nil) + }.done(callback) + } + } + + func cacheIfNecessary(_ promise: Promise) -> Promise { + return promise.get { image in + if let data = image?.jpegData(compressionQuality: self.cachedArtworkQuality) { + self.cacheArtwork(data: data) } } } } + + +//getCachedArtwork +// .then { +// callback($0) +// } +// .catch { +// getFileSystemArtwork +// } +// .then { +// callback($0) +// } +// .catch { +// getRemoteArtwork +// }4 +// .then { +// callback($0) +// } +// +//// [() -> Promise] +//// () -> Promise diff --git a/Persephone/Services/Extensions/AlbumArtService+Caching.swift b/Persephone/Services/Extensions/AlbumArtService+Caching.swift index 92c30af..ae5c839 100644 --- a/Persephone/Services/Extensions/AlbumArtService+Caching.swift +++ b/Persephone/Services/Extensions/AlbumArtService+Caching.swift @@ -7,30 +7,22 @@ // import Cocoa +import PromiseKit extension AlbumArtService { - 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 } + static let cacheDir = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent(Bundle.main.bundleIdentifier!) - let cacheFilePath = cacheDir.appendingPathComponent(album.hash).path + func getCachedArtwork() -> Promise { + return Promise { seal in + let cacheFilePath = AlbumArtService.cacheDir.appendingPathComponent(album.hash).path + let data = FileManager.default.contents(atPath: cacheFilePath) + let image = data.flatMap(NSImage.init(data:)) - 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 + seal.fulfill(image) } } - func cacheArtwork(for album: AlbumItem, data: Data?) { + func cacheArtwork(data: Data?) { guard let bundleIdentifier = Bundle.main.bundleIdentifier, let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent(bundleIdentifier) diff --git a/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift b/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift index 3989a1e..773d2cc 100644 --- a/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift +++ b/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift @@ -7,56 +7,36 @@ // import Cocoa +import PromiseKit extension AlbumArtService { - func getArtworkFromFilesystem( - for album: AlbumItem, - callback: @escaping (_ image: NSImage) -> Void - ) { - var tryImage: NSImage? - + func getArtworkFromFilesystem() -> Promise { let coverArtFilenames = [ "folder.jpg", "cover.jpg", "\(album.artist) - \(album.title).jpg" ] - let callback = { (_ albumURI: String?) in - guard let albumURI = albumURI - else { return } + return getAlbumURI().map { albumURI in + let musicDir = self.preferences.expandedMpdLibraryDir - let musicDir = self.preferences.expandedMpdLibraryDir - let fullAlbumURI = "\(musicDir)/\(albumURI)" - - for coverArtFilename in coverArtFilenames { - let coverArtURI = "\(fullAlbumURI)/\(coverArtFilename)" - - tryImage = self.tryImage(coverArtURI) - - if let image = tryImage { - self.cacheArtwork( - for: album, - data: image.jpegData(compressionQuality: self.cachedArtworkQuality) - ) - callback(image) - break - } + return coverArtFilenames + .lazy + .map { "\(musicDir)/\($0)" } + .compactMap(self.tryImage) + .first } + } - if tryImage == nil && self.preferences.fetchMissingArtworkFromInternet { - self.getRemoteArtwork(for: album, callback: callback) - } + func getAlbumURI() -> Promise { + return Promise { seal in + AppDelegate.mpdClient.getAlbumURI(for: album.album, callback: seal.fulfill) } - - AppDelegate.mpdClient.getAlbumURI( - for: album.album, - callback: callback - ) + .compactMap { $0 } } func tryImage(_ filePath: String) -> NSImage? { - guard FileManager.default.fileExists(atPath: filePath), - let data = FileManager.default.contents(atPath: filePath), + guard let data = FileManager.default.contents(atPath: filePath), let image = NSImage(data: data) else { return nil } diff --git a/Persephone/Services/Extensions/AlbumArtService+Remote.swift b/Persephone/Services/Extensions/AlbumArtService+Remote.swift index 7c9fa87..5829370 100644 --- a/Persephone/Services/Extensions/AlbumArtService+Remote.swift +++ b/Persephone/Services/Extensions/AlbumArtService+Remote.swift @@ -12,24 +12,25 @@ import PromiseKit import PMKFoundation extension AlbumArtService { - func getRemoteArtwork(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) { - let albumArtWorkItem = DispatchWorkItem() { - self.getArtworkFromMusicBrainz(for: album, callback: callback) - } - - AlbumArtQueue.shared.addToQueue(workItem: albumArtWorkItem) + enum MusicBrainzError: Error { + case noArtworkAvailable } - func getArtworkFromMusicBrainz(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) { - guard var urlComponents = URLComponents(string: "https://musicbrainz.org/ws/2/release/") - else { return } + func getRemoteArtwork() -> Promise { + return Promise { seal in + let albumArtWorkItem = DispatchWorkItem { + self.getArtworkFromMusicBrainz().pipe(to: seal.resolve) + } - urlComponents.query = "query=artist:\(album.artist) AND release:\(album.title) AND country:US&limit=1&fmt=json" + AlbumArtQueue.shared.addToQueue(workItem: albumArtWorkItem) + } + } - guard let searchURL = urlComponents.url - else { return } + func getArtworkFromMusicBrainz() -> Promise { + var search = URLComponents(string: "https://musicbrainz.org/ws/2/release/")! + search.query = "query=artist:\(album.artist) AND release:\(album.title) AND country:US&limit=1&fmt=json" - URLSession.shared.dataTask(.promise, with: searchURL).validate() + return URLSession.shared.dataTask(.promise, with: search.url!).validate() .compactMap { JSON($0.data) }.compactMap { @@ -43,25 +44,11 @@ extension AlbumArtService { NSImage(data: $0.data)?.toFitBox( size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize) ) - }.compactMap { - self.cacheArtwork( - for: album, - data: $0.jpegData(compressionQuality: self.cachedArtworkQuality) - ) - return $0 - }.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) - } - } + }.recover { error -> Promise in + if case PMKHTTPError.badStatusCode(404, _, _) = error { + throw MusicBrainzError.noArtworkAvailable + } else { + throw error } } }