From 9123a25bc7bef9957e63121066e808bbcfde85e0 Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Sun, 17 Mar 2019 21:30:22 -0400 Subject: [PATCH] Refactor art service --- Persephone.xcodeproj/project.pbxproj | 20 +++ Persephone/Services/AlbumArtService.swift | 130 +----------------- .../Extensions/AlbumArtService+Caching.swift | 43 ++++++ .../AlbumArtService+Filesystem.swift | 63 +++++++++ .../Extensions/AlbumArtService+Remote.swift | 61 ++++++++ 5 files changed, 190 insertions(+), 127 deletions(-) create mode 100644 Persephone/Services/Extensions/AlbumArtService+Caching.swift create mode 100644 Persephone/Services/Extensions/AlbumArtService+Filesystem.swift create mode 100644 Persephone/Services/Extensions/AlbumArtService+Remote.swift diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 557b937..2d7b702 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -30,6 +30,9 @@ E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5306223C019100173814 /* MPDClient+Status.swift */; }; E41E5309223C020400173814 /* MPDClient+Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5308223C020400173814 /* MPDClient+Command.swift */; }; E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530A223C033700173814 /* MPDClient+Album.swift */; }; + E41E530E223EF4CF00173814 /* AlbumArtService+Caching.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530D223EF4CF00173814 /* AlbumArtService+Caching.swift */; }; + E41E5310223EF6CE00173814 /* AlbumArtService+Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */; }; + E41E5312223EF74A00173814 /* AlbumArtService+Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */; }; E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */; }; E421ACA3221F73C4008B2449 /* MediaKeyTap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */; }; E421ACA4221F73C4008B2449 /* MediaKeyTap.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -186,6 +189,9 @@ E41E5306223C019100173814 /* MPDClient+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Status.swift"; sourceTree = ""; }; E41E5308223C020400173814 /* MPDClient+Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Command.swift"; sourceTree = ""; }; E41E530A223C033700173814 /* MPDClient+Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Album.swift"; sourceTree = ""; }; + E41E530D223EF4CF00173814 /* AlbumArtService+Caching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumArtService+Caching.swift"; sourceTree = ""; }; + E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumArtService+Remote.swift"; sourceTree = ""; }; + E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumArtService+Filesystem.swift"; sourceTree = ""; }; E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPrefsViewController.swift; sourceTree = ""; }; E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaKeyTap.framework; path = Carthage/Build/Mac/MediaKeyTap.framework; sourceTree = ""; }; E42A8F3922176D6400A13ED9 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; @@ -433,6 +439,16 @@ path = mpd; sourceTree = ""; }; + E41E530C223EF4BA00173814 /* Extensions */ = { + isa = PBXGroup; + children = ( + E41E530D223EF4CF00173814 /* AlbumArtService+Caching.swift */, + E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */, + E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */, + ); + path = Extensions; + sourceTree = ""; + }; E450AD8922262B420091BED3 /* Operations */ = { isa = PBXGroup; children = ( @@ -482,6 +498,7 @@ E4A83BF2222207BE0098FED6 /* Services */ = { isa = PBXGroup; children = ( + E41E530C223EF4BA00173814 /* Extensions */, E4A83BF3222207D50098FED6 /* AlbumArtService.swift */, ); path = Services; @@ -727,6 +744,7 @@ E4F6B460221E119B00ACF42A /* QueueDataSource.swift in Sources */, E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */, E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */, + E41E5310223EF6CE00173814 /* AlbumArtService+Remote.swift in Sources */, E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */, E4A642DA22090CBE00067D21 /* Status.swift in Sources */, E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */, @@ -755,7 +773,9 @@ E4A83BF4222207D50098FED6 /* AlbumArtService.swift in Sources */, E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, + E41E530E223EF4CF00173814 /* AlbumArtService+Caching.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */, + E41E5312223EF74A00173814 /* AlbumArtService+Filesystem.swift in Sources */, E41E5301223BF99300173814 /* MPDClient+Queue.swift in Sources */, E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */, E450AD9D2229B9050091BED3 /* String.swift in Sources */, diff --git a/Persephone/Services/AlbumArtService.swift b/Persephone/Services/AlbumArtService.swift index 91fc2c5..8b151f4 100644 --- a/Persephone/Services/AlbumArtService.swift +++ b/Persephone/Services/AlbumArtService.swift @@ -7,11 +7,11 @@ // import Cocoa -import SwiftyJSON -import PromiseKit -import PMKFoundation class AlbumArtService: NSObject { + let cachedArtworkSize = 180 + let cachedArtworkQuality: CGFloat = 0.5 + static var shared = AlbumArtService() var preferences = Preferences() @@ -20,132 +20,8 @@ class AlbumArtService: NSObject { 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) - } - } } } } diff --git a/Persephone/Services/Extensions/AlbumArtService+Caching.swift b/Persephone/Services/Extensions/AlbumArtService+Caching.swift new file mode 100644 index 0000000..92c30af --- /dev/null +++ b/Persephone/Services/Extensions/AlbumArtService+Caching.swift @@ -0,0 +1,43 @@ +// +// AlbumArtService+Caching.swift +// Persephone +// +// Created by Daniel Barber on 2019/3/17. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +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 } + + 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 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) + } +} diff --git a/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift b/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift new file mode 100644 index 0000000..8e8c08e --- /dev/null +++ b/Persephone/Services/Extensions/AlbumArtService+Filesystem.swift @@ -0,0 +1,63 @@ +// +// AlbumArtService+Filesystem.swift +// Persephone +// +// Created by Daniel Barber on 2019/3/17. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa + +extension AlbumArtService { + func getArtworkFromFilesystem( + for album: AlbumItem, + callback: @escaping (_ image: NSImage) -> Void + ) { + let coverArtFilenames = [ + "folder.jpg", + "cover.jpg", + "\(album.artist) - \(album.title).jpg" + ] + + let 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 let image = self.tryImage(coverArtURI) { + self.cacheArtwork( + for: album, + data: image.jpegData(compressionQuality: self.cachedArtworkQuality) + ) + callback(image) + break + } + } + } + + AppDelegate.mpdClient.getAlbumURI( + for: album.album, + callback: callback + ) + } + + func tryImage(_ filePath: String) -> NSImage? { + if FileManager.default.fileExists(atPath: filePath), + let data = FileManager.default.contents(atPath: filePath), + let image = NSImage(data: data) { + + let imageThumb = image.toFitBox( + size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize) + ) + + return imageThumb + } else { + return nil + } + } +} diff --git a/Persephone/Services/Extensions/AlbumArtService+Remote.swift b/Persephone/Services/Extensions/AlbumArtService+Remote.swift new file mode 100644 index 0000000..2c017d1 --- /dev/null +++ b/Persephone/Services/Extensions/AlbumArtService+Remote.swift @@ -0,0 +1,61 @@ +// +// AlbumArtService+Remote.swift +// Persephone +// +// Created by Daniel Barber on 2019/3/17. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import Cocoa +import SwiftyJSON +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) + } + + 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) + } + } + } + } + } +}