1
1
mirror of https://github.com/danbee/persephone synced 2025-03-04 08:39:11 +00:00

Big album art display works!

Still to be done: if an image does not exist on the filesystem it will
keep fetching it remotely. We probably shouldn't do this.
This commit is contained in:
Daniel Barber 2019-03-29 15:28:46 -04:00
parent 812af07c1a
commit 6bec0c170d
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
8 changed files with 155 additions and 105 deletions

View File

@ -298,23 +298,23 @@
E407861A2110CE6E006887B1 /* Persephone */ = {
isa = PBXGroup;
children = (
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */,
E450AD8922262B420091BED3 /* Operations */,
E4A83BF2222207BE0098FED6 /* Services */,
E4A83BEC2221F5DD0098FED6 /* Preferences */,
E407861B2110CE6E006887B1 /* AppDelegate.swift */,
E407861F2110CE70006887B1 /* Assets.xcassets */,
E4D1B597220BA3A20026F233 /* Controllers */,
E4F6B45E221E117600ACF42A /* DataSources */,
E408D3B7220DE8CC0006D9BE /* Extensions */,
E41B22C721FB966C00D544F6 /* include */,
E40786242110CE70006887B1 /* Info.plist */,
E47E2FE32220AA0700F747E6 /* Layouts */,
E4F6B461221E124700ACF42A /* Models */,
E4F6B45E221E117600ACF42A /* DataSources */,
E407861F2110CE70006887B1 /* Assets.xcassets */,
E408D3B7220DE8CC0006D9BE /* Extensions */,
E4D1B598220BA3C90026F233 /* Resources */,
E4D1B597220BA3A20026F233 /* Controllers */,
E408D3C3220E138B0006D9BE /* Views */,
E41B22C721FB966C00D544F6 /* include */,
E407861B2110CE6E006887B1 /* AppDelegate.swift */,
E40786242110CE70006887B1 /* Info.plist */,
E40786252110CE70006887B1 /* Persephone.entitlements */,
E4A642DB220912FA00067D21 /* MPDClient */,
E450AD8922262B420091BED3 /* Operations */,
E40786252110CE70006887B1 /* Persephone.entitlements */,
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */,
E4A83BEC2221F5DD0098FED6 /* Preferences */,
E4D1B598220BA3C90026F233 /* Resources */,
E4A83BF2222207BE0098FED6 /* Services */,
E408D3C3220E138B0006D9BE /* Views */,
);
path = Persephone;
sourceTree = "<group>";
@ -342,10 +342,10 @@
children = (
E4928E0A2218D62A001D4BEA /* CGColor.swift */,
E408D3B5220DD8970006D9BE /* Notification.swift */,
E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */,
E40FE71A221B904300A4223F /* NSEvent.swift */,
E435E3E1221CD4E200184CFC /* NSFont.swift */,
E435E3E3221CD75D00184CFC /* NSImage.swift */,
E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */,
E450AD9C2229B9050091BED3 /* String.swift */,
);
path = Extensions;
@ -354,15 +354,15 @@
E408D3BC220E03D20006D9BE /* Extensions */ = {
isa = PBXGroup;
children = (
E41E530A223C033700173814 /* MPDClient+Album.swift */,
E41E5308223C020400173814 /* MPDClient+Command.swift */,
E41E52FC223BF87300173814 /* MPDClient+Connection.swift */,
E42410B52241B956005ED6DF /* MPDClient+Database.swift */,
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */,
E41E5300223BF99300173814 /* MPDClient+Queue.swift */,
E41E5302223BF9C300173814 /* MPDClient+Idle.swift */,
E41E5304223BFB0700173814 /* MPDClient+Error.swift */,
E41E5302223BF9C300173814 /* MPDClient+Idle.swift */,
E41E5300223BF99300173814 /* MPDClient+Queue.swift */,
E41E5306223C019100173814 /* MPDClient+Status.swift */,
E41E5308223C020400173814 /* MPDClient+Command.swift */,
E41E530A223C033700173814 /* MPDClient+Album.swift */,
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */,
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
);
path = Extensions;
@ -450,8 +450,8 @@
isa = PBXGroup;
children = (
E41E530D223EF4CF00173814 /* AlbumArtService+Caching.swift */,
E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */,
E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */,
E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -475,10 +475,10 @@
E4A642DB220912FA00067D21 /* MPDClient */ = {
isa = PBXGroup;
children = (
E41B22C521FB932700D544F6 /* MPDClient.swift */,
E408D3BC220E03D20006D9BE /* Extensions */,
E4D1B595220BA27C0026F233 /* Protocols */,
E4D1B594220BA2490026F233 /* Models */,
E41B22C521FB932700D544F6 /* MPDClient.swift */,
E4D1B595220BA27C0026F233 /* Protocols */,
);
path = MPDClient;
sourceTree = "<group>";
@ -494,8 +494,8 @@
E4A83BED2221F5E60098FED6 /* Controllers */ = {
isa = PBXGroup;
children = (
E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */,
E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */,
E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */,
E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */,
E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */,
);
@ -505,8 +505,8 @@
E4A83BF2222207BE0098FED6 /* Services */ = {
isa = PBXGroup;
children = (
E41E530C223EF4BA00173814 /* Extensions */,
E4A83BF3222207D50098FED6 /* AlbumArtService.swift */,
E41E530C223EF4BA00173814 /* Extensions */,
);
path = Services;
sourceTree = "<group>";
@ -514,12 +514,12 @@
E4D1B594220BA2490026F233 /* Models */ = {
isa = PBXGroup;
children = (
E4EB237A220F7CF1008C70C0 /* MPDAlbum.swift */,
E45962C52241A78500FC1A1E /* MPDCommand.swift */,
E4C8B53D22349002009A20F3 /* MPDIdle.swift */,
E4EB2378220F10B8008C70C0 /* MPDPair.swift */,
E4E8CC9922075D370024217A /* MPDSong.swift */,
E4A642D922090CBE00067D21 /* MPDStatus.swift */,
E4EB2378220F10B8008C70C0 /* MPDPair.swift */,
E4EB237A220F7CF1008C70C0 /* MPDAlbum.swift */,
E4C8B53D22349002009A20F3 /* MPDIdle.swift */,
E45962C52241A78500FC1A1E /* MPDCommand.swift */,
);
path = Models;
sourceTree = "<group>";
@ -535,12 +535,12 @@
E4D1B597220BA3A20026F233 /* Controllers */ = {
isa = PBXGroup;
children = (
E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */,
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */,
E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */,
E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */,
E4E8CC932206097F0024217A /* NotificationsController.swift */,
E4E8CC912204F4B80024217A /* QueueViewController.swift */,
E465049921E94DF500A70F4C /* WindowController.swift */,
E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */,
);
path = Controllers;
sourceTree = "<group>";
@ -557,8 +557,8 @@
E4F6B45E221E117600ACF42A /* DataSources */ = {
isa = PBXGroup;
children = (
E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */,
E4F6B466221E233200ACF42A /* AlbumDataSource.swift */,
E4F6B45F221E119B00ACF42A /* QueueDataSource.swift */,
);
path = DataSources;
sourceTree = "<group>";

View File

@ -7,6 +7,7 @@
//
import Cocoa
import PromiseKit
class QueueViewController: NSViewController,
NSOutlineViewDelegate {
@ -66,8 +67,18 @@ class QueueViewController: NSViewController,
}
func updateAlbumArt() {
if let playingSong = dataSource.queue.first(where: { $0.isPlaying }) {
if let playingQueueItem = dataSource.queue.first(where: { $0.isPlaying }) {
let albumArtService = AlbumArtService(song: playingQueueItem.song)
albumArtService.fetchBigAlbumArt()
.done() {
guard let image = $0 else { return }
self.queueAlbumArtImage.image = image.toFitBox(
size: NSSize(width: 500, height: 500)
)
}
.cauterize()
} else {
queueAlbumArtImage.image = NSImage.defaultCoverArt
}

View File

@ -7,6 +7,7 @@
//
import Cocoa
import PromiseKit
class AlbumDataSource: NSObject, NSCollectionViewDataSource {
var albums: [Album] = []
@ -26,13 +27,15 @@ class AlbumDataSource: NSObject, NSCollectionViewDataSource {
AppDelegate.mpdClient.getAlbumFirstSong(for: albums[indexPath.item].mpdAlbum) {
guard let song = $0 else { return }
AlbumArtService(song: Song(mpdSong: song)).fetchAlbumArt { image in
self.albums[indexPath.item].coverArt = image
AlbumArtService(song: Song(mpdSong: song))
.fetchAlbumArt()
.done { image in
self.albums[indexPath.item].coverArt = image
DispatchQueue.main.async {
collectionView.reloadItems(at: [indexPath])
DispatchQueue.main.async {
collectionView.reloadItems(at: [indexPath])
}
}
}
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="System colors introduced in macOS 10.14" minToolsVersion="10.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -599,7 +599,7 @@
<rect key="frame" x="0.0" y="220" width="328" height="328"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Dw3-M5-tWY">
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Dw3-M5-tWY">
<rect key="frame" x="0.0" y="0.0" width="328" height="328"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="blankAlbum" id="IoN-3N-TCb"/>
</imageView>

View File

@ -18,38 +18,54 @@ class AlbumArtService {
let cachedArtworkQuality: CGFloat = 0.5
var session = URLSession(configuration: .default)
let cacheQueue = DispatchQueue(label: "albumArtCacheQueue")
let artworkQueue = DispatchQueue(
label: "albumArtQueue",
qos: .background,
attributes: .concurrent
)
init(song: Song) {
self.song = song
self.album = song.album
}
func fetchAlbumArt(callback: @escaping (_ image: NSImage?) -> Void) {
cacheQueue.async {
firstly {
self.getCachedArtwork()
}.then { artwork -> Promise<NSImage?> in
artwork.map { Promise.value($0 as NSImage?) } ?? self.cacheIfNecessary(self.getArtworkFromFilesystem())
}.then { artwork -> Promise<NSImage?> in
artwork.map { Promise.value($0 as NSImage?) } ?? 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 fetchBigAlbumArt() -> Promise<NSImage?> {
return firstly {
self.getArtworkFromFilesystem()
}.then { (artwork: NSImage?) -> Promise<NSImage?> in
artwork.map(Promise.value) ?? self.getRemoteArtwork().map(Optional.some)
}
}
func cacheIfNecessary(_ promise: Promise<NSImage?>) -> Promise<NSImage?> {
return promise.get { image in
if let data = image?.jpegData(compressionQuality: self.cachedArtworkQuality) {
self.cacheArtwork(data: data)
func fetchAlbumArt() -> Guarantee<NSImage?> {
return firstly {
self.getCachedArtwork()
}.then { (artwork: NSImage?) -> Promise<NSImage?> in
artwork.map(Promise.value) ?? self.getArtworkFromFilesystem()
}.then { (artwork: NSImage?) -> Promise<NSImage?> in
artwork.map(Promise.value) ?? self.getRemoteArtwork().map(Optional.some)
}.compactMap(on: artworkQueue) {
return self.sizeAndCacheImage($0).map(Optional.some)
}.recover { error in
self.cacheArtwork(data: Data())
return .value(nil)
}
}
func sizeAndCacheImage(_ image: NSImage?) -> NSImage? {
switch image {
case nil:
self.cacheArtwork(data: Data())
return image
case let image:
if self.isArtworkCached() {
return image
} else {
let sizedImage = image?.toFitBox(
size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize)
)
self.cacheArtwork(data: sizedImage?.jpegData(compressionQuality: self.cachedArtworkQuality))
return sizedImage
}
}
}

View File

@ -14,24 +14,38 @@ extension AlbumArtService {
func getCachedArtwork() -> Promise<NSImage?> {
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:))
artworkQueue.async {
if self.isArtworkCached() {
let cacheFilePath = AlbumArtService.cacheDir.appendingPathComponent(self.album.hash).path
let data = FileManager.default.contents(atPath: cacheFilePath)
let image = NSImage(data: data ?? Data()) ?? NSImage.defaultCoverArt
seal.fulfill(image)
seal.fulfill(image)
} else {
seal.fulfill(nil)
}
}
}
}
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)
else { return }
artworkQueue.async {
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
let cacheFilePath = cacheDir.appendingPathComponent(self.album.hash).path
if !FileManager.default.fileExists(atPath: cacheFilePath) {
FileManager.default.createFile(atPath: cacheFilePath, contents: data, attributes: nil)
if !self.isArtworkCached() {
FileManager.default.createFile(atPath: cacheFilePath, contents: data, attributes: nil)
}
}
}
func isArtworkCached() -> Bool {
let cacheFilePath = AlbumArtService.cacheDir.appendingPathComponent(album.hash).path
return FileManager.default.fileExists(atPath: cacheFilePath)
}
}

View File

@ -21,18 +21,21 @@ extension AlbumArtService {
let songPath = self.songPath()
return Promise { seal in
let image = coverArtFilenames
.lazy
.map { "\(musicDir)/\(songPath)/\($0)" }
.compactMap(self.tryImage)
.first
artworkQueue.async {
let image = coverArtFilenames
.lazy
.map { "\(musicDir)/\(songPath)/\($0)" }
.compactMap(self.tryImage)
.first
seal.fulfill(image)
seal.fulfill(image)
}
}
}
func songPath() -> String {
return song.mpdSong
return song
.mpdSong
.uriString
.split(separator: "/")
.dropLast()

View File

@ -18,11 +18,13 @@ extension AlbumArtService {
func getRemoteArtwork() -> Promise<NSImage> {
return Promise { seal in
let albumArtWorkItem = DispatchWorkItem {
self.getArtworkFromMusicBrainz().pipe(to: seal.resolve)
}
artworkQueue.async {
let albumArtWorkItem = DispatchWorkItem {
self.getArtworkFromMusicBrainz().pipe(to: seal.resolve)
}
AlbumArtQueue.shared.addToQueue(workItem: albumArtWorkItem)
AlbumArtQueue.shared.addToQueue(workItem: albumArtWorkItem)
}
}
}
@ -30,25 +32,26 @@ extension AlbumArtService {
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"
return URLSession.shared.dataTask(.promise, with: search.url!).validate()
.compactMap {
JSON($0.data)
}.compactMap {
$0["releases"][0]["id"].string
}.compactMap {
URLComponents(string: "https://coverartarchive.org/release/\($0)/front-500")?.url
}.then { (url: URL?) -> Promise<(data: Data, response: URLResponse)> in
return URLSession.shared.dataTask(.promise, with: url!).validate()
}.compactMap {
NSImage(data: $0.data)?.toFitBox(
size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize)
)
}.recover { error -> Promise<NSImage> in
if case PMKHTTPError.badStatusCode(404, _, _) = error {
throw MusicBrainzError.noArtworkAvailable
} else {
throw error
}
return firstly {
URLSession.shared.dataTask(.promise, with: search.url!).validate()
}.compactMap {
JSON($0.data)
}.compactMap {
$0["releases"][0]["id"].string
}.compactMap {
URLComponents(string: "https://coverartarchive.org/release/\($0)/front-500")?.url
}.then { (url: URL?) -> Promise<(data: Data, response: URLResponse)> in
return URLSession.shared.dataTask(.promise, with: url!).validate()
}.compactMap {
NSImage(data: $0.data)?.toFitBox(
size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize)
)
}.recover { error -> Promise<NSImage> in
if case PMKHTTPError.badStatusCode(404, _, _) = error {
throw MusicBrainzError.noArtworkAvailable
} else {
throw error
}
}
}
}