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

Fetching album art via MPD connection now

- Remove music dir prefs
+ Add refresh album art option to album list context menu
+ Wire up album view context menu
+ Force an idle after transport commands
+ Add "clear cache" button
This commit is contained in:
Daniel Barber 2020-02-02 14:48:38 -05:00
parent 50321219b2
commit ff3c7c4856
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
36 changed files with 429 additions and 279 deletions

View File

@ -53,7 +53,6 @@
E44051A0227BB0AB0090CD6F /* UIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E440519F227BB0AB0090CD6F /* UIState.swift */; }; E44051A0227BB0AB0090CD6F /* UIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E440519F227BB0AB0090CD6F /* UIState.swift */; };
E442CCCD2347E73C00004E0C /* Artist.swift in Sources */ = {isa = PBXBuildFile; fileRef = E442CCCC2347E73C00004E0C /* Artist.swift */; }; E442CCCD2347E73C00004E0C /* Artist.swift in Sources */ = {isa = PBXBuildFile; fileRef = E442CCCC2347E73C00004E0C /* Artist.swift */; };
E450AD7E222620A10091BED3 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD7D222620A10091BED3 /* Album.swift */; }; E450AD7E222620A10091BED3 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD7D222620A10091BED3 /* Album.swift */; };
E450AD9522262DF10091BED3 /* CoverArtQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD9422262DF10091BED3 /* CoverArtQueue.swift */; };
E451E36B22BD214D008BE9B2 /* DraggedSongType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E451E36A22BD214D008BE9B2 /* DraggedSongType.swift */; }; E451E36B22BD214D008BE9B2 /* DraggedSongType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E451E36A22BD214D008BE9B2 /* DraggedSongType.swift */; };
E451E36E22BD2501008BE9B2 /* DraggedSong.swift in Sources */ = {isa = PBXBuildFile; fileRef = E451E36C22BD23DB008BE9B2 /* DraggedSong.swift */; }; E451E36E22BD2501008BE9B2 /* DraggedSong.swift in Sources */ = {isa = PBXBuildFile; fileRef = E451E36C22BD23DB008BE9B2 /* DraggedSong.swift */; };
E45878382296173C00586A1C /* AlbumDetailSongRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45878372296173C00586A1C /* AlbumDetailSongRowView.swift */; }; E45878382296173C00586A1C /* AlbumDetailSongRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45878372296173C00586A1C /* AlbumDetailSongRowView.swift */; };
@ -81,6 +80,8 @@
E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* MPDStatus.swift */; }; E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* MPDStatus.swift */; };
E4A83BEF2221F8CF0098FED6 /* CoverArtPrefsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BEE2221F8CF0098FED6 /* CoverArtPrefsController.swift */; }; E4A83BEF2221F8CF0098FED6 /* CoverArtPrefsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BEE2221F8CF0098FED6 /* CoverArtPrefsController.swift */; };
E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */; }; E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */; };
E4B079C723E5E0AD0044B6D3 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; };
E4B079C823E5E0AD0044B6D3 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E4B11B53226928F20075461B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B52226928F20075461B /* AppState.swift */; }; E4B11B53226928F20075461B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B52226928F20075461B /* AppState.swift */; };
E4B11B61226A4C000075461B /* PlayerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B60226A4BFF0075461B /* PlayerReducer.swift */; }; E4B11B61226A4C000075461B /* PlayerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B60226A4BFF0075461B /* PlayerReducer.swift */; };
E4B11B63226A4C510075461B /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B62226A4C510075461B /* AppReducer.swift */; }; E4B11B63226A4C510075461B /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B62226A4C510075461B /* AppReducer.swift */; };
@ -90,8 +91,6 @@
E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B72226A6C770075461B /* TrackTimer.swift */; }; E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B72226A6C770075461B /* TrackTimer.swift */; };
E4B11B75226CC4D30075461B /* QueueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B74226CC4D30075461B /* QueueReducer.swift */; }; E4B11B75226CC4D30075461B /* QueueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B74226CC4D30075461B /* QueueReducer.swift */; };
E4B11B79226D346B0075461B /* AlbumListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B78226D346B0075461B /* AlbumListReducer.swift */; }; E4B11B79226D346B0075461B /* AlbumListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B78226D346B0075461B /* AlbumListReducer.swift */; };
E4B11BA62274E44A0075461B /* libmpdclient.2.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E4B11BA72274E4500075461B /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; };
E4B11BA92274EDE30075461B /* Loading.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BA82274EDE30075461B /* Loading.swift */; }; E4B11BA92274EDE30075461B /* Loading.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BA82274EDE30075461B /* Loading.swift */; };
E4B11BB62275374B0075461B /* UserNotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BB52275374B0075461B /* UserNotificationsController.swift */; }; E4B11BB62275374B0075461B /* UserNotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BB52275374B0075461B /* UserNotificationsController.swift */; };
E4B11BB8227538FA0075461B /* CurrentCoverArtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BB7227538FA0075461B /* CurrentCoverArtView.swift */; }; E4B11BB8227538FA0075461B /* CurrentCoverArtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BB7227538FA0075461B /* CurrentCoverArtView.swift */; };
@ -100,11 +99,14 @@
E4B11BC22275EE410075461B /* AlbumListActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BC12275EE410075461B /* AlbumListActions.swift */; }; E4B11BC22275EE410075461B /* AlbumListActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BC12275EE410075461B /* AlbumListActions.swift */; };
E4B3DF6523D66A4400728F6B /* QueueSongCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */; }; E4B3DF6523D66A4400728F6B /* QueueSongCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */; };
E4B5AE7E22F4C49600CCEC65 /* MPDServerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B5AE7D22F4C49600CCEC65 /* MPDServerDelegate.swift */; }; E4B5AE7E22F4C49600CCEC65 /* MPDServerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B5AE7D22F4C49600CCEC65 /* MPDServerDelegate.swift */; };
E4BB7F8F23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB7F8E23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift */; };
E4BB7F9323E9150A00906E2F /* CoverArtService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB7F9223E9150A00906E2F /* CoverArtService.swift */; };
E4BBD2F323357C0700702C16 /* ArtistListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BBD2F223357C0700702C16 /* ArtistListState.swift */; }; E4BBD2F323357C0700702C16 /* ArtistListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BBD2F223357C0700702C16 /* ArtistListState.swift */; };
E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */; }; E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */; };
E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53D22349002009A20F3 /* MPDIdle.swift */; }; E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53D22349002009A20F3 /* MPDIdle.swift */; };
E4D3BFA622B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */; }; E4D3BFA622B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */; };
E4DA820623D6236200C1EE58 /* NSSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DA820523D6236200C1EE58 /* NSSize.swift */; }; E4DA820623D6236200C1EE58 /* NSSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DA820523D6236200C1EE58 /* NSSize.swift */; };
E4DCCFAE23E4DB5D009A8113 /* MPDClientWrapper.c in Sources */ = {isa = PBXBuildFile; fileRef = E4DCCFAD23E4DB5D009A8113 /* MPDClientWrapper.c */; };
E4E7A6AD22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */; }; E4E7A6AD22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */; };
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; }; E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; };
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; }; E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; };
@ -151,15 +153,15 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
E4B11BA52274E43B0075461B /* Embed Frameworks */ = { E4B079C923E5E0AD0044B6D3 /* Embed Libraries */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
dstPath = ""; dstPath = "";
dstSubfolderSpec = 10; dstSubfolderSpec = 10;
files = ( files = (
E4B11BA62274E44A0075461B /* libmpdclient.2.dylib in Embed Frameworks */, E4B079C823E5E0AD0044B6D3 /* libmpdclient.2.dylib in Embed Libraries */,
); );
name = "Embed Frameworks"; name = "Embed Libraries";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
@ -254,7 +256,6 @@
E440519F227BB0AB0090CD6F /* UIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIState.swift; sourceTree = "<group>"; }; E440519F227BB0AB0090CD6F /* UIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIState.swift; sourceTree = "<group>"; };
E442CCCC2347E73C00004E0C /* Artist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Artist.swift; sourceTree = "<group>"; }; E442CCCC2347E73C00004E0C /* Artist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Artist.swift; sourceTree = "<group>"; };
E450AD7D222620A10091BED3 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; }; E450AD7D222620A10091BED3 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
E450AD9422262DF10091BED3 /* CoverArtQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtQueue.swift; sourceTree = "<group>"; };
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PersephoneBridgingHeader.h; sourceTree = "<group>"; }; E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PersephoneBridgingHeader.h; sourceTree = "<group>"; };
E451E36A22BD214D008BE9B2 /* DraggedSongType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSongType.swift; sourceTree = "<group>"; }; E451E36A22BD214D008BE9B2 /* DraggedSongType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSongType.swift; sourceTree = "<group>"; };
E451E36C22BD23DB008BE9B2 /* DraggedSong.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSong.swift; sourceTree = "<group>"; }; E451E36C22BD23DB008BE9B2 /* DraggedSong.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSong.swift; sourceTree = "<group>"; };
@ -294,11 +295,16 @@
E4B11BC12275EE410075461B /* AlbumListActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumListActions.swift; sourceTree = "<group>"; }; E4B11BC12275EE410075461B /* AlbumListActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumListActions.swift; sourceTree = "<group>"; };
E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueSongCoverView.swift; sourceTree = "<group>"; }; E4B3DF6423D66A4400728F6B /* QueueSongCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueSongCoverView.swift; sourceTree = "<group>"; };
E4B5AE7D22F4C49600CCEC65 /* MPDServerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDServerDelegate.swift; sourceTree = "<group>"; }; E4B5AE7D22F4C49600CCEC65 /* MPDServerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDServerDelegate.swift; sourceTree = "<group>"; };
E4BB7F8E23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDAlbumArtImageDataProvider.swift; sourceTree = "<group>"; };
E4BB7F9223E9150A00906E2F /* CoverArtService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtService.swift; sourceTree = "<group>"; };
E4BBD2F223357C0700702C16 /* ArtistListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistListState.swift; sourceTree = "<group>"; }; E4BBD2F223357C0700702C16 /* ArtistListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistListState.swift; sourceTree = "<group>"; };
E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; }; E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
E4C8B53D22349002009A20F3 /* MPDIdle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDIdle.swift; sourceTree = "<group>"; }; E4C8B53D22349002009A20F3 /* MPDIdle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDIdle.swift; sourceTree = "<group>"; };
E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueueViewController+NSOutlineViewDelegate.swift"; sourceTree = "<group>"; }; E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueueViewController+NSOutlineViewDelegate.swift"; sourceTree = "<group>"; };
E4DA820523D6236200C1EE58 /* NSSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSize.swift; sourceTree = "<group>"; }; E4DA820523D6236200C1EE58 /* NSSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSize.swift; sourceTree = "<group>"; };
E4DCCFAB23E4DB5D009A8113 /* Persephone-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Persephone-Bridging-Header.h"; sourceTree = "<group>"; };
E4DCCFAC23E4DB5D009A8113 /* MPDClientWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPDClientWrapper.h; sourceTree = "<group>"; };
E4DCCFAD23E4DB5D009A8113 /* MPDClientWrapper.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = MPDClientWrapper.c; sourceTree = "<group>"; };
E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumDetailView+NSTableViewDelegate.swift"; sourceTree = "<group>"; }; E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumDetailView+NSTableViewDelegate.swift"; sourceTree = "<group>"; };
E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = "<group>"; }; E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = "<group>"; };
E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; }; E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
@ -322,12 +328,12 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E4B11BA72274E4500075461B /* libmpdclient.2.dylib in Frameworks */,
E49A5485233E5ADC00EED353 /* Differ in Frameworks */, E49A5485233E5ADC00EED353 /* Differ in Frameworks */,
E4677C48233E60E70041474F /* MediaKeyTap in Frameworks */, E4677C48233E60E70041474F /* MediaKeyTap in Frameworks */,
E43BECA0238835DC00CAF1EB /* Kingfisher in Frameworks */, E43BECA0238835DC00CAF1EB /* Kingfisher in Frameworks */,
E49A548E233E5B6000EED353 /* SwiftyJSON in Frameworks */, E49A548E233E5B6000EED353 /* SwiftyJSON in Frameworks */,
E4E96D13233E630800AFD36F /* PMKFoundation in Frameworks */, E4E96D13233E630800AFD36F /* PMKFoundation in Frameworks */,
E4B079C723E5E0AD0044B6D3 /* libmpdclient.2.dylib in Frameworks */,
E49A5482233E580800EED353 /* PromiseKit in Frameworks */, E49A5482233E580800EED353 /* PromiseKit in Frameworks */,
E49A548B233E5B2D00EED353 /* CryptoSwift in Frameworks */, E49A548B233E5B2D00EED353 /* CryptoSwift in Frameworks */,
E49A5488233E5B0000EED353 /* ReSwift in Frameworks */, E49A5488233E5B0000EED353 /* ReSwift in Frameworks */,
@ -387,7 +393,6 @@
E40786242110CE70006887B1 /* Info.plist */, E40786242110CE70006887B1 /* Info.plist */,
E4F6B461221E124700ACF42A /* Models */, E4F6B461221E124700ACF42A /* Models */,
E4A642DB220912FA00067D21 /* MPDClient */, E4A642DB220912FA00067D21 /* MPDClient */,
E450AD8922262B420091BED3 /* Operations */,
E40786252110CE70006887B1 /* Persephone.entitlements */, E40786252110CE70006887B1 /* Persephone.entitlements */,
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */, E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */,
E4A83BF2222207BE0098FED6 /* Services */, E4A83BF2222207BE0098FED6 /* Services */,
@ -446,6 +451,9 @@
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */, E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
E41E5306223C019100173814 /* MPDClient+Status.swift */, E41E5306223C019100173814 /* MPDClient+Status.swift */,
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */, E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */,
E4DCCFAC23E4DB5D009A8113 /* MPDClientWrapper.h */,
E4DCCFAD23E4DB5D009A8113 /* MPDClientWrapper.c */,
E4DCCFAB23E4DB5D009A8113 /* Persephone-Bridging-Header.h */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -574,6 +582,7 @@
E442CCC92347D6FD00004E0C /* Shared */ = { E442CCC92347D6FD00004E0C /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E4BB7F8D23E5E7A300906E2F /* ImageDataProviders */,
E4E13C2C2350D8CB00092A6E /* Layouts */, E4E13C2C2350D8CB00092A6E /* Layouts */,
E408D3B7220DE8CC0006D9BE /* Extensions */, E408D3B7220DE8CC0006D9BE /* Extensions */,
E489E3A222B9D31800CA8CBD /* DraggedSongView.swift */, E489E3A222B9D31800CA8CBD /* DraggedSongView.swift */,
@ -594,14 +603,6 @@
path = Browser; path = Browser;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E450AD8922262B420091BED3 /* Operations */ = {
isa = PBXGroup;
children = (
E450AD9422262DF10091BED3 /* CoverArtQueue.swift */,
);
path = Operations;
sourceTree = "<group>";
};
E4A642DB220912FA00067D21 /* MPDClient */ = { E4A642DB220912FA00067D21 /* MPDClient */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -628,6 +629,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E439109722640213002982E9 /* SongNotifierService.swift */, E439109722640213002982E9 /* SongNotifierService.swift */,
E4BB7F9223E9150A00906E2F /* CoverArtService.swift */,
); );
path = Services; path = Services;
sourceTree = "<group>"; sourceTree = "<group>";
@ -675,6 +677,14 @@
path = Actions; path = Actions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E4BB7F8D23E5E7A300906E2F /* ImageDataProviders */ = {
isa = PBXGroup;
children = (
E4BB7F8E23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift */,
);
path = ImageDataProviders;
sourceTree = "<group>";
};
E4D1B594220BA2490026F233 /* Models */ = { E4D1B594220BA2490026F233 /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -735,7 +745,7 @@
E40786162110CE6E006887B1 /* Resources */, E40786162110CE6E006887B1 /* Resources */,
E42A98F122430936004D8180 /* ShellScript */, E42A98F122430936004D8180 /* ShellScript */,
E421AC9B221F7319008B2449 /* CopyFiles */, E421AC9B221F7319008B2449 /* CopyFiles */,
E4B11BA52274E43B0075461B /* Embed Frameworks */, E4B079C923E5E0AD0044B6D3 /* Embed Libraries */,
); );
buildRules = ( buildRules = (
); );
@ -804,7 +814,7 @@
TargetAttributes = { TargetAttributes = {
E40786172110CE6E006887B1 = { E40786172110CE6E006887B1 = {
CreatedOnToolsVersion = 9.4.1; CreatedOnToolsVersion = 9.4.1;
LastSwiftMigration = 1020; LastSwiftMigration = 1130;
SystemCapabilities = { SystemCapabilities = {
com.apple.HardenedRuntime = { com.apple.HardenedRuntime = {
enabled = 1; enabled = 1;
@ -933,13 +943,14 @@
E4B11B63226A4C510075461B /* AppReducer.swift in Sources */, E4B11B63226A4C510075461B /* AppReducer.swift in Sources */,
E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */, E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */,
E442CCCD2347E73C00004E0C /* Artist.swift in Sources */, E442CCCD2347E73C00004E0C /* Artist.swift in Sources */,
E4DCCFAE23E4DB5D009A8113 /* MPDClientWrapper.c in Sources */,
E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */, E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */,
E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */, E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */,
E4BB7F8F23E5E7BC00906E2F /* MPDAlbumArtImageDataProvider.swift in Sources */,
E4235640228623D2001216D6 /* QueueSongInfoView.swift in Sources */, E4235640228623D2001216D6 /* QueueSongInfoView.swift in Sources */,
E451E36E22BD2501008BE9B2 /* DraggedSong.swift in Sources */, E451E36E22BD2501008BE9B2 /* DraggedSong.swift in Sources */,
E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */, E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */,
E4B11BC02275EE150075461B /* QueueActions.swift in Sources */, E4B11BC02275EE150075461B /* QueueActions.swift in Sources */,
E450AD9522262DF10091BED3 /* CoverArtQueue.swift in Sources */,
E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */, E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */,
E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */, E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */,
E450AD7E222620A10091BED3 /* Album.swift in Sources */, E450AD7E222620A10091BED3 /* Album.swift in Sources */,
@ -978,6 +989,7 @@
E4F26F7923411B1500D45FF9 /* ArtistReducer.swift in Sources */, E4F26F7923411B1500D45FF9 /* ArtistReducer.swift in Sources */,
E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */, E4B11B73226A6C770075461B /* TrackTimer.swift in Sources */,
E44051942278765A0090CD6F /* App.swift in Sources */, E44051942278765A0090CD6F /* App.swift in Sources */,
E4BB7F9323E9150A00906E2F /* CoverArtService.swift in Sources */,
E4B11B79226D346B0075461B /* AlbumListReducer.swift in Sources */, E4B11B79226D346B0075461B /* AlbumListReducer.swift in Sources */,
E47E2FE52220AA0700F747E6 /* FlexibleGridViewLayout.swift in Sources */, E47E2FE52220AA0700F747E6 /* FlexibleGridViewLayout.swift in Sources */,
E41E52FF223BF95E00173814 /* MPDClient+Transport.swift in Sources */, E41E52FF223BF95E00173814 /* MPDClient+Transport.swift in Sources */,
@ -1173,6 +1185,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
@ -1192,6 +1205,8 @@
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone; PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Persephone/MPDClient/Extensions/Persephone-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
SYSTEM_HEADER_SEARCH_PATHS = Persephone/include; SYSTEM_HEADER_SEARCH_PATHS = Persephone/include;
USER_HEADER_SEARCH_PATHS = libmpdclient/output; USER_HEADER_SEARCH_PATHS = libmpdclient/output;
@ -1202,6 +1217,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
@ -1221,6 +1237,7 @@
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone; PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Persephone/MPDClient/Extensions/Persephone-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
SYSTEM_HEADER_SEARCH_PATHS = Persephone/include; SYSTEM_HEADER_SEARCH_PATHS = Persephone/include;
USER_HEADER_SEARCH_PATHS = libmpdclient/output; USER_HEADER_SEARCH_PATHS = libmpdclient/output;

View File

@ -27,6 +27,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E40786172110CE6E006887B1"
BuildableName = "Persephone.app"
BlueprintName = "Persephone"
ReferencedContainer = "container:Persephone.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables> <Testables>
<TestableReference <TestableReference
skipped = "NO"> skipped = "NO">
@ -49,17 +58,6 @@
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
</Testables> </Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E40786172110CE6E006887B1"
BuildableName = "Persephone.app"
BlueprintName = "Persephone"
ReferencedContainer = "container:Persephone.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -81,8 +79,6 @@
ReferencedContainer = "container:Persephone.xcodeproj"> ReferencedContainer = "container:Persephone.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -56,10 +56,14 @@ class AlbumViewItem: NSCollectionViewItem {
} }
func setAlbumCover(_ album: Album) { func setAlbumCover(_ album: Album) {
guard let imagePath = album.coverArtFilePath else { return } guard let song = album.mpdAlbum.firstSong
else { return }
let provider = MPDAlbumArtImageDataProvider(
songUri: song.uriString,
cacheKey: album.hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
albumCoverView.kf.setImage( albumCoverView.kf.setImage(
with: .provider(provider), with: .provider(provider),
placeholder: NSImage.defaultCoverArt, placeholder: NSImage.defaultCoverArt,
@ -89,6 +93,18 @@ class AlbumViewItem: NSCollectionViewItem {
} }
} }
func refreshAlbumArt() {
guard let album = album,
let mpdSong = album.mpdAlbum.firstSong
else { return }
let song = Song(mpdSong: mpdSong)
CoverArtService(song: song).refresh {
self.setAlbumCover(album)
}
}
@IBAction func showAlbumDetail(_ sender: NSButton) { @IBAction func showAlbumDetail(_ sender: NSButton) {
guard let album = album else { return } guard let album = album else { return }
@ -121,4 +137,8 @@ class AlbumViewItem: NSCollectionViewItem {
App.mpdClient.addAlbumToQueue(album: album.mpdAlbum, at: queueLength) App.mpdClient.addAlbumToQueue(album: album.mpdAlbum, at: queueLength)
} }
@IBAction func refreshAlbumArtMenuAction(_ sender: NSMenuItem) {
refreshAlbumArt()
}
} }

View File

@ -35,7 +35,7 @@
</shadow> </shadow>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="defaultCoverArt" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" refusesFirstResponder="YES" state="on" transparent="YES" imageScaling="proportionallyUpOrDown" inset="2" id="t8A-Hz-L38"> <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="defaultCoverArt" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" refusesFirstResponder="YES" state="on" transparent="YES" imageScaling="proportionallyUpOrDown" inset="2" id="t8A-Hz-L38">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="label" size="13"/> <font key="font" metaFont="system"/>
</buttonCell> </buttonCell>
<connections> <connections>
<action selector="showAlbumDetail:" target="-2" id="A4Q-gb-B45"/> <action selector="showAlbumDetail:" target="-2" id="A4Q-gb-B45"/>
@ -54,7 +54,7 @@
<textField identifier="albumTitle" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KEh-NL-c2W"> <textField identifier="albumTitle" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KEh-NL-c2W">
<rect key="frame" x="8" y="28" width="142" height="17"/> <rect key="frame" x="8" y="28" width="142" height="17"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" id="pDs-0t-e1j"> <textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" id="pDs-0t-e1j">
<font key="font" metaFont="label" size="13"/> <font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
@ -62,7 +62,7 @@
<textField identifier="albumArtist" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5Uu-j1-qyT"> <textField identifier="albumArtist" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5Uu-j1-qyT">
<rect key="frame" x="8" y="10" width="142" height="16"/> <rect key="frame" x="8" y="10" width="142" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" id="yZn-e9-zyP"> <textFieldCell key="cell" lineBreakMode="truncatingTail" title="Label" id="yZn-e9-zyP">
<font key="font" metaFont="label" size="13"/> <font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
@ -98,6 +98,13 @@
<action selector="addAlbumToQueueMenuAction:" target="-2" id="6wW-oR-ykh"/> <action selector="addAlbumToQueueMenuAction:" target="-2" id="6wW-oR-ykh"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="g9v-9R-xDB"/>
<menuItem title="Refresh album art" id="4Ld-db-wka">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="refreshAlbumArtMenuAction:" target="-2" id="wyO-Dn-k9r"/>
</connections>
</menuItem>
</items> </items>
<point key="canvasLocation" x="191" y="38"/> <point key="canvasLocation" x="191" y="38"/>
</menu> </menu>

View File

@ -128,15 +128,18 @@ class AlbumDetailView: NSViewController {
self.dataSource.albumSongs[1].song self.dataSource.albumSongs[1].song
else { return } else { return }
self.getBigCoverArt(song: song, album: album) DispatchQueue.main.async {
self.getBigCoverArt(song: song, album: album)
}
} }
} }
func getBigCoverArt(song: Song, album: Album) { func getBigCoverArt(song: Song, album: Album) {
guard let imagePath = album.coverArtFilePath else { return } let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: album.hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
albumCoverView.kf.setImage( albumCoverView.kf.setImage(
with: .provider(provider), with: .provider(provider),
placeholder: NSImage.defaultCoverArt, placeholder: NSImage.defaultCoverArt,
@ -146,13 +149,19 @@ class AlbumDetailView: NSViewController {
] ]
) )
cacheSmallCover(provider: provider) cacheSmallCover(song: song, album: album)
} }
func cacheSmallCover(provider: ImageDataProvider) { func cacheSmallCover(song: Song, album: Album) {
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: album.hash
)
_ = KingfisherManager.shared.retrieveImage( _ = KingfisherManager.shared.retrieveImage(
with: .provider(provider), with: .provider(provider),
options: [ options: [
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .queueSongCoverSize)), .processor(DownsamplingImageProcessor(size: .queueSongCoverSize)),
.scaleFactor(2), .scaleFactor(2),
] ]

View File

@ -50,7 +50,8 @@ class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
type: .albumSongItem(song.mpdSong.uriString), type: .albumSongItem(song.mpdSong.uriString),
title: song.title, title: song.title,
artist: song.artist, artist: song.artist,
cover: song.album.coverArtFilePath album: song.album.title,
uri: song.mpdSong.uriString
), ),
ofType: .songPasteboardType ofType: .songPasteboardType
) )
@ -69,10 +70,11 @@ class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
) { draggingItem, index, stop in ) { draggingItem, index, stop in
guard let item = draggingItem.item as? NSPasteboardItem, guard let item = draggingItem.item as? NSPasteboardItem,
let draggedSong = item.draggedSong(forType: .songPasteboardType), let draggedSong = item.draggedSong(forType: .songPasteboardType),
case let (title?, artist?, cover?) = ( case let (title, artist, album, uri) = (
draggedSong.title, draggedSong.title,
draggedSong.artist, draggedSong.artist,
draggedSong.cover draggedSong.album,
draggedSong.uri
) )
else { return } else { return }
@ -81,7 +83,8 @@ class AlbumTracksDataSource: NSObject, NSTableViewDataSource {
let draggedSongView = DraggedSongView( let draggedSongView = DraggedSongView(
title: title, title: title,
artist: artist, artist: artist,
cover: cover album: album,
uri: uri
) )
component.contents = draggedSongView.view.image() component.contents = draggedSongView.view.image()

View File

@ -7,15 +7,12 @@
// //
import AppKit import AppKit
import Kingfisher
class CoverArtPrefsController: NSViewController { class CoverArtPrefsController: NSViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
if let mpdLibraryDir = App.store.state.preferencesState.mpdLibraryDir {
mpdLibraryDirField.stringValue = mpdLibraryDir
}
if App.store.state.preferencesState.fetchMissingArtworkFromInternet { if App.store.state.preferencesState.fetchMissingArtworkFromInternet {
fetchMissingArtworkFromInternet.state = .on fetchMissingArtworkFromInternet.state = .on
} else { } else {
@ -33,12 +30,6 @@ class CoverArtPrefsController: NSViewController {
self.parent?.view.window?.title = title self.parent?.view.window?.title = title
} }
@IBAction func updateMpdLibraryDir(_ sender: NSTextField) {
App.store.dispatch(UpdateMPDLibraryDir(mpdLibraryDir: sender.stringValue))
}
@IBOutlet var mpdLibraryDirField: NSTextField!
@IBAction func updateFetchMissingArtworkFromInternet(_ sender: NSButton) { @IBAction func updateFetchMissingArtworkFromInternet(_ sender: NSButton) {
App.store.dispatch( App.store.dispatch(
UpdateFetchMissingArtworkFromInternet( UpdateFetchMissingArtworkFromInternet(
@ -47,5 +38,10 @@ class CoverArtPrefsController: NSViewController {
) )
} }
@IBAction func clearAlbumArtCache(_ sender: NSButton) {
KingfisherManager.shared.cache.clearDiskCache()
KingfisherManager.shared.cache.clearMemoryCache()
}
@IBOutlet var fetchMissingArtworkFromInternet: NSButton! @IBOutlet var fetchMissingArtworkFromInternet: NSButton!
} }

View File

@ -17,13 +17,23 @@ class CurrentCoverArtView: NSImageView {
App.store.subscribe(self) { App.store.subscribe(self) {
$0.select { $0.playerState.currentSong } $0.select { $0.playerState.currentSong }
} }
NotificationCenter.default.addObserver(self, selector: #selector(didReloadAlbumArt), name: .didReloadAlbumArt, object: nil)
} }
func setAlbumImage(_ album: Album) { @objc func didReloadAlbumArt() {
guard let imagePath = album.coverArtFilePath else { return } guard let song = App.store.state.playerState.currentSong
else { return }
setSongImage(song)
}
func setSongImage(_ song: Song) {
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
kf.setImage( kf.setImage(
with: .provider(provider), with: .provider(provider),
placeholder: NSImage.defaultCoverArt, placeholder: NSImage.defaultCoverArt,
@ -44,6 +54,6 @@ extension CurrentCoverArtView: StoreSubscriber {
return return
} }
setAlbumImage(song.album) setSongImage(song)
} }
} }

View File

@ -44,7 +44,8 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
type: .queueItem(queueItem.queuePos), type: .queueItem(queueItem.queuePos),
title: queueItem.song.title, title: queueItem.song.title,
artist: queueItem.song.artist, artist: queueItem.song.artist,
cover: queueItem.song.album.coverArtFilePath album: queueItem.song.album.title,
uri: queueItem.song.mpdSong.uriString
), ),
ofType: .songPasteboardType ofType: .songPasteboardType
) )
@ -128,10 +129,11 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
guard let item = draggingItem.item as? NSPasteboardItem, guard let item = draggingItem.item as? NSPasteboardItem,
let data = item.data(forType: .songPasteboardType), let data = item.data(forType: .songPasteboardType),
let draggedSong = try? PropertyListDecoder().decode(DraggedSong.self, from: data), let draggedSong = try? PropertyListDecoder().decode(DraggedSong.self, from: data),
case let (title?, artist?, cover?) = ( case let (title, artist, album, uri) = (
draggedSong.title, draggedSong.title,
draggedSong.artist, draggedSong.artist,
draggedSong.cover draggedSong.album,
draggedSong.uri
) )
else { return } else { return }
@ -140,7 +142,8 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
let draggedSongView = DraggedSongView( let draggedSongView = DraggedSongView(
title: title, title: title,
artist: artist, artist: artist,
cover: cover album: album,
uri: uri
) )
let view = draggedSongView.view let view = draggedSongView.view

View File

@ -60,13 +60,14 @@ class QueueSongCoverView: NSTableCellView {
} }
func setSong(_ queueItem: QueueItem, queueIcon: NSImage?) { func setSong(_ queueItem: QueueItem, queueIcon: NSImage?) {
guard let imagePath = queueItem.song.album.coverArtFilePath let song = queueItem.song
else { return }
isPlaying = queueItem.isPlaying isPlaying = queueItem.isPlaying
let imageURL = URL(fileURLWithPath: imagePath) let provider = MPDAlbumArtImageDataProvider(
let provider = LocalFileImageDataProvider(fileURL: imageURL) songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
queueSongCover.kf.setImage( queueSongCover.kf.setImage(
with: .provider(provider), with: .provider(provider),

View File

@ -24,6 +24,7 @@ class QueueViewController: NSViewController {
NotificationCenter.default.addObserver(self, selector: #selector(didConnect), name: .didConnect, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didConnect), name: .didConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willDisconnect), name: .willDisconnect, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willDisconnect), name: .willDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didReloadAlbumArt), name: .didReloadAlbumArt, object: nil)
queueView.dataSource = dataSource queueView.dataSource = dataSource
queueView.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle queueView.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle
@ -62,6 +63,10 @@ class QueueViewController: NSViewController {
} }
} }
@objc func didReloadAlbumArt() {
queueView.reloadData()
}
@IBAction func playTrack(_ sender: Any) { @IBAction func playTrack(_ sender: Any) {
let queuePos = queueView.selectedRow let queuePos = queueView.selectedRow

View File

@ -16,12 +16,14 @@ class DraggedSongView: NSViewController {
private let songTitle: String private let songTitle: String
private let songArtist: String private let songArtist: String
private let songCover: String? private let songAlbum: String
private let songUri: String
init(title: String, artist: String, cover: String? = nil) { init(title: String, artist: String, album: String, uri: String) {
songTitle = title songTitle = title
songArtist = artist songArtist = artist
songCover = cover songAlbum = album
songUri = uri
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@ -58,10 +60,12 @@ class DraggedSongView: NSViewController {
} }
func setCoverArt() { func setCoverArt() {
guard let imagePath = songCover else { return } let mpdAlbum = MPDClient.MPDAlbum(title: songAlbum, artist: songArtist)
let imageURL = URL(fileURLWithPath: imagePath) let provider = MPDAlbumArtImageDataProvider(
let provider = LocalFileImageDataProvider(fileURL: imageURL) songUri: songUri,
cacheKey: Album(mpdAlbum: mpdAlbum).hash
)
coverImage.kf.setImage( coverImage.kf.setImage(
with: .provider(provider), with: .provider(provider),

View File

@ -15,34 +15,4 @@ extension NSImage {
static let queuePauseIcon = NSImage(named: "queuePauseButton") static let queuePauseIcon = NSImage(named: "queuePauseButton")
static let defaultCoverArt = NSImage(named: "defaultCoverArt") static let defaultCoverArt = NSImage(named: "defaultCoverArt")
func toFitBox(size: NSSize) -> NSImage {
var newSize: NSSize = NSSize.zero
let aspectRatio = self.size.width / self.size.height
let boxAspectRatio = size.width / size.height
if aspectRatio > boxAspectRatio {
newSize = NSSize(width: size.width, height: size.width / aspectRatio)
} else {
newSize = NSSize(width: size.height * aspectRatio, height: size.height)
}
let newImage = NSImage(size: newSize)
newImage.lockFocus()
self.draw(in: newImage.alignmentRect)
newImage.unlockFocus()
return newImage
}
func jpegData(compressionQuality: CGFloat) -> Data? {
guard let image = cgImage(forProposedRect: nil, context: nil, hints: nil)
else { return nil }
let bitmapImageRep = NSBitmapImageRep(cgImage: image)
return bitmapImageRep.representation(
using: .jpeg,
properties: [.compressionFactor: compressionQuality]
)
}
} }

View File

@ -11,4 +11,5 @@ import Foundation
extension Notification.Name { extension Notification.Name {
static let didConnect = Notification.Name("MPDClientDidConnect") static let didConnect = Notification.Name("MPDClientDidConnect")
static let willDisconnect = Notification.Name("MPDClientWillDisconnect") static let willDisconnect = Notification.Name("MPDClientWillDisconnect")
static let didReloadAlbumArt = Notification.Name("MPDDidReloadAlbumArt")
} }

View File

@ -0,0 +1,34 @@
//
// MPDAlbumArtImageDataProvider.swift
// Persephone
//
// Created by Daniel Barber on 2/1/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import Foundation
import Kingfisher
public struct MPDAlbumArtImageDataProvider: ImageDataProvider {
let songUri: String
init(songUri: String, cacheKey: String? = nil) {
self.songUri = songUri
self.cacheKey = cacheKey ?? songUri
}
public var cacheKey: String
public func data(handler: @escaping (Result<Data, Error>) -> Void) {
App.mpdClient.fetchAlbumArt(songUri: songUri, imageData: nil) { imageData in
guard let imageData = imageData
else { return }
handler(.success(imageData))
}
}
public var contentURL: String? {
return songUri
}
}

View File

@ -18,14 +18,16 @@ class UserNotificationsController {
} }
func notifyTrack(_ state: Song?) { func notifyTrack(_ state: Song?) {
guard let currentSong = state, guard let song = state,
let coverArtFilePath = currentSong.album.coverArtFilePath,
let status = App.mpdClient.status, let status = App.mpdClient.status,
status.state == .playing status.state == .playing
else { return } else { return }
let imageURL = URL(fileURLWithPath: coverArtFilePath) let provider = MPDAlbumArtImageDataProvider(
let provider = LocalFileImageDataProvider(fileURL: imageURL) songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
_ = KingfisherManager.shared.retrieveImage( _ = KingfisherManager.shared.retrieveImage(
with: .provider(provider), with: .provider(provider),
options: [ options: [
@ -35,7 +37,7 @@ class UserNotificationsController {
) { result in ) { result in
switch result { switch result {
case .success(let value): case .success(let value):
SongNotifierService(song: currentSong, image: value.image) SongNotifierService(song: song, image: value.image)
.deliver() .deliver()
case .failure: case .failure:
break break

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16085"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -429,30 +429,14 @@
<objects> <objects>
<viewController title="Cover Art" id="3C9-vU-zjZ" userLabel="Cover Art" customClass="CoverArtPrefsController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController"> <viewController title="Cover Art" id="3C9-vU-zjZ" userLabel="Cover Art" customClass="CoverArtPrefsController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="PyK-v2-kus"> <view key="view" id="PyK-v2-kus">
<rect key="frame" x="0.0" y="0.0" width="524" height="168"/> <rect key="frame" x="0.0" y="0.0" width="420" height="103"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zZn-Rm-e1f">
<rect key="frame" x="53" y="129" width="104" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Music Directory:" id="sPn-V6-CfK">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gDk-ca-eOa">
<rect key="frame" x="162" y="125" width="288" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="~/Music" drawsBackground="YES" id="7WZ-b7-GUs">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<action selector="updateMpdLibraryDir:" target="3C9-vU-zjZ" id="3Ta-fH-5Zh"/>
</connections>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pRL-MG-1Be"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pRL-MG-1Be">
<rect key="frame" x="160" y="94" width="264" height="18"/> <rect key="frame" x="82" y="67" width="264" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="260" id="PJN-iZ-3RV"/>
</constraints>
<buttonCell key="cell" type="check" title="Fetch missing artwork from MusicBrainz" bezelStyle="regularSquare" imagePosition="left" enabled="NO" inset="2" id="LpD-Ew-HMd"> <buttonCell key="cell" type="check" title="Fetch missing artwork from MusicBrainz" bezelStyle="regularSquare" imagePosition="left" enabled="NO" inset="2" id="LpD-Ew-HMd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -461,61 +445,31 @@
<action selector="updateFetchMissingArtworkFromInternet:" target="3C9-vU-zjZ" id="I7x-9V-xJr"/> <action selector="updateFetchMissingArtworkFromInternet:" target="3C9-vU-zjZ" id="I7x-9V-xJr"/>
</connections> </connections>
</button> </button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="z1g-nP-ksw"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mXh-kY-tMC">
<rect key="frame" x="160" y="63" width="264" height="18"/> <rect key="frame" x="78" y="21" width="185" height="32"/>
<constraints> <buttonCell key="cell" type="push" title="Clear album art cache..." bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="l81-SG-7mf">
<constraint firstAttribute="width" constant="260" id="gK0-aW-CJy"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
</constraints>
<buttonCell key="cell" type="check" title="Save fetched artwork to music directory" bezelStyle="regularSquare" imagePosition="left" enabled="NO" inset="2" id="ZeZ-O4-vjS">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
</buttonCell> </buttonCell>
<connections>
<action selector="clearAlbumArtCache:" target="3C9-vU-zjZ" id="tXg-rz-lvh"/>
</connections>
</button> </button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xgS-Kg-8KR">
<rect key="frame" x="162" y="26" width="144" height="21"/>
<constraints>
<constraint firstAttribute="width" constant="144" id="DSX-th-Wn1"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" enabled="NO" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="folder.jpg" drawsBackground="YES" id="nKF-YI-xBL">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SmH-w6-5QI">
<rect key="frame" x="37" y="30" width="119" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" enabled="NO" alignment="right" title="Cover art filename:" id="b4u-u7-iWD">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="pRL-MG-1Be" firstAttribute="centerX" secondItem="z1g-nP-ksw" secondAttribute="centerX" id="0Ev-Ia-XBO"/> <constraint firstItem="pRL-MG-1Be" firstAttribute="leading" secondItem="PyK-v2-kus" secondAttribute="leading" constant="84" id="81s-YL-o8A"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="leading" secondItem="pRL-MG-1Be" secondAttribute="leading" id="1jd-wZ-a4Q"/> <constraint firstAttribute="bottom" secondItem="pRL-MG-1Be" secondAttribute="bottom" constant="69" id="L7b-jE-keG"/>
<constraint firstItem="pRL-MG-1Be" firstAttribute="leading" secondItem="z1g-nP-ksw" secondAttribute="leading" id="3tZ-Ub-RaT"/> <constraint firstItem="mXh-kY-tMC" firstAttribute="leading" secondItem="pRL-MG-1Be" secondAttribute="leading" id="b9X-hO-aYJ"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="leading" secondItem="zZn-Rm-e1f" secondAttribute="trailing" constant="7" id="C0m-yx-gXh"/> <constraint firstAttribute="bottom" secondItem="mXh-kY-tMC" secondAttribute="bottom" constant="28" id="ras-nE-Oq8"/>
<constraint firstItem="z1g-nP-ksw" firstAttribute="leading" secondItem="xgS-Kg-8KR" secondAttribute="leading" id="Dkv-ai-X2G"/>
<constraint firstItem="xgS-Kg-8KR" firstAttribute="leading" secondItem="SmH-w6-5QI" secondAttribute="trailing" constant="8" symbolic="YES" id="KQk-nr-H8y"/>
<constraint firstItem="zZn-Rm-e1f" firstAttribute="leading" secondItem="PyK-v2-kus" secondAttribute="leading" constant="55" id="OzK-MR-zuB"/>
<constraint firstAttribute="bottom" secondItem="SmH-w6-5QI" secondAttribute="bottom" constant="30" id="aBY-Ny-jPe"/>
<constraint firstItem="pRL-MG-1Be" firstAttribute="top" secondItem="gDk-ca-eOa" secondAttribute="bottom" constant="15" id="dKy-uC-r43"/>
<constraint firstItem="xgS-Kg-8KR" firstAttribute="top" secondItem="z1g-nP-ksw" secondAttribute="bottom" constant="18" id="lfR-Im-bd4"/>
<constraint firstAttribute="trailing" secondItem="gDk-ca-eOa" secondAttribute="trailing" constant="74" id="n8X-T2-tXA"/>
<constraint firstAttribute="bottom" secondItem="xgS-Kg-8KR" secondAttribute="bottom" constant="26" id="oXZ-qo-HwX"/>
<constraint firstItem="SmH-w6-5QI" firstAttribute="top" secondItem="zZn-Rm-e1f" secondAttribute="bottom" constant="83" id="qhC-mD-Bvw"/>
<constraint firstItem="z1g-nP-ksw" firstAttribute="top" secondItem="pRL-MG-1Be" secondAttribute="bottom" constant="17" id="sTP-hk-zfU"/>
</constraints> </constraints>
</view> </view>
<connections> <connections>
<outlet property="fetchMissingArtworkFromInternet" destination="pRL-MG-1Be" id="Xcp-sb-iZm"/> <outlet property="fetchMissingArtworkFromInternet" destination="pRL-MG-1Be" id="Xcp-sb-iZm"/>
<outlet property="mpdLibraryDirField" destination="gDk-ca-eOa" id="myi-BQ-0NS"/>
</connections> </connections>
</viewController> </viewController>
<customObject id="KzD-E3-lpA" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> <customObject id="KzD-E3-lpA" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1459" y="-242"/> <point key="canvasLocation" x="1407" y="-274.5"/>
</scene> </scene>
<!--View Controller--> <!--View Controller-->
<scene sceneID="VvW-vT-alQ"> <scene sceneID="VvW-vT-alQ">
@ -647,18 +601,18 @@
<objects> <objects>
<viewController id="KIP-rq-4dM" customClass="QueueViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="KIP-rq-4dM" customClass="QueueViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<splitView key="view" misplaced="YES" dividerStyle="thin" id="84I-w3-Mxl"> <splitView key="view" misplaced="YES" dividerStyle="thin" id="84I-w3-Mxl">
<rect key="frame" x="0.0" y="0.0" width="329" height="543"/> <rect key="frame" x="0.0" y="0.0" width="329" height="498"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews> <subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="42" horizontalPageScroll="10" verticalLineScroll="42" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="S3o-nF-NN7"> <scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="42" horizontalPageScroll="10" verticalLineScroll="42" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="S3o-nF-NN7">
<rect key="frame" x="0.0" y="0.0" width="329" height="217"/> <rect key="frame" x="0.0" y="0.0" width="329" height="198"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<clipView key="contentView" drawsBackground="NO" id="WI8-Pw-03L"> <clipView key="contentView" drawsBackground="NO" id="WI8-Pw-03L">
<rect key="frame" x="0.0" y="0.0" width="329" height="217"/> <rect key="frame" x="0.0" y="0.0" width="329" height="198"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" multipleSelection="NO" autosaveColumns="NO" rowHeight="42" rowSizeStyle="automatic" viewBased="YES" indentationMarkerFollowsCell="NO" outlineTableColumn="0Co-uF-CCB" id="jEJ-jg-fll"> <outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" multipleSelection="NO" autosaveColumns="NO" rowHeight="42" rowSizeStyle="automatic" viewBased="YES" indentationMarkerFollowsCell="NO" outlineTableColumn="0Co-uF-CCB" id="jEJ-jg-fll">
<rect key="frame" x="0.0" y="0.0" width="329" height="217"/> <rect key="frame" x="0.0" y="0.0" width="329" height="198"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="0.0"/> <size key="intercellSpacing" width="3" height="0.0"/>
<color key="backgroundColor" name="_sourceListBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="_sourceListBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -666,7 +620,6 @@
<tableColumns> <tableColumns>
<tableColumn identifier="songCoverColumn" width="43" minWidth="42" maxWidth="1000" id="0Co-uF-CCB" userLabel="Position"> <tableColumn identifier="songCoverColumn" width="43" minWidth="42" maxWidth="1000" id="0Co-uF-CCB" userLabel="Position">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell> </tableHeaderCell>
@ -712,7 +665,6 @@
</tableColumn> </tableColumn>
<tableColumn identifier="songInfoColumn" width="213" minWidth="10" maxWidth="3.4028234663852886e+38" id="HP0-ty-PFY" userLabel="Song Info"> <tableColumn identifier="songInfoColumn" width="213" minWidth="10" maxWidth="3.4028234663852886e+38" id="HP0-ty-PFY" userLabel="Song Info">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell> </tableHeaderCell>
@ -761,7 +713,6 @@
</tableColumn> </tableColumn>
<tableColumn identifier="songDurationColumn" width="64" minWidth="64" maxWidth="64" id="8O6-ox-kx2" userLabel="Duration"> <tableColumn identifier="songDurationColumn" width="64" minWidth="64" maxWidth="64" id="8O6-ox-kx2" userLabel="Duration">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell> </tableHeaderCell>
@ -824,7 +775,7 @@
</scroller> </scroller>
</scrollView> </scrollView>
<customView misplaced="YES" id="iUb-eV-Qws"> <customView misplaced="YES" id="iUb-eV-Qws">
<rect key="frame" x="0.0" y="218" width="329" height="325"/> <rect key="frame" x="0.0" y="199" width="329" height="299"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews> <subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Dw3-M5-tWY" customClass="CurrentCoverArtView" customModule="Persephone" customModuleProvider="target"> <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Dw3-M5-tWY" customClass="CurrentCoverArtView" customModule="Persephone" customModuleProvider="target">

View File

@ -74,8 +74,8 @@ extension MPDClient {
let mpdAlbum = MPDAlbum( let mpdAlbum = MPDAlbum(
title: mpdSong.album.title, title: mpdSong.album.title,
artist: mpdSong.artist, artist: mpdSong.artist,
date: mpdSong.date, firstSong: mpdSong,
path: mpdSong.path date: mpdSong.date
) )
if (mpdAlbum != albums.last) { if (mpdAlbum != albums.last) {
albums.append(mpdAlbum) albums.append(mpdAlbum)

View File

@ -119,6 +119,22 @@ extension MPDClient {
else { return } else { return }
albumSongs(for: album, callback: callback) albumSongs(for: album, callback: callback)
// Song commands
case .fetchAlbumArt:
guard let songUri = userData["songUri"] as? String,
let offset = userData["offset"] as? Int32,
let callback = userData["callback"] as? (Data?) -> Void
else { return }
var imageData = userData["imageData"] as? Data? ?? nil
sendFetchAlbumArt(
forUri: songUri,
imageData: imageData,
offset: offset,
callback: callback
)
} }
} }
@ -126,6 +142,7 @@ extension MPDClient {
func enqueueCommand( func enqueueCommand(
command: MPDCommand, command: MPDCommand,
priority: BlockOperation.QueuePriority = .normal, priority: BlockOperation.QueuePriority = .normal,
forceIdle: Bool = false,
userData: Dictionary<String, Any> = [:] userData: Dictionary<String, Any> = [:]
) { ) {
guard isConnected else { return } guard isConnected else { return }
@ -135,7 +152,7 @@ extension MPDClient {
let commandOperation = BlockOperation() { [unowned self] in let commandOperation = BlockOperation() { [unowned self] in
self.sendCommand(command: command, userData: userData) self.sendCommand(command: command, userData: userData)
self.idle() self.idle(forceIdle)
} }
commandOperation.queuePriority = priority commandOperation.queuePriority = priority
commandQueue.addOperation(commandOperation) commandQueue.addOperation(commandOperation)

View File

@ -12,15 +12,23 @@ import mpdclient
extension MPDClient { extension MPDClient {
func noIdle() { func noIdle() {
if isIdle { if isIdle {
mpd_send_noidle(connection) do {
isIdle = false idleLock.lock()
defer { idleLock.unlock() }
mpd_send_noidle(connection)
isIdle = false
}
} }
} }
func idle() { func idle(_ force: Bool = false) {
if !self.isIdle && self.commandQueue.operationCount == 1 { if (!self.isIdle && self.commandQueue.operationCount == 1) || force {
mpd_send_idle(self.connection) do {
self.isIdle = true idleLock.lock()
defer { idleLock.unlock() }
mpd_send_idle(self.connection)
self.isIdle = true
}
let result = mpd_recv_idle(self.connection, true) let result = mpd_recv_idle(self.connection, true)
self.handleIdleResult(result) self.handleIdleResult(result)
@ -28,7 +36,11 @@ extension MPDClient {
} }
func handleIdleResult(_ result: mpd_idle) { func handleIdleResult(_ result: mpd_idle) {
isIdle = false do {
idleLock.lock()
defer { idleLock.unlock() }
isIdle = false
}
let mpdIdle = MPDIdle(rawValue: result.rawValue) let mpdIdle = MPDIdle(rawValue: result.rawValue)

View File

@ -10,6 +10,23 @@ import Foundation
import mpdclient import mpdclient
extension MPDClient { extension MPDClient {
func fetchAlbumArt(
songUri: String,
imageData: Data?,
offset: Int32 = 0,
callback: @escaping (Data?) -> Void
) {
enqueueCommand(
command: .fetchAlbumArt,
userData: [
"songUri": songUri,
"callback": callback,
"imageData": imageData as Any,
"offset": offset,
]
)
}
func searchSongs(_ terms: [MPDClient.MPDTag: String]) -> [MPDSong] { func searchSongs(_ terms: [MPDClient.MPDTag: String]) -> [MPDSong] {
var songs: [MPDSong] = [] var songs: [MPDSong] = []
@ -25,4 +42,51 @@ extension MPDClient {
return songs return songs
} }
func sendFetchAlbumArt(
forUri songUri: String,
imageData: Data?,
offset: Int32,
callback: @escaping (Data?) -> Void
) -> Void {
var size: Int?
mpd_send_albumart(self.connection, songUri, String(offset))
guard let sizePair = mpd_recv_pair(self.connection) else {
mpd_connection_clear_error(self.connection)
return
}
size = Int(MPDPair(sizePair).value)
mpd_return_pair(self.connection, sizePair)
var data = imageData ?? Data(count: size!)
let binaryPair = MPDPair(mpd_recv_pair(self.connection))
let chunkSize = Int(binaryPair.value)!
mpd_return_pair(self.connection, binaryPair.pair)
_ = data[offset...].withUnsafeMutableBytes { (pointer) in
mpd_recv_binary(self.connection, pointer, chunkSize)
}
guard mpd_response_finish(self.connection) else { return }
let newOffset = offset + Int32(chunkSize)
if newOffset < size! {
DispatchQueue.main.async {
self.fetchAlbumArt(
songUri: songUri,
imageData: data,
offset: newOffset,
callback: callback
)
}
} else {
DispatchQueue.main.async {
callback(data)
}
}
}
} }

View File

@ -11,24 +11,26 @@ import mpdclient
extension MPDClient { extension MPDClient {
func playPause() { func playPause() {
enqueueCommand(command: .playPause) enqueueCommand(command: .playPause, priority: .veryHigh, forceIdle: true)
} }
func stop() { func stop() {
enqueueCommand(command: .stop) enqueueCommand(command: .stop, priority: .veryHigh, forceIdle: true)
} }
func prevTrack() { func prevTrack() {
enqueueCommand(command: .prevTrack) enqueueCommand(command: .prevTrack, priority: .veryHigh, forceIdle: true)
} }
func nextTrack() { func nextTrack() {
enqueueCommand(command: .nextTrack) enqueueCommand(command: .nextTrack, priority: .veryHigh, forceIdle: true)
} }
func seekCurrentSong(timeInSeconds: Float) { func seekCurrentSong(timeInSeconds: Float) {
enqueueCommand( enqueueCommand(
command: .seekCurrentSong, command: .seekCurrentSong,
priority: .veryHigh,
forceIdle: true,
userData: ["timeInSeconds": timeInSeconds] userData: ["timeInSeconds": timeInSeconds]
) )
} }
@ -36,6 +38,8 @@ extension MPDClient {
func setShuffleState(shuffleState: Bool) { func setShuffleState(shuffleState: Bool) {
enqueueCommand( enqueueCommand(
command: .setShuffleState, command: .setShuffleState,
priority: .veryHigh,
forceIdle: true,
userData: ["shuffleState": shuffleState] userData: ["shuffleState": shuffleState]
) )
} }
@ -43,6 +47,8 @@ extension MPDClient {
func setRepeatState(repeatState: Bool) { func setRepeatState(repeatState: Bool) {
enqueueCommand( enqueueCommand(
command: .setRepeatState, command: .setRepeatState,
priority: .veryHigh,
forceIdle: true,
userData: ["repeatState": repeatState] userData: ["repeatState": repeatState]
) )
} }

View File

@ -0,0 +1,15 @@
//
// MPDClientWrapper.c
// Persephone
//
// Created by Daniel Barber on 1/31/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
#include "MPDClientWrapper.h"
int
mpd_send_albumart(struct mpd_connection *connection, const char * uri, const char * offset)
{
return mpd_send_command(connection, "albumart", uri, offset, NULL);
}

View File

@ -0,0 +1,12 @@
//
// MPDClientWrapper.h
// Persephone
//
// Created by Daniel Barber on 1/31/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
#include <mpd/client.h>
int
mpd_send_albumart(struct mpd_connection *connection, const char * uri, const char * offset);

View File

@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "MPDClientWrapper.h"

View File

@ -22,6 +22,8 @@ class MPDClient {
let commandQueue = OperationQueue() let commandQueue = OperationQueue()
let idleLock = NSLock()
init(withDelegate delegate: MPDClientDelegate?) { init(withDelegate delegate: MPDClientDelegate?) {
commandQueue.maxConcurrentOperationCount = 1 commandQueue.maxConcurrentOperationCount = 1
self.delegate = delegate self.delegate = delegate

View File

@ -12,8 +12,8 @@ extension MPDClient {
struct MPDAlbum: Equatable { struct MPDAlbum: Equatable {
let title: String let title: String
let artist: String let artist: String
var firstSong: MPDSong?
var date: String? var date: String?
var path: String?
static func == (lhs: MPDAlbum, rhs: MPDAlbum) -> Bool { static func == (lhs: MPDAlbum, rhs: MPDAlbum) -> Bool {
return lhs.title == rhs.title && return lhs.title == rhs.title &&

View File

@ -48,5 +48,8 @@ extension MPDClient {
case playAlbum case playAlbum
case getAlbumFirstSong case getAlbumFirstSong
case getAlbumSongs case getAlbumSongs
// Song commands
case fetchAlbumArt
} }
} }

View File

@ -37,8 +37,7 @@ extension MPDClient {
return MPDAlbum( return MPDAlbum(
title: getTag(.album), title: getTag(.album),
artist: artist, artist: artist,
date: date, date: date
path: path
) )
} }
@ -54,11 +53,6 @@ extension MPDClient {
return getTag(.date) return getTag(.date)
} }
var path: String {
return NSString(string: uriString)
.deletingLastPathComponent
}
func getTag(_ tagType: MPDTag) -> String { func getTag(_ tagType: MPDTag) -> String {
guard let tag = mpd_song_get_tag(song, tagType.mpdTag(), 0) guard let tag = mpd_song_get_tag(song, tagType.mpdTag(), 0)
else { return "" } else { return "" }

View File

@ -32,29 +32,6 @@ struct Album {
var hash: String { var hash: String {
return "\(title) - \(artist)".sha1() return "\(title) - \(artist)".sha1()
} }
var coverArtFilenames: [String] {
return [
"cover.jpg",
"folder.jpg",
"\(artist) - \(title).jpg",
"cover.png",
"folder.png",
"\(artist) - \(title ).png",
]
}
var coverArtFilePath: String? {
let musicDir = App.store.state.preferencesState.expandedMpdLibraryDir
guard let albumPath = mpdAlbum.path else { return nil }
return coverArtFilenames
.lazy
.map { "\(musicDir)/\(albumPath)/\($0)" }
.first {
FileManager.default.fileExists(atPath: $0)
}
}
} }
extension Album: Equatable { extension Album: Equatable {

View File

@ -8,7 +8,8 @@
struct DraggedSong: Codable { struct DraggedSong: Codable {
var type: DraggedSongType var type: DraggedSongType
var title: String? var title: String
var artist: String? var artist: String
var cover: String? var album: String
var uri: String
} }

View File

@ -1,23 +0,0 @@
//
// CoverArtQueue.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/26.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
class CoverArtQueue {
static let shared = CoverArtQueue()
let queue = DispatchQueue(label: "CoverArtQueue")
var lastDispatchedTime = DispatchTime(uptimeNanoseconds: 0) - 1
func addToQueue(workItem: DispatchWorkItem) {
let dispatchTime = max(lastDispatchedTime + 1, DispatchTime(uptimeNanoseconds: 0))
lastDispatchedTime = dispatchTime
queue.asyncAfter(deadline: dispatchTime, execute: workItem)
}
}

View File

@ -0,0 +1,56 @@
//
// CoverArtService.swift
// Persephone
//
// Created by Daniel Barber on 2/3/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import Foundation
import Kingfisher
struct CoverArtService {
let song: Song
func refresh(callback: @escaping () -> Void?) {
let provider = MPDAlbumArtImageDataProvider(
songUri: song.mpdSong.uriString,
cacheKey: song.album.hash
)
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.forceRefresh,
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .albumListCoverSize)),
.scaleFactor(2),
]
) { _ in
callback()
}
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.forceRefresh,
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .currentlyPlayingCoverSize)),
.scaleFactor(2),
.callbackQueue(.mainAsync)
]
) { _ in }
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.forceRefresh,
.memoryCacheExpiration(.never),
.processor(DownsamplingImageProcessor(size: .queueSongCoverSize)),
.scaleFactor(2),
]
) { _ in
NotificationCenter.default.post(name: .didReloadAlbumArt, object: nil)
}
}
}

View File

@ -16,10 +16,6 @@ struct UpdateServerPort: Action {
var port: Int? var port: Int?
} }
struct UpdateMPDLibraryDir: Action {
var mpdLibraryDir: String?
}
struct UpdateFetchMissingArtworkFromInternet: Action { struct UpdateFetchMissingArtworkFromInternet: Action {
var fetchMissingArtworkFromInternet: Bool var fetchMissingArtworkFromInternet: Bool
} }

View File

@ -13,17 +13,6 @@ struct PreferencesState: StateType, Equatable {
let preferences = UserDefaults.standard let preferences = UserDefaults.standard
var mpdServer: MPDServer var mpdServer: MPDServer
let mpdLibraryDirDefault = "~/Music"
var mpdLibraryDir: String?
var mpdLibraryDirOrDefault: String {
return mpdLibraryDir ?? mpdLibraryDirDefault
}
var expandedMpdLibraryDir: String {
return NSString(string: mpdLibraryDirOrDefault).expandingTildeInPath
}
var fetchMissingArtworkFromInternet: Bool var fetchMissingArtworkFromInternet: Bool
@ -32,7 +21,6 @@ struct PreferencesState: StateType, Equatable {
host: preferences.string(forKey: "mpdHost"), host: preferences.string(forKey: "mpdHost"),
port: preferences.value(forKey: "mpdPort") as? Int port: preferences.value(forKey: "mpdPort") as? Int
) )
self.mpdLibraryDir = preferences.string(forKey: "mpdLibraryDir")
self.fetchMissingArtworkFromInternet = preferences.bool( self.fetchMissingArtworkFromInternet = preferences.bool(
forKey: "fetchMissingArtworkFromInternet" forKey: "fetchMissingArtworkFromInternet"
) )
@ -45,7 +33,6 @@ struct PreferencesState: StateType, Equatable {
} else { } else {
preferences.removeObject(forKey: "mpdPort") preferences.removeObject(forKey: "mpdPort")
} }
preferences.set(mpdLibraryDir, forKey: "mpdLibraryDir")
preferences.set(fetchMissingArtworkFromInternet, forKey: "fetchMissingArtworkFromInternet") preferences.set(fetchMissingArtworkFromInternet, forKey: "fetchMissingArtworkFromInternet")
} }
} }

View File

@ -20,9 +20,6 @@ func preferencesReducer(action: Action, state: PreferencesState?) -> Preferences
case let action as UpdateServerPort: case let action as UpdateServerPort:
state.mpdServer.port = action.port state.mpdServer.port = action.port
case let action as UpdateMPDLibraryDir:
state.mpdLibraryDir = action.mpdLibraryDir
case is SavePreferences: case is SavePreferences:
state.save() state.save()