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

Compare commits

..

31 Commits

Author SHA1 Message Date
6458b7402b
Adjust version number 2020-01-20 13:38:08 -05:00
ae075914b3
Use NSString helper here 2020-01-20 13:36:08 -05:00
27425b76d6
No need for parentheses here 2020-01-20 13:36:06 -05:00
2932ab9e33
Eliminate some single line functions 2020-01-20 13:36:04 -05:00
f878daad2c
Create constants for sizes 2020-01-20 13:36:03 -05:00
555a99d5be
We don't need self here 2020-01-20 13:36:03 -05:00
b8698d2830
✂️ stray space
Co-Authored-By: louis-antonopoulos <louis@thoughtbot.com>
2020-01-20 13:36:02 -05:00
8f3e93db66
✂️ 2020-01-20 13:36:02 -05:00
3405df578b
Something something storyboard editor 2020-01-20 13:36:02 -05:00
0e996cbb4c
We can simplify this 2020-01-20 13:36:01 -05:00
26646ea88e
We don't need this at the moment 2020-01-20 13:36:01 -05:00
7b1728b521
Clear search field on disconnect 2020-01-20 13:36:00 -05:00
de5d3e94fb
Fix connection bug
TODO: We really should do something better about informing the user of
connection errors and the like.
2020-01-20 13:36:00 -05:00
b581f297c9
Refactor connection logic
The app would crash when connection settings were changed. This
refactors the connection logic to be consistent with the rest of the
mpdclient command structure. This ultimately fixes the bug.
2020-01-20 13:35:59 -05:00
d075a06c41
Fix queuepos out of bounds error
Sometimes the status gets updated before the queue has been retrieved.
In these cases we want to skip setting the current song until it has
been.
2020-01-20 13:35:50 -05:00
9f948df141
Use the search query UI state 2020-01-20 13:35:49 -05:00
04d4c77f8d
Update version to prealpha 2020-01-20 13:35:49 -05:00
1dae55c2b8
Clear album are when a cell gets reused
This prevents the wrong album art showing up for albums that don't have
any album are.
2020-01-20 13:35:48 -05:00
1d5d87c75b
Responding to repeated space can get us into trouble 2020-01-20 13:35:48 -05:00
91fa2f62dc
We're not using these any more 2019-12-06 17:52:58 -05:00
80597cdd56
Disable Musicbrainz artwork pref 2019-12-06 17:51:06 -05:00
e2149a2f3e
Add PNG's and change the search order for album art 2019-12-06 17:51:06 -05:00
c201ebaab6
Fix bug where playing a song or album crashes the app
Playing an album would only crash the app if the queue was empty before
trying to play.
2019-12-06 17:51:06 -05:00
bed09eb888
Fix bug with getting tracks for compilations 2019-12-06 17:51:05 -05:00
b712a8d00d
Use Kingfisher to fetch the notification album art 2019-12-06 17:51:05 -05:00
4322a25b8b
Get currently playing art using Kingfisher 2019-12-06 17:51:05 -05:00
d407f1e5f9
Get album browser artwork using Kingfisher 2019-12-06 17:51:05 -05:00
01428d8126
WIP 2019-12-06 17:51:05 -05:00
42d274058f
WIP: Get album metadata when fetching albums 2019-12-06 17:51:04 -05:00
4af3e7aead
WIP: Search 2019-12-06 17:51:04 -05:00
87d4f33b09
WIP: Search 2019-12-06 17:51:04 -05:00
37 changed files with 334 additions and 529 deletions

View File

@ -29,9 +29,6 @@
E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5306223C019100173814 /* MPDClient+Status.swift */; };
E41E5309223C020400173814 /* MPDClient+Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5308223C020400173814 /* MPDClient+Command.swift */; };
E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530A223C033700173814 /* MPDClient+Album.swift */; };
E41E530E223EF4CF00173814 /* CoverArtService+Caching.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530D223EF4CF00173814 /* CoverArtService+Caching.swift */; };
E41E5310223EF6CE00173814 /* CoverArtService+Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530F223EF6CE00173814 /* CoverArtService+Remote.swift */; };
E41E5312223EF74A00173814 /* CoverArtService+Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5311223EF74A00173814 /* CoverArtService+Filesystem.swift */; };
E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */; };
E4235640228623D2001216D6 /* QueueSongTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E423563F228623D2001216D6 /* QueueSongTitleView.swift */; };
E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42410B52241B956005ED6DF /* MPDClient+Database.swift */; };
@ -49,6 +46,7 @@
E43B67AA22909793007DCF55 /* AlbumDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43B67A822909793007DCF55 /* AlbumDetailView.swift */; };
E43B67AB22909793007DCF55 /* AlbumDetailView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E43B67A922909793007DCF55 /* AlbumDetailView.xib */; };
E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43B67AC229194CD007DCF55 /* AlbumTracksDataSource.swift */; };
E43BECA0238835DC00CAF1EB /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = E43BEC9F238835DC00CAF1EB /* Kingfisher */; };
E4405192227644340090CD6F /* MPDServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4405191227644340090CD6F /* MPDServerController.swift */; };
E44051942278765A0090CD6F /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44051932278765A0090CD6F /* App.swift */; };
E440519C227BAF2E0090CD6F /* UIActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E440519B227BAF2E0090CD6F /* UIActions.swift */; };
@ -74,7 +72,6 @@
E489E39D22B9CF0000CA8CBD /* NSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E489E39C22B9CF0000CA8CBD /* NSView.swift */; };
E489E3A422B9D31800CA8CBD /* DraggedSongView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E489E3A222B9D31800CA8CBD /* DraggedSongView.swift */; };
E489E3A522B9D31800CA8CBD /* DraggedSongView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E489E3A322B9D31800CA8CBD /* DraggedSongView.xib */; };
E48E92D7235113DF00A5E1BB /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48E92D6235113DF00A5E1BB /* Metadata.swift */; };
E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4928E0A2218D62A001D4BEA /* CGColor.swift */; };
E49A5482233E580800EED353 /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = E49A5481233E580800EED353 /* PromiseKit */; };
E49A5485233E5ADC00EED353 /* Differ in Frameworks */ = {isa = PBXBuildFile; productRef = E49A5484233E5ADC00EED353 /* Differ */; };
@ -85,7 +82,6 @@
E4A642DA22090CBE00067D21 /* MPDStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* MPDStatus.swift */; };
E4A83BEF2221F8CF0098FED6 /* CoverArtPrefsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BEE2221F8CF0098FED6 /* CoverArtPrefsController.swift */; };
E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */; };
E4A83BF4222207D50098FED6 /* CoverArtService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF3222207D50098FED6 /* CoverArtService.swift */; };
E4B11B53226928F20075461B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B52226928F20075461B /* AppState.swift */; };
E4B11B61226A4C000075461B /* PlayerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B60226A4BFF0075461B /* PlayerReducer.swift */; };
E4B11B63226A4C510075461B /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11B62226A4C510075461B /* AppReducer.swift */; };
@ -108,6 +104,7 @@
E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.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 */; };
E4DA820623D6236200C1EE58 /* NSSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DA820523D6236200C1EE58 /* NSSize.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 */; };
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; };
@ -234,9 +231,6 @@
E41E5306223C019100173814 /* MPDClient+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Status.swift"; sourceTree = "<group>"; };
E41E5308223C020400173814 /* MPDClient+Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Command.swift"; sourceTree = "<group>"; };
E41E530A223C033700173814 /* MPDClient+Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Album.swift"; sourceTree = "<group>"; };
E41E530D223EF4CF00173814 /* CoverArtService+Caching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoverArtService+Caching.swift"; sourceTree = "<group>"; };
E41E530F223EF6CE00173814 /* CoverArtService+Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoverArtService+Remote.swift"; sourceTree = "<group>"; };
E41E5311223EF74A00173814 /* CoverArtService+Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoverArtService+Filesystem.swift"; sourceTree = "<group>"; };
E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPrefsViewController.swift; sourceTree = "<group>"; };
E423563F228623D2001216D6 /* QueueSongTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueSongTitleView.swift; sourceTree = "<group>"; };
E42410B52241B956005ED6DF /* MPDClient+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Database.swift"; sourceTree = "<group>"; };
@ -279,13 +273,11 @@
E489E39C22B9CF0000CA8CBD /* NSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSView.swift; sourceTree = "<group>"; };
E489E3A222B9D31800CA8CBD /* DraggedSongView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggedSongView.swift; sourceTree = "<group>"; };
E489E3A322B9D31800CA8CBD /* DraggedSongView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraggedSongView.xib; sourceTree = "<group>"; };
E48E92D6235113DF00A5E1BB /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
E4928E0A2218D62A001D4BEA /* CGColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGColor.swift; sourceTree = "<group>"; };
E4A3A6A022A457B600EA2C40 /* AlbumDetailSongListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailSongListView.swift; sourceTree = "<group>"; };
E4A642D922090CBE00067D21 /* MPDStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDStatus.swift; sourceTree = "<group>"; };
E4A83BEE2221F8CF0098FED6 /* CoverArtPrefsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtPrefsController.swift; sourceTree = "<group>"; };
E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; };
E4A83BF3222207D50098FED6 /* CoverArtService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverArtService.swift; sourceTree = "<group>"; };
E4B11B52226928F20075461B /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
E4B11B60226A4BFF0075461B /* PlayerReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerReducer.swift; sourceTree = "<group>"; };
E4B11B62226A4C510075461B /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = "<group>"; };
@ -306,6 +298,7 @@
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>"; };
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>"; };
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>"; };
E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
@ -332,6 +325,7 @@
E4B11BA72274E4500075461B /* libmpdclient.2.dylib in Frameworks */,
E49A5485233E5ADC00EED353 /* Differ in Frameworks */,
E4677C48233E60E70041474F /* MediaKeyTap in Frameworks */,
E43BECA0238835DC00CAF1EB /* Kingfisher in Frameworks */,
E49A548E233E5B6000EED353 /* SwiftyJSON in Frameworks */,
E4E96D13233E630800AFD36F /* PMKFoundation in Frameworks */,
E49A5482233E580800EED353 /* PromiseKit in Frameworks */,
@ -432,6 +426,7 @@
E489E39822B85D0400CA8CBD /* NSPasteboard.swift */,
E489E39C22B9CF0000CA8CBD /* NSView.swift */,
E43AC1F022C68E6A001E483C /* NSPasteboardItem.swift */,
E4DA820523D6236200C1EE58 /* NSSize.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -448,9 +443,9 @@
E41E5302223BF9C300173814 /* MPDClient+Idle.swift */,
E41E5300223BF99300173814 /* MPDClient+Queue.swift */,
E42A4D5022E2167E001C6CAD /* MPDClient+Songs.swift */,
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
E41E5306223C019100173814 /* MPDClient+Status.swift */,
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */,
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -514,16 +509,6 @@
path = mpd;
sourceTree = "<group>";
};
E41E530C223EF4BA00173814 /* Extensions */ = {
isa = PBXGroup;
children = (
E41E530D223EF4CF00173814 /* CoverArtService+Caching.swift */,
E41E5311223EF74A00173814 /* CoverArtService+Filesystem.swift */,
E41E530F223EF6CE00173814 /* CoverArtService+Remote.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
E442CCC42347D5B900004E0C /* Components */ = {
isa = PBXGroup;
children = (
@ -642,8 +627,6 @@
E4A83BF2222207BE0098FED6 /* Services */ = {
isa = PBXGroup;
children = (
E41E530C223EF4BA00173814 /* Extensions */,
E4A83BF3222207D50098FED6 /* CoverArtService.swift */,
E439109722640213002982E9 /* SongNotifierService.swift */,
);
path = Services;
@ -736,7 +719,6 @@
E419E2862249B96600216A8C /* Song.swift */,
E47E2FDC2220A6D100F747E6 /* Time.swift */,
E4B11B72226A6C770075461B /* TrackTimer.swift */,
E48E92D6235113DF00A5E1BB /* Metadata.swift */,
);
path = Models;
sourceTree = "<group>";
@ -768,6 +750,7 @@
E49A548D233E5B6000EED353 /* SwiftyJSON */,
E4677C47233E60E70041474F /* MediaKeyTap */,
E4E96D12233E630800AFD36F /* PMKFoundation */,
E43BEC9F238835DC00CAF1EB /* Kingfisher */,
);
productName = Persephone;
productReference = E40786182110CE6E006887B1 /* Persephone.app */;
@ -860,6 +843,7 @@
E49A548C233E5B6000EED353 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
E4A9BEF9233E5F9000457785 /* XCRemoteSwiftPackageReference "MediaKeyTap" */,
E4E96D11233E630800AFD36F /* XCRemoteSwiftPackageReference "Foundation" */,
E43BEC9E238835DC00CAF1EB /* XCRemoteSwiftPackageReference "Kingfisher" */,
);
productRefGroup = E40786192110CE6E006887B1 /* Products */;
projectDirPath = "";
@ -948,9 +932,7 @@
E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */,
E4B11B63226A4C510075461B /* AppReducer.swift in Sources */,
E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */,
E41E5310223EF6CE00173814 /* CoverArtService+Remote.swift in Sources */,
E442CCCD2347E73C00004E0C /* Artist.swift in Sources */,
E48E92D7235113DF00A5E1BB /* Metadata.swift in Sources */,
E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */,
E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */,
E4235640228623D2001216D6 /* QueueSongTitleView.swift in Sources */,
@ -961,6 +943,7 @@
E43B67AD229194CD007DCF55 /* AlbumTracksDataSource.swift in Sources */,
E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */,
E450AD7E222620A10091BED3 /* Album.swift in Sources */,
E4DA820623D6236200C1EE58 /* NSSize.swift in Sources */,
E408D3B6220DD8970006D9BE /* Notification.swift in Sources */,
E43AC1F822C7065A001E483C /* AlbumCoverButton.swift in Sources */,
E45962C62241A78500FC1A1E /* MPDCommand.swift in Sources */,
@ -1005,18 +988,15 @@
E4120D6C22AD8139004CB1F8 /* QueueView.swift in Sources */,
E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */,
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */,
E4A83BF4222207D50098FED6 /* CoverArtService.swift in Sources */,
E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */,
E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */,
E4B11B66226A4F830075461B /* PlayerState.swift in Sources */,
E4B11BBE2275EDAA0075461B /* PlayerActions.swift in Sources */,
E4F26F7723411AE300D45FF9 /* ArtistListActions.swift in Sources */,
E4FF71942276043A00D4C412 /* MPDServer.swift in Sources */,
E41E530E223EF4CF00173814 /* CoverArtService+Caching.swift in Sources */,
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */,
E440519C227BAF2E0090CD6F /* UIActions.swift in Sources */,
E45878382296173C00586A1C /* AlbumDetailSongRowView.swift in Sources */,
E41E5312223EF74A00173814 /* CoverArtService+Filesystem.swift in Sources */,
E41E5301223BF99300173814 /* MPDClient+Queue.swift in Sources */,
E4EB237B220F7CF1008C70C0 /* MPDAlbum.swift in Sources */,
E41E5303223BF9C300173814 /* MPDClient+Idle.swift in Sources */,
@ -1193,10 +1173,10 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = BDEE7ZBFZ3;
DEVELOPMENT_TEAM = 8E7TQ638ZD;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Persephone/Info.plist;
@ -1208,6 +1188,7 @@
"$(inherited)",
"$(PROJECT_DIR)/libmpdclient/output",
);
MARKETING_VERSION = "0.14.0-alpha";
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1221,10 +1202,10 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Mac Developer";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = BDEE7ZBFZ3;
DEVELOPMENT_TEAM = 8E7TQ638ZD;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Persephone/Info.plist;
@ -1236,6 +1217,7 @@
"$(inherited)",
"$(PROJECT_DIR)/libmpdclient/output",
);
MARKETING_VERSION = "0.14.0-alpha";
PRODUCT_BUNDLE_IDENTIFIER = me.danbarber.Persephone;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1389,6 +1371,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
E43BEC9E238835DC00CAF1EB /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.1;
};
};
E49A5480233E580800EED353 /* XCRemoteSwiftPackageReference "PromiseKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mxcl/PromiseKit";
@ -1448,6 +1438,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
E43BEC9F238835DC00CAF1EB /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = E43BEC9E238835DC00CAF1EB /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
E4677C47233E60E70041474F /* MediaKeyTap */ = {
isa = XCSwiftPackageProductDependency;
package = E4A9BEF9233E5F9000457785 /* XCRemoteSwiftPackageReference "MediaKeyTap" */;

View File

@ -28,6 +28,15 @@
"version": "3.3.3"
}
},
{
"package": "Kingfisher",
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "8ef6ca8b1b767ac2400762ed2f2bf75ddea3de5b",
"version": "5.10.1"
}
},
{
"package": "MediaKeyTap",
"repositoryURL": "https://github.com/danbee/MediaKeyTap",

View File

@ -23,9 +23,6 @@ class AppDelegate: NSObject,
@IBOutlet weak var addSelectedSongToQueueMenuItem: NSMenuItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
connectToMPDServer()
instantiateControllers()
mediaKeyTap = MediaKeyTap(delegate: self)
mediaKeyTap?.start()
@ -34,23 +31,10 @@ class AppDelegate: NSObject,
$0.uiState
}
}
}
func connectToMPDServer() {
let mpdServer = App.store.state.preferencesState.mpdServer
App.mpdClient = MPDClient(
host: mpdServer.hostOrDefault,
port: mpdServer.portOrDefault,
withDelegate: App.mpdServerDelegate
)
App.mpdClient.connect()
}
func instantiateControllers() {
_ = App.mpdServerController
_ = App.userNotificationsController
App.mpdServerController.connect()
}
func applicationWillTerminate(_ aNotification: Notification) {

View File

@ -23,34 +23,6 @@ class AlbumDataSource: NSObject, NSCollectionViewDataSource {
albumViewItem.view.wantsLayer = true
albumViewItem.setAlbum(albums[indexPath.item])
switch albums[indexPath.item].coverArt {
case .notLoaded:
App.mpdClient.getAlbumFirstSong(for: albums[indexPath.item].mpdAlbum) { mpdSong in
guard let mpdSong = mpdSong else { return }
DispatchQueue.main.async {
App.store.dispatch(
UpdateAlbumMetaData(
metadata: Metadata(date: mpdSong.date),
albumIndex: indexPath.item
)
)
}
CoverArtService(song: Song(mpdSong: mpdSong))
.fetchCoverArt()
.done { image in
DispatchQueue.main.async {
App.store.dispatch(
UpdateCoverArtAction(coverArt: image, albumIndex: indexPath.item)
)
}
}
}
default:
break
}
return albumViewItem
}
}

View File

@ -7,6 +7,7 @@
//
import AppKit
import Kingfisher
class AlbumViewItem: NSCollectionViewItem {
var observer: NSKeyValueObservation?
@ -43,6 +44,7 @@ class AlbumViewItem: NSCollectionViewItem {
override func prepareForReuse() {
super.prepareForReuse()
albumCoverView.image = .defaultCoverArt
AlbumDetailView.popover.close()
}
@ -50,13 +52,22 @@ class AlbumViewItem: NSCollectionViewItem {
self.album = album
albumTitle.stringValue = album.title
albumArtist.stringValue = album.artist
setAlbumCover(album)
}
switch album.coverArt {
case .loaded(let coverArt):
albumCoverView.image = coverArt ?? .defaultCoverArt
default:
albumCoverView.image = .defaultCoverArt
}
func setAlbumCover(_ album: Album) {
guard let imagePath = album.coverArtFilePath else { return }
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
albumCoverView.kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,
options: [
.processor(DownsamplingImageProcessor(size: .albumListCoverSize)),
.scaleFactor(2),
]
)
}
func setAppearance(selected isSelected: Bool) {

View File

@ -7,6 +7,7 @@
//
import AppKit
import Kingfisher
class AlbumDetailView: NSViewController {
var observer: NSKeyValueObservation?
@ -49,18 +50,11 @@ class AlbumDetailView: NSViewController {
getAlbumSongs(for: album)
let date = album.metadata?.date ?? ""
let date = album.mpdAlbum.date ?? ""
albumTitle.stringValue = album.title
albumMetadata.stringValue = "\(album.artist) · \(date)"
switch album.coverArt {
case .loaded(let coverArt):
albumCoverView.image = coverArt ?? .defaultCoverArt
default:
albumCoverView.image = .defaultCoverArt
}
super.viewWillAppear()
}
@ -86,6 +80,7 @@ class AlbumDetailView: NSViewController {
let queueLength = App.store.state.queueState.queue.count
App.mpdClient.appendSong(song.mpdSong)
App.mpdClient.fetchQueue()
App.mpdClient.playTrack(at: queueLength)
}
@ -95,6 +90,7 @@ class AlbumDetailView: NSViewController {
let queueLength = App.store.state.queueState.queue.count
App.mpdClient.appendSong(song.mpdSong)
App.mpdClient.fetchQueue()
App.mpdClient.playTrack(at: queueLength)
}
@ -132,20 +128,23 @@ class AlbumDetailView: NSViewController {
self.dataSource.albumSongs[1].song
else { return }
self.getBigCoverArt(song: song)
self.getBigCoverArt(song: song, album: album)
}
}
func getBigCoverArt(song: Song) {
let coverArtService = CoverArtService(song: song)
func getBigCoverArt(song: Song, album: Album) {
guard let imagePath = album.coverArtFilePath else { return }
coverArtService.fetchBigCoverArt()
.done(on: DispatchQueue.main) { [weak self] image in
if let image = image {
self?.albumCoverView.image = image
}
}
.cauterize()
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
albumCoverView.kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,
options: [
.processor(DownsamplingImageProcessor(size: .albumDetailCoverSize)),
.scaleFactor(2),
]
)
}
func setAppearance() {

View File

@ -8,25 +8,42 @@
import AppKit
import ReSwift
import Kingfisher
class CurrentCoverArtView: NSImageView {
required init?(coder: NSCoder) {
super.init(coder: coder)
App.store.subscribe(self) {
$0.select { $0.playerState.currentArtwork }
$0.select { $0.playerState.currentSong }
}
}
func setAlbumImage(_ album: Album) {
guard let imagePath = album.coverArtFilePath else { return }
let imageURL = URL(fileURLWithPath: imagePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
kf.setImage(
with: .provider(provider),
placeholder: NSImage.defaultCoverArt,
options: [
.processor(DownsamplingImageProcessor(size: .currentlyPlayingCoverSize)),
.scaleFactor(2),
]
)
}
}
extension CurrentCoverArtView: StoreSubscriber {
typealias StoreSubscriberStateType = NSImage?
typealias StoreSubscriberStateType = Song?
func newState(state: NSImage?) {
if let coverArt = state {
image = coverArt
} else {
func newState(state: Song?) {
guard let song = state else {
image = .defaultCoverArt
return
}
setAlbumImage(song.album)
}
}

View File

@ -0,0 +1,16 @@
//
// NSSize.swift
// Persephone
//
// Created by Daniel Barber on 1/20/20.
// Copyright © 2020 Dan Barber. All rights reserved.
//
import AppKit
extension NSSize {
static let albumListCoverSize = NSSize(width: 180, height: 180)
static let albumDetailCoverSize = NSSize(width: 500, height: 500)
static let currentlyPlayingCoverSize = albumDetailCoverSize
static let notificationCoverSize = albumListCoverSize
}

View File

@ -11,9 +11,24 @@ import ReSwift
class MPDServerController {
init() {
// App.store.subscribe(self) {
// $0.select { $0.preferencesState.mpdServer }
// }
App.mpdClient = MPDClient(withDelegate: App.mpdServerDelegate)
App.store.subscribe(self) {
$0.select { $0.preferencesState.mpdServer }
}
}
func connect() {
let mpdServer = App.store.state.preferencesState.mpdServer
App.mpdClient.connect(
host: mpdServer.hostOrDefault,
port: mpdServer.portOrDefault
)
}
func disconnect() {
App.mpdClient.disconnect()
}
}
@ -21,7 +36,9 @@ extension MPDServerController: StoreSubscriber {
typealias StoreSubscriberStateType = MPDServer
func newState(state: MPDServer) {
App.mpdClient.disconnect()
App.mpdClient.connect()
guard App.mpdClient != nil else { return }
disconnect()
connect()
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import ReSwift
import Kingfisher
class UserNotificationsController {
init() {
@ -18,18 +19,28 @@ class UserNotificationsController {
func notifyTrack(_ state: Song?) {
guard let currentSong = state,
let coverArtFilePath = currentSong.album.coverArtFilePath,
let status = App.mpdClient.status,
status.state == .playing
else { return }
let coverArtService = CoverArtService(song: currentSong)
coverArtService.fetchBigCoverArt()
.done() {
SongNotifierService(song: currentSong, image: $0)
let imageURL = URL(fileURLWithPath: coverArtFilePath)
let provider = LocalFileImageDataProvider(fileURL: imageURL)
_ = KingfisherManager.shared.retrieveImage(
with: .provider(provider),
options: [
.processor(DownsamplingImageProcessor(size: .notificationCoverSize)),
.scaleFactor(2),
]
) { result in
switch result {
case .success(let value):
SongNotifierService(song: currentSong, image: value.image)
.deliver()
case .failure:
break
}
.cauterize()
}
}
}

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="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15702"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -302,6 +302,21 @@
</connections>
</button>
</toolbarItem>
<toolbarItem implicitItemIdentifier="7C15D391-03F5-42C7-AC6D-6E17549C698E" label="Search" paletteLabel="Search" sizingBehavior="auto" id="FRe-rR-Ulo">
<nil key="toolTip"/>
<searchField key="view" wantsLayer="YES" verticalHuggingPriority="750" textCompletion="NO" id="xfU-Xe-eno">
<rect key="frame" x="0.0" y="14" width="96" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" id="F3N-3P-tS3">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</searchFieldCell>
<connections>
<action selector="handleSearchQuery:" target="B8D-0N-5wS" id="BPh-1O-bUU"/>
</connections>
</searchField>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="p3r-ty-Pxf"/>
@ -314,6 +329,7 @@
<toolbarItem reference="s1h-EC-nvL"/>
<toolbarItem reference="5U7-UV-xn2"/>
<toolbarItem reference="9ol-aR-mzv"/>
<toolbarItem reference="FRe-rR-Ulo"/>
</defaultToolbarItems>
</toolbar>
<connections>
@ -323,6 +339,7 @@
<connections>
<outlet property="databaseUpdatingIndicator" destination="LpV-iM-o6t" id="y0T-eR-ygY"/>
<outlet property="repeatState" destination="OqH-lV-sAg" id="DPC-Ff-Srr"/>
<outlet property="searchQuery" destination="xfU-Xe-eno" id="d8u-sH-13V"/>
<outlet property="shuffleState" destination="E8L-uK-XT0" id="dCF-hm-dBs"/>
<outlet property="trackProgress" destination="kx6-xm-TAN" id="XDv-Th-Agj"/>
<outlet property="trackProgressBar" destination="KMy-xf-rmN" id="a67-JU-cyQ"/>
@ -392,7 +409,7 @@
<tabView key="tabView" type="noTabsNoBorder" id="6dC-M0-oC5">
<rect key="frame" x="0.0" y="0.0" width="418" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<font key="font" metaFont="message"/>
<font key="font" metaFont="system"/>
<connections>
<outlet property="delegate" destination="zhe-qh-Mal" id="LUL-qN-JlP"/>
</connections>
@ -436,7 +453,7 @@
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pRL-MG-1Be">
<rect key="frame" x="160" y="94" width="264" height="18"/>
<buttonCell key="cell" type="check" title="Fetch missing artwork from MusicBrainz" bezelStyle="regularSquare" imagePosition="left" 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"/>
<font key="font" metaFont="system"/>
</buttonCell>
@ -543,9 +560,6 @@
<constraint firstItem="ARv-cj-xlz" firstAttribute="leading" secondItem="BRY-0R-F3u" secondAttribute="leading" id="w2Z-xv-Fwz"/>
</constraints>
</view>
<connections>
<outlet property="browseTabView" destination="ARv-cj-xlz" id="h93-fi-yY7"/>
</connections>
</viewController>
</objects>
<point key="canvasLocation" x="1436" y="238"/>
@ -633,26 +647,26 @@
<objects>
<viewController id="KIP-rq-4dM" customClass="QueueViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<splitView key="view" dividerStyle="thin" id="84I-w3-Mxl">
<rect key="frame" x="0.0" y="0.0" width="328" height="548"/>
<rect key="frame" x="0.0" y="0.0" width="328" height="547"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="17" horizontalPageScroll="10" verticalLineScroll="17" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="S3o-nF-NN7">
<scrollView misplaced="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="17" horizontalPageScroll="10" verticalLineScroll="17" verticalPageScroll="10" usesPredominantAxisScrolling="NO" id="S3o-nF-NN7">
<rect key="frame" x="0.0" y="0.0" width="328" height="219"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<clipView key="contentView" drawsBackground="NO" id="WI8-Pw-03L">
<rect key="frame" x="0.0" y="0.0" width="328" height="219"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" multipleSelection="NO" autosaveColumns="NO" rowSizeStyle="automatic" viewBased="YES" indentationPerLevel="14" outlineTableColumn="0Co-uF-CCB" id="jEJ-jg-fll" customClass="QueueView" customModule="Persephone" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="328" height="219"/>
<autoresizingMask key="autoresizingMask"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="0.0"/>
<color key="backgroundColor" name="_sourceListBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="songTitleColumn" width="200" minWidth="128" maxWidth="1000" id="0Co-uF-CCB">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Title">
<font key="font" metaFont="smallSystem"/>
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
@ -733,7 +747,7 @@
</tableColumn>
<tableColumn identifier="songArtistColumn" width="122" minWidth="64" maxWidth="1000" id="SPM-QP-DX8">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Artist">
<font key="font" metaFont="smallSystem"/>
<font key="font" metaFont="message" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
@ -791,8 +805,8 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<customView id="iUb-eV-Qws">
<rect key="frame" x="0.0" y="220" width="328" height="328"/>
<customView misplaced="YES" id="iUb-eV-Qws">
<rect key="frame" x="0.0" y="220" width="328" height="327"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Dw3-M5-tWY" customClass="CurrentCoverArtView" customModule="Persephone" customModuleProvider="target">

View File

@ -27,7 +27,7 @@ class WindowController: NSWindowController {
@IBOutlet var shuffleState: NSButton!
@IBOutlet var repeatState: NSButton!
@IBOutlet var browseViewControls: NSSegmentedControl!
@IBOutlet weak var searchQuery: NSSearchField!
override func windowDidLoad() {
super.windowDidLoad()
@ -41,6 +41,8 @@ class WindowController: NSWindowController {
}
App.store.dispatch(MainWindowDidOpenAction())
NotificationCenter.default.addObserver(self, selector: #selector(willDisconnect), name: .willDisconnect, object: nil)
trackProgress.font = .timerFont
trackRemaining.font = .timerFont
@ -49,7 +51,9 @@ class WindowController: NSWindowController {
override func keyDown(with event: NSEvent) {
switch event.keyCode {
case NSEvent.keyCodeSpace:
App.mpdClient.playPause()
if !event.isARepeat {
App.mpdClient.playPause()
}
default:
nextResponder?.keyDown(with: event)
}
@ -116,6 +120,13 @@ class WindowController: NSWindowController {
trackRemaining.stringValue = time.formattedTime
}
@objc func willDisconnect() {
DispatchQueue.main.async {
App.store.dispatch(SetSearchQuery(searchQuery: ""))
self.searchQuery.stringValue = ""
}
}
// TODO: Refactor this using a gesture recognizer
@IBAction func changeTrackProgress(_ sender: NSSlider) {
@ -161,6 +172,10 @@ class WindowController: NSWindowController {
@IBAction func handleRepeatButton(_ sender: NSButton) {
App.mpdClient.setRepeatState(repeatState: sender.state == .on)
}
@IBAction func handleSearchQuery(_ sender: NSSearchField) {
App.store.dispatch(SetSearchQuery(searchQuery: sender.stringValue))
}
}
extension WindowController: NSWindowDelegate {

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.13.0-alpha</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>

View File

@ -11,7 +11,11 @@ import mpdclient
extension MPDClient {
func fetchAllAlbums() {
enqueueCommand(command: .fetchAllAlbums)
enqueueCommand(command: .fetchAlbums, userData: ["filter": ""])
}
func fetchAlbums(filter: String) {
enqueueCommand(command: .fetchAlbums, userData: ["filter": filter])
}
func playAlbum(_ album: MPDAlbum) {
@ -41,6 +45,7 @@ extension MPDClient {
priority: .normal,
userData: ["songs": songs]
)
self.enqueueCommand(command: .fetchQueue)
self.enqueueCommand(
command: .playTrack,
priority: .normal,
@ -49,27 +54,32 @@ extension MPDClient {
}
}
func allAlbums() {
func albums(filter: String) {
var albums: [MPDAlbum] = []
var artist: String = ""
mpd_search_db_tags(self.connection, MPD_TAG_ALBUM)
mpd_search_add_group_tag(self.connection, MPD_TAG_ALBUM_ARTIST)
mpd_search_db_songs(self.connection, false)
if filter != "" {
mpd_search_add_expression(
self.connection,
"(any contains '\(filter)')"
)
}
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_TRACK, "1")
mpd_search_commit(self.connection)
while let pair = mpd_recv_pair(self.connection) {
let pair = MPDPair(pair)
while let song = mpd_recv_song(self.connection) {
let mpdSong = MPDSong(song)
switch pair.name {
case "AlbumArtist":
artist = pair.value
case "Album":
albums.append(MPDAlbum(title: pair.value, artist: artist))
default:
break
let mpdAlbum = MPDAlbum(
title: mpdSong.album.title,
artist: mpdSong.artist,
date: mpdSong.date,
path: mpdSong.path
)
if (mpdAlbum != albums.last) {
albums.append(mpdAlbum)
}
mpd_return_pair(self.connection, pair.pair)
}
self.delegate?.didLoadAlbums(mpdClient: self, albums: albums)
@ -101,7 +111,7 @@ extension MPDClient {
func albumSongs(for album: MPDAlbum, callback: ([MPDSong]) -> Void) {
guard isConnected else { return }
let songs = searchSongs([MPDTag.album: album.title, MPDTag.artist: album.artist])
let songs = searchSongs([MPDTag.album: album.title, MPDTag.albumArtist: album.artist])
callback(songs)
}

View File

@ -14,6 +14,14 @@ extension MPDClient {
userData: Dictionary<String, Any> = [:]
) {
switch command {
case .connect:
guard let host = userData["host"] as? String,
let port = userData["port"] as? Int
else { return }
createConnection(host: host, port: port)
case .disconnect:
freeConnection()
// Transport commands
case .prevTrack:
@ -92,8 +100,9 @@ extension MPDClient {
allArtists()
// Album commands
case .fetchAllAlbums:
allAlbums()
case .fetchAlbums:
guard let filter = userData["filter"] as? String else { return }
albums(filter: filter)
case .playAlbum:
guard let album = userData["album"] as? MPDAlbum else { return }
sendPlayAlbum(album)

View File

@ -10,40 +10,44 @@ import Foundation
import mpdclient
extension MPDClient {
func makeConnectionOperation(host: String, port: Int) -> BlockOperation {
BlockOperation { [unowned self] in
guard let connection = mpd_connection_new(host, UInt32(port), 10000),
mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS
else { return }
func createConnection(host: String, port: Int) {
guard let connection = mpd_connection_new(host, UInt32(port), 10000),
mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS
else { return }
self.isConnected = true
self.isConnected = true
guard let status = mpd_run_status(connection)
else { return }
guard let status = mpd_run_status(connection)
else { return }
self.connection = connection
self.status = MPDStatus(status)
self.connection = connection
self.status = MPDStatus(status)
self.delegate?.didConnect(mpdClient: self)
self.delegate?.didUpdateStatus(mpdClient: self, status: self.status!)
self.idle()
}
delegate?.didConnect(mpdClient: self)
delegate?.didUpdateStatus(mpdClient: self, status: self.status!)
}
func connect() {
commandQueue.addOperation(connectionOperation)
}
func disconnect() {
func freeConnection() {
guard isConnected else { return }
self.delegate?.willDisconnect(mpdClient: self)
noIdle()
commandQueue.addOperation { [unowned self] in
self.delegate?.willDisconnect(mpdClient: self)
mpd_connection_free(self.connection)
self.isConnected = false
}
mpd_connection_free(self.connection)
self.isConnected = false
func connect(host: String, port: Int) {
let commandOperation = BlockOperation() { [unowned self] in
self.sendCommand(command: .connect, userData: ["host": host, "port": port])
if self.isConnected {
self.idle()
}
}
commandQueue.addOperation(commandOperation)
}
func disconnect() {
enqueueCommand(command: .disconnect)
}
}

View File

@ -22,9 +22,8 @@ class MPDClient {
let commandQueue = OperationQueue()
init(host: String, port: Int, withDelegate delegate: MPDClientDelegate?) {
init(withDelegate delegate: MPDClientDelegate?) {
commandQueue.maxConcurrentOperationCount = 1
self.delegate = delegate
self.connectionOperation = makeConnectionOperation(host: host, port: port)
}
}

View File

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

View File

@ -10,6 +10,9 @@ import Foundation
extension MPDClient {
enum MPDCommand {
case connect
case disconnect
// Transport commands
case prevTrack
case nextTrack
@ -41,7 +44,7 @@ extension MPDClient {
case fetchAllArtists
// Album commands
case fetchAllAlbums
case fetchAlbums
case playAlbum
case getAlbumFirstSong
case getAlbumSongs

View File

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

View File

@ -11,8 +11,6 @@ import CryptoSwift
struct Album {
var mpdAlbum: MPDClient.MPDAlbum
var coverArt: Loading<NSImage?> = .notLoaded
var metadata: Metadata?
init(mpdAlbum: MPDClient.MPDAlbum) {
self.mpdAlbum = mpdAlbum
@ -26,14 +24,41 @@ struct Album {
return mpdAlbum.artist
}
var date: String {
guard let date = mpdAlbum.date else { return "" }
return date
}
var hash: String {
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 {
static func == (lhs: Album, rhs: Album) -> Bool {
return (lhs.mpdAlbum == rhs.mpdAlbum) &&
(lhs.coverArt ~= rhs.coverArt)
return lhs.mpdAlbum == rhs.mpdAlbum
}
}

View File

@ -19,6 +19,6 @@ struct Artist {
extension Artist: Equatable {
static func == (lhs: Artist, rhs: Artist) -> Bool {
return (lhs.name == rhs.name)
return lhs.name == rhs.name
}
}

View File

@ -27,7 +27,7 @@ struct MPDServer {
extension MPDServer: Equatable {
static func == (lhs: MPDServer, rhs: MPDServer) -> Bool {
return (lhs.host == rhs.host) &&
(lhs.port == rhs.port)
return lhs.host == rhs.host &&
lhs.port == rhs.port
}
}

View File

@ -1,13 +0,0 @@
//
// Metadata.swift
// Persephone
//
// Created by Daniel Barber on 2019/10/11.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
struct Metadata {
var date: String?
}

View File

@ -1,70 +0,0 @@
//
// CoverArtService.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/23.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import PromiseKit
class CoverArtService {
let song: Song
let album: Album
let cachedArtworkSize = 180
let cachedArtworkQuality: CGFloat = 0.5
let bigArtworkSize = 600
var session = URLSession(configuration: .default)
let coverArtQueue = DispatchQueue(label: "coverArtQueue", qos: .utility)
init(song: Song) {
self.song = song
self.album = song.album
}
func fetchBigCoverArt() -> Promise<NSImage?> {
return firstly {
self.getArtworkFromFilesystem()
}.then { (image: NSImage?) -> Promise<NSImage?> in
image.map(Promise.value) ?? self.getRemoteArtwork()
}.recover { (_) -> Guarantee<NSImage?> in
return .value(nil)
}
}
func fetchCoverArt() -> 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()
}.compactMap(on: coverArtQueue) {
return self.sizeAndCacheImage($0).map(Optional.some)
}.recover { _ in
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

@ -1,51 +0,0 @@
//
// CoverArtService+Caching.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/17.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import PromiseKit
extension CoverArtService {
static let cacheDir = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent(Bundle.main.bundleIdentifier!)
func getCachedArtwork() -> Promise<NSImage?> {
return Promise { seal in
coverArtQueue.async {
if self.isArtworkCached() {
let cacheFilePath = CoverArtService.cacheDir.appendingPathComponent(self.album.hash).path
let data = FileManager.default.contents(atPath: cacheFilePath)
let image = NSImage(data: data ?? Data()) ?? NSImage.defaultCoverArt
seal.fulfill(image)
} else {
seal.fulfill(nil)
}
}
}
}
func cacheArtwork(data: Data?) {
coverArtQueue.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(self.album.hash).path
if !self.isArtworkCached() {
FileManager.default.createFile(atPath: cacheFilePath, contents: data, attributes: nil)
}
}
}
func isArtworkCached() -> Bool {
let cacheFilePath = CoverArtService.cacheDir.appendingPathComponent(album.hash).path
return FileManager.default.fileExists(atPath: cacheFilePath)
}
}

View File

@ -1,77 +0,0 @@
//
// CoverArtService+Filesystem.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/17.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import PromiseKit
extension CoverArtService {
var coverArtFilenames: [String] {
return [
"folder.jpg",
"cover.jpg",
"\(album.artist) - \(album.title).jpg"
]
}
var musicDir: String {
return App.store.state.preferencesState.expandedMpdLibraryDir
}
func getArtworkFromFilesystem() -> Promise<NSImage?> {
return Promise { seal in
coverArtQueue.async {
guard let artworkPath = self.fileSystemArtworkFilePath()
else { seal.fulfill(nil); return }
let image = self.tryImage(artworkPath)
seal.fulfill(image)
}
}
}
func saveArtworkToFilesystem(data: Data?) {
let artworkFileName = coverArtFilenames.first!
if self.fileSystemArtworkFilePath() == nil {
FileManager.default.createFile(
atPath: "\(self.musicDir)/\(self.songPath)/\(artworkFileName)",
contents: data,
attributes: nil
)
}
}
func fileSystemArtworkFilePath() -> String? {
let musicDir = App.store.state.preferencesState.expandedMpdLibraryDir
return self.coverArtFilenames
.lazy
.map { "\(musicDir)/\(self.songPath)/\($0)" }
.first {
FileManager.default.fileExists(atPath: $0)
}
}
var songPath: String {
return song
.mpdSong
.uriString
.split(separator: "/")
.dropLast()
.joined(separator: "/")
}
func tryImage(_ filePath: String) -> NSImage? {
guard let data = FileManager.default.contents(atPath: filePath),
let image = NSImage(data: data)
else { return nil }
return image
}
}

View File

@ -1,62 +0,0 @@
//
// CoverArtService+Remote.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/17.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import SwiftyJSON
import PromiseKit
import PMKFoundation
extension CoverArtService {
enum RemoteArtworkError: Error {
case noArtworkAvailable
case notConfigured
}
func getRemoteArtwork() -> Promise<NSImage?> {
return Promise { seal in
if App.store.state.preferencesState .fetchMissingArtworkFromInternet {
coverArtQueue.async {
let coverArtWorkItem = DispatchWorkItem {
self.getArtworkFromMusicBrainz().map(Optional.some).pipe(to: seal.resolve)
}
CoverArtQueue.shared.addToQueue(workItem: coverArtWorkItem)
}
} else {
throw RemoteArtworkError.notConfigured
}
}
}
func getArtworkFromMusicBrainz() -> Promise<NSImage> {
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 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 RemoteArtworkError.noArtworkAvailable
} else {
throw error
}
}
}
}

View File

@ -9,18 +9,6 @@
import AppKit
import ReSwift
struct ResetAlbumListCoverArtAction: Action {}
struct UpdateCoverArtAction: Action {
var coverArt: NSImage?
var albumIndex: Int
}
struct UpdateAlbumMetaData: Action {
var metadata: Metadata
var albumIndex: Int
}
struct UpdateAlbumListAction: Action {
var albums: [MPDClient.MPDAlbum]
}

View File

@ -9,10 +9,6 @@
import AppKit
import ReSwift
struct UpdateCurrentCoverArtAction: Action {
var coverArt: NSImage?
}
struct UpdateCurrentSongAction: Action {
var currentSong: Song?
}

View File

@ -25,3 +25,7 @@ struct SetSelectedQueueItem: Action {
struct SetSelectedSong: Action {
let selectedSong: Song?
}
struct SetSearchQuery: Action {
let searchQuery: String
}

View File

@ -12,7 +12,6 @@ import ReSwift
struct PlayerState: StateType {
var status: MPDClient.MPDStatus?
var currentSong: Song?
var currentArtwork: NSImage?
var state: MPDClient.MPDStatus.State?
var shuffleState: Bool = false

View File

@ -15,26 +15,6 @@ func albumListReducer(action: Action, state: AlbumListState?) -> AlbumListState
case let action as UpdateAlbumListAction:
state.albums = action.albums.map { Album(mpdAlbum: $0) }
case let action as UpdateCoverArtAction:
state.albums[action.albumIndex].coverArt = .loaded(action.coverArt)
case let action as UpdateAlbumMetaData:
state.albums[action.albumIndex].metadata = action.metadata
case is ResetAlbumListCoverArtAction:
state.albums = state.albums.map {
var album = $0
switch album.coverArt {
case .loaded(let coverArt):
if coverArt == nil {
album.coverArt = .notLoaded
}
default:
album.coverArt = .notLoaded
}
return album
}
default:
break

View File

@ -39,29 +39,6 @@ func playerReducer(action: Action, state: PlayerState?) -> PlayerState {
case let action as UpdateCurrentSongAction:
state.currentSong = action.currentSong
if let currentSong = state.currentSong {
let coverArtService = CoverArtService(song: currentSong)
coverArtService.fetchBigCoverArt()
.done() { image in
DispatchQueue.main.async {
if let image = image {
App.store.dispatch(UpdateCurrentCoverArtAction(coverArt: image))
} else {
App.store.dispatch(UpdateCurrentCoverArtAction(coverArt: .defaultCoverArt))
}
}
}
.cauterize()
} else {
DispatchQueue.main.async {
App.store.dispatch(UpdateCurrentCoverArtAction(coverArt: .defaultCoverArt))
}
}
case let action as UpdateCurrentCoverArtAction:
state.currentArtwork = action.coverArt
case let action as UpdateElapsedTimeAction:
state.elapsedTimeMs = action.elapsedTimeMs

View File

@ -36,7 +36,7 @@ func queueReducer(action: Action, state: QueueState?) -> QueueState {
if oldSongRowPos >= 0 {
state.queue[oldSongRowPos].isPlaying = false
}
if newSongRowPos >= 0 {
if newSongRowPos >= 0 && state.queue.count > newSongRowPos {
state.queue[newSongRowPos].isPlaying = true
DispatchQueue.main.async {

View File

@ -6,6 +6,7 @@
// Copyright © 2019 Dan Barber. All rights reserved.
//
import AppKit
import ReSwift
func uiReducer(action: Action, state: UIState?) -> UIState {
@ -33,6 +34,12 @@ func uiReducer(action: Action, state: UIState?) -> UIState {
case let action as SetSelectedQueueItem:
state.selectedQueueItem = action.selectedQueueItem
case let action as SetSearchQuery:
state.searchQuery = action.searchQuery
DispatchQueue.main.async {
App.mpdClient.fetchAlbums(filter: state.searchQuery)
}
default:
break
}

View File

@ -22,4 +22,6 @@ struct UIState: StateType {
var selectedSong: Song?
var selectedQueueItem: QueueItem?
var searchQuery: String = ""
}