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

Compare commits

..

34 Commits

Author SHA1 Message Date
c406737ec1
Update screenshot 2019-03-23 19:25:50 -04:00
55c543411e
Add changelog 2019-03-23 19:18:19 -04:00
4b43d661c6
Bump version number 2019-03-23 14:42:42 -04:00
0db3cbe137
Add icon for album art preferences 2019-03-23 14:42:41 -04:00
560a3bf14f
Update minor version number 2019-03-23 14:42:41 -04:00
9eacd2b25a
Rename delegate methods to better reflect purpose 2019-03-23 14:42:41 -04:00
92ca7ea93f
Update version number 2019-03-23 14:42:41 -04:00
b43afb405f
Disable the menu item during updates 2019-03-23 14:42:41 -04:00
79ad585530
The database doesn't always update (if there's no update) 2019-03-23 14:42:41 -04:00
8983106cda
Wire up "update database"
* Menu option starts a database update.

* Spinner to show that update is taking place.
2019-03-23 14:42:40 -04:00
0147ffbfda
Couple of tweaks 2019-03-23 14:29:41 -04:00
ce5b0be2e1
Refactor album art with promises
Co-authored-by: Adam Sharp <adam@sharplet.me>
2019-03-22 17:14:32 -04:00
5672ded50a
Fetch artwork from MusicBrainz if not on FS 2019-03-20 20:06:24 -04:00
11be238788
Address PR feedback
* Use the OperationQueue's `operationCount` function instead of keeping
count ourselves. This is reliable now each command is entirely self
contained.

* Rename `queueCommand` to `enqueueCommand`

* Move the command Enum into its own model file

* Move the `enqueueCommand` function into MPDClient+Command
2019-03-20 20:06:24 -04:00
9714aabb10
Update version number 2019-03-20 20:06:24 -04:00
9123a25bc7
Refactor art service 2019-03-20 20:06:24 -04:00
e8b58b7686
Scale down cover images
This brings memory usage (for my music library) down from 2+GB to less
than 300MB. 👍🏼
2019-03-20 20:06:23 -04:00
280ec0cdc4
Reload album data when the library path is changed 2019-03-20 20:06:23 -04:00
487e0cc2c2
Fix a few things that got forgotten during the refactor 2019-03-20 20:06:23 -04:00
9517abf319
Now gets artwork from filesystem! 2019-03-20 20:06:23 -04:00
fe748e2c61
WIP: Refactor MPDClient
This should make handling the queuing side work more reliably.
2019-03-20 20:06:23 -04:00
537a66d6aa
Add album art preferences pane 2019-03-20 20:06:23 -04:00
88aa765e83
Re-queue any cover art request that errors 2019-03-20 20:06:22 -04:00
b1f3e6d399
Updated screenshot 2019-03-20 20:06:22 -04:00
592cd73ec7
WIP: Code to try out different services 2019-03-20 20:06:22 -04:00
5e97bfc42b
Bump version number to 0.9.0 2019-03-20 20:06:22 -04:00
02a68ba539
We have a shared resource for these now 2019-03-20 20:06:22 -04:00
38431702d2
Refactor the album art code to use promises 2019-03-20 20:06:21 -04:00
4ff0ff6e9b
This is how the collection view should be updated
The way I was doing it before was resulting in reused views getting
updated by the code, which caused albums to appear in the wrong place.
2019-03-20 20:06:21 -04:00
b91cb50f4e
New blank album icon 2019-03-20 20:06:21 -04:00
51bc2c9adf
Getting album art should be the datasource's responsibility 2019-03-20 20:06:21 -04:00
8afe0a15fa
WIP: Add Alamofire 2019-03-20 20:06:21 -04:00
3b7bdc7983
WIP: Create operation queue for album art download 2019-03-20 20:06:21 -04:00
480c2786ad
WIP: Fetch album art from MusicBrainz 2019-03-20 20:06:20 -04:00
64 changed files with 1511 additions and 339 deletions

27
CHANGELOG.md Normal file
View File

@ -0,0 +1,27 @@
# Changelog
## 0.10.2a - 2019-03-23
### Added
- Will fetch album art from the filesystem or MusicBrainz
- Menu option to update the database
- Album art preferences icon
### Changed
- Refactored command queue to be more reliable
- Resizing the windows no longer makes the album view jump around
- Fixed a crash when another client clears the queue
## 0.8.0a - 2019-02-23
### Added
- Connects to MPD
- Lists all albums
- Will play an album
- Queue is working
- Transport controls are working
- Seek bar is working
- Media keys work

View File

@ -1 +1,3 @@
github "SwiftyJSON/SwiftyJSON" ~> 4.0
github "PromiseKit/Foundation" ~> 3.0
github "nhurden/MediaKeyTap" "fix-tis-tsm-error"

View File

@ -1 +1,4 @@
github "PromiseKit/Foundation" "3.3.1"
github "SwiftyJSON/SwiftyJSON" "4.2.0"
github "mxcl/PromiseKit" "6.8.3"
github "nhurden/MediaKeyTap" "355d346c56243e6d56487fa46fcad945251e16ae"

View File

@ -16,29 +16,60 @@
E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */; };
E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */; };
E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C1220E134F0006D9BE /* AlbumViewController.swift */; };
E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = E408D3C9220E341D0006D9BE /* AlbumItem.xib */; };
E408D3CB220E341D0006D9BE /* AlbumViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = E408D3C9220E341D0006D9BE /* AlbumViewItem.xib */; };
E40F41F3221EDE27004B6CB8 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40F41F2221EDE27004B6CB8 /* Preferences.swift */; };
E40FE71B221B904300A4223F /* NSEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40FE71A221B904300A4223F /* NSEvent.swift */; };
E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (Required, ); }; };
E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41B22C521FB932700D544F6 /* MPDClient.swift */; };
E41EA46C221636AF0068EF46 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */; };
E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E52FC223BF87300173814 /* MPDClient+Connection.swift */; };
E41E52FF223BF95E00173814 /* MPDClient+Transport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */; };
E41E5301223BF99300173814 /* MPDClient+Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5300223BF99300173814 /* MPDClient+Queue.swift */; };
E41E5303223BF9C300173814 /* MPDClient+Idle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5302223BF9C300173814 /* MPDClient+Idle.swift */; };
E41E5305223BFB0700173814 /* MPDClient+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5304223BFB0700173814 /* MPDClient+Error.swift */; };
E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5306223C019100173814 /* MPDClient+Status.swift */; };
E41E5309223C020400173814 /* MPDClient+Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5308223C020400173814 /* MPDClient+Command.swift */; };
E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530A223C033700173814 /* MPDClient+Album.swift */; };
E41E530E223EF4CF00173814 /* AlbumArtService+Caching.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530D223EF4CF00173814 /* AlbumArtService+Caching.swift */; };
E41E5310223EF6CE00173814 /* AlbumArtService+Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */; };
E41E5312223EF74A00173814 /* AlbumArtService+Filesystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */; };
E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */; };
E421ACA3221F73C4008B2449 /* MediaKeyTap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */; };
E421ACA4221F73C4008B2449 /* MediaKeyTap.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42410B52241B956005ED6DF /* MPDClient+Database.swift */; };
E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = E42A8F3922176D6400A13ED9 /* LICENSE.md */; };
E42A8F3C22176D6400A13ED9 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = E42A8F3A22176D6400A13ED9 /* README.md */; };
E435E3E2221CD4E200184CFC /* NSFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = E435E3E1221CD4E200184CFC /* NSFont.swift */; };
E435E3E4221CD75D00184CFC /* NSImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E435E3E3221CD75D00184CFC /* NSImage.swift */; };
E450AD7E222620A10091BED3 /* AlbumItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD7D222620A10091BED3 /* AlbumItem.swift */; };
E450AD8622262AE60091BED3 /* SwiftyJSON.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E450AD8522262AE60091BED3 /* SwiftyJSON.framework */; };
E450AD8822262AEC0091BED3 /* SwiftyJSON.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E450AD8522262AE60091BED3 /* SwiftyJSON.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E450AD8F22262C620091BED3 /* PromiseKit.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E450AD8E22262C620091BED3 /* PromiseKit.framework.dSYM */; };
E450AD9122262C780091BED3 /* SwiftyJSON.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E450AD9022262C780091BED3 /* SwiftyJSON.framework.dSYM */; };
E450AD9222262C970091BED3 /* PromiseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E450AD8C22262C590091BED3 /* PromiseKit.framework */; };
E450AD9322262C970091BED3 /* PromiseKit.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E450AD8C22262C590091BED3 /* PromiseKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E450AD9522262DF10091BED3 /* AlbumArtQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD9422262DF10091BED3 /* AlbumArtQueue.swift */; };
E450AD98222633920091BED3 /* Alamofire.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E450AD96222633920091BED3 /* Alamofire.framework.dSYM */; };
E450AD9D2229B9050091BED3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = E450AD9C2229B9050091BED3 /* String.swift */; };
E450ADA12229E7C90091BED3 /* PMKFoundation.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E450AD9F2229E7C90091BED3 /* PMKFoundation.framework.dSYM */; };
E450ADA32229E7E00091BED3 /* PMKFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E450ADA02229E7C90091BED3 /* PMKFoundation.framework */; };
E450ADA42229E7E00091BED3 /* PMKFoundation.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E450ADA02229E7C90091BED3 /* PMKFoundation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E45962C62241A78500FC1A1E /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45962C52241A78500FC1A1E /* Command.swift */; };
E465049A21E94DF500A70F4C /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E465049921E94DF500A70F4C /* WindowController.swift */; };
E47E2FCC2220573500F747E6 /* MediaKeyTap.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = E47E2FCB2220573500F747E6 /* MediaKeyTap.framework.dSYM */; };
E47E2FD122205C4600F747E6 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */; };
E47E2FD322205D2500F747E6 /* MainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD222205D2500F747E6 /* MainWindow.swift */; };
E47E2FD5222071FD00F747E6 /* AlbumItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD4222071FD00F747E6 /* AlbumItem.swift */; };
E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */; };
E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FD62220720300F747E6 /* AlbumItemView.swift */; };
E47E2FDD2220A6D100F747E6 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FDC2220A6D100F747E6 /* Time.swift */; };
E47E2FE52220AA0700F747E6 /* AlbumViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47E2FE42220AA0700F747E6 /* AlbumViewLayout.swift */; };
E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4928E0A2218D62A001D4BEA /* CGColor.swift */; };
E4A642DA22090CBE00067D21 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A642D922090CBE00067D21 /* Status.swift */; };
E4A83BEF2221F8CF0098FED6 /* AlbumArtPrefsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */; };
E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */; };
E4A83BF4222207D50098FED6 /* AlbumArtService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A83BF3222207D50098FED6 /* AlbumArtService.swift */; };
E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */; };
E4C8B53E22349002009A20F3 /* Idle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53D22349002009A20F3 /* Idle.swift */; };
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; };
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; };
E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC932206097F0024217A /* NotificationsController.swift */; };
@ -75,7 +106,10 @@
dstSubfolderSpec = 10;
files = (
E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */,
E450ADA42229E7E00091BED3 /* PMKFoundation.framework in Embed Libraries */,
E421ACA4221F73C4008B2449 /* MediaKeyTap.framework in Embed Libraries */,
E450AD8822262AEC0091BED3 /* SwiftyJSON.framework in Embed Libraries */,
E450AD9322262C970091BED3 /* PromiseKit.framework in Embed Libraries */,
);
name = "Embed Libraries";
runOnlyForDeploymentPostprocessing = 0;
@ -108,7 +142,7 @@
E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSUserInterfaceItemIdentifier.swift; sourceTree = "<group>"; };
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawRepresentable.swift; sourceTree = "<group>"; };
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewController.swift; sourceTree = "<group>"; };
E408D3C9220E341D0006D9BE /* AlbumItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumItem.xib; sourceTree = "<group>"; };
E408D3C9220E341D0006D9BE /* AlbumViewItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumViewItem.xib; sourceTree = "<group>"; };
E40F41F2221EDE27004B6CB8 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
E40FE71A221B904300A4223F /* NSEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEvent.swift; sourceTree = "<group>"; };
E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libmpdclient.2.dylib; path = libmpdclient/output/libmpdclient.2.dylib; sourceTree = "<group>"; };
@ -149,22 +183,52 @@
E41B22E921FB966C00D544F6 /* capabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = capabilities.h; sourceTree = "<group>"; };
E41B22EA21FB966C00D544F6 /* queue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = queue.h; sourceTree = "<group>"; };
E41B22EB21FB966C00D544F6 /* playlist.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = playlist.h; sourceTree = "<group>"; };
E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; };
E41E52FC223BF87300173814 /* MPDClient+Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Connection.swift"; sourceTree = "<group>"; };
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Transport.swift"; sourceTree = "<group>"; };
E41E5300223BF99300173814 /* MPDClient+Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Queue.swift"; sourceTree = "<group>"; };
E41E5302223BF9C300173814 /* MPDClient+Idle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Idle.swift"; sourceTree = "<group>"; };
E41E5304223BFB0700173814 /* MPDClient+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Error.swift"; sourceTree = "<group>"; };
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 /* AlbumArtService+Caching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumArtService+Caching.swift"; sourceTree = "<group>"; };
E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumArtService+Remote.swift"; sourceTree = "<group>"; };
E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumArtService+Filesystem.swift"; sourceTree = "<group>"; };
E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPrefsViewController.swift; sourceTree = "<group>"; };
E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaKeyTap.framework; path = Carthage/Build/Mac/MediaKeyTap.framework; sourceTree = "<group>"; };
E42410B52241B956005ED6DF /* MPDClient+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Database.swift"; sourceTree = "<group>"; };
E42A8F3922176D6400A13ED9 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
E42A8F3A22176D6400A13ED9 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
E435E3E1221CD4E200184CFC /* NSFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFont.swift; sourceTree = "<group>"; };
E435E3E3221CD75D00184CFC /* NSImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImage.swift; sourceTree = "<group>"; };
E450AD7D222620A10091BED3 /* AlbumItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = "<group>"; };
E450AD8522262AE60091BED3 /* SwiftyJSON.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyJSON.framework; path = Carthage/Build/Mac/SwiftyJSON.framework; sourceTree = "<group>"; };
E450AD8C22262C590091BED3 /* PromiseKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PromiseKit.framework; path = Carthage/Build/Mac/PromiseKit.framework; sourceTree = "<group>"; };
E450AD8E22262C620091BED3 /* PromiseKit.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = PromiseKit.framework.dSYM; path = Carthage/Build/Mac/PromiseKit.framework.dSYM; sourceTree = "<group>"; };
E450AD9022262C780091BED3 /* SwiftyJSON.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = SwiftyJSON.framework.dSYM; path = Carthage/Build/Mac/SwiftyJSON.framework.dSYM; sourceTree = "<group>"; };
E450AD9422262DF10091BED3 /* AlbumArtQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtQueue.swift; sourceTree = "<group>"; };
E450AD96222633920091BED3 /* Alamofire.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = Alamofire.framework.dSYM; path = Carthage/Build/Mac/Alamofire.framework.dSYM; sourceTree = "<group>"; };
E450AD97222633920091BED3 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = Carthage/Build/Mac/Alamofire.framework; sourceTree = "<group>"; };
E450AD9C2229B9050091BED3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PersephoneBridgingHeader.h; sourceTree = "<group>"; };
E450AD9F2229E7C90091BED3 /* PMKFoundation.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = PMKFoundation.framework.dSYM; path = Carthage/Build/Mac/PMKFoundation.framework.dSYM; sourceTree = "<group>"; };
E450ADA02229E7C90091BED3 /* PMKFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PMKFoundation.framework; path = Carthage/Build/Mac/PMKFoundation.framework; sourceTree = "<group>"; };
E45962C52241A78500FC1A1E /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
E465049921E94DF500A70F4C /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = "<group>"; };
E47E2FCB2220573500F747E6 /* MediaKeyTap.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = MediaKeyTap.framework.dSYM; path = Carthage/Build/Mac/MediaKeyTap.framework.dSYM; sourceTree = "<group>"; };
E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
E47E2FD222205D2500F747E6 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = "<group>"; };
E47E2FD4222071FD00F747E6 /* AlbumItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = "<group>"; };
E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumViewItem.swift; sourceTree = "<group>"; };
E47E2FD62220720300F747E6 /* AlbumItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumItemView.swift; sourceTree = "<group>"; };
E47E2FDC2220A6D100F747E6 /* Time.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = "<group>"; };
E47E2FE42220AA0700F747E6 /* AlbumViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumViewLayout.swift; sourceTree = "<group>"; };
E4928E0A2218D62A001D4BEA /* CGColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGColor.swift; sourceTree = "<group>"; };
E4A642D922090CBE00067D21 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtPrefsController.swift; sourceTree = "<group>"; };
E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; };
E4A83BF3222207D50098FED6 /* AlbumArtService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtService.swift; sourceTree = "<group>"; };
E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
E4C8B53D22349002009A20F3 /* Idle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Idle.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>"; };
E4E8CC932206097F0024217A /* NotificationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsController.swift; sourceTree = "<group>"; };
@ -183,6 +247,9 @@
files = (
E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */,
E421ACA3221F73C4008B2449 /* MediaKeyTap.framework in Frameworks */,
E450AD8622262AE60091BED3 /* SwiftyJSON.framework in Frameworks */,
E450ADA32229E7E00091BED3 /* PMKFoundation.framework in Frameworks */,
E450AD9222262C970091BED3 /* PromiseKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -229,6 +296,10 @@
E407861A2110CE6E006887B1 /* Persephone */ = {
isa = PBXGroup;
children = (
E450AD9E2229B9BC0091BED3 /* PersephoneBridgingHeader.h */,
E450AD8922262B420091BED3 /* Operations */,
E4A83BF2222207BE0098FED6 /* Services */,
E4A83BEC2221F5DD0098FED6 /* Preferences */,
E47E2FE32220AA0700F747E6 /* Layouts */,
E4F6B461221E124700ACF42A /* Models */,
E4F6B45E221E117600ACF42A /* DataSources */,
@ -273,6 +344,7 @@
E40FE71A221B904300A4223F /* NSEvent.swift */,
E435E3E1221CD4E200184CFC /* NSFont.swift */,
E435E3E3221CD75D00184CFC /* NSImage.swift */,
E450AD9C2229B9050091BED3 /* String.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -280,6 +352,15 @@
E408D3BC220E03D20006D9BE /* Extensions */ = {
isa = PBXGroup;
children = (
E41E52FC223BF87300173814 /* MPDClient+Connection.swift */,
E42410B52241B956005ED6DF /* MPDClient+Database.swift */,
E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */,
E41E5300223BF99300173814 /* MPDClient+Queue.swift */,
E41E5302223BF9C300173814 /* MPDClient+Idle.swift */,
E41E5304223BFB0700173814 /* MPDClient+Error.swift */,
E41E5306223C019100173814 /* MPDClient+Status.swift */,
E41E5308223C020400173814 /* MPDClient+Command.swift */,
E41E530A223C033700173814 /* MPDClient+Album.swift */,
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */,
);
path = Extensions;
@ -297,8 +378,16 @@
E41B22BE21FB6B3300D544F6 /* Frameworks */ = {
isa = PBXGroup;
children = (
E47E2FCB2220573500F747E6 /* MediaKeyTap.framework.dSYM */,
E450ADA02229E7C90091BED3 /* PMKFoundation.framework */,
E450AD9F2229E7C90091BED3 /* PMKFoundation.framework.dSYM */,
E450AD97222633920091BED3 /* Alamofire.framework */,
E450AD96222633920091BED3 /* Alamofire.framework.dSYM */,
E450AD8C22262C590091BED3 /* PromiseKit.framework */,
E450AD8E22262C620091BED3 /* PromiseKit.framework.dSYM */,
E450AD8522262AE60091BED3 /* SwiftyJSON.framework */,
E450AD9022262C780091BED3 /* SwiftyJSON.framework.dSYM */,
E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */,
E47E2FCB2220573500F747E6 /* MediaKeyTap.framework.dSYM */,
E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */,
);
name = Frameworks;
@ -355,6 +444,24 @@
path = mpd;
sourceTree = "<group>";
};
E41E530C223EF4BA00173814 /* Extensions */ = {
isa = PBXGroup;
children = (
E41E530D223EF4CF00173814 /* AlbumArtService+Caching.swift */,
E41E530F223EF6CE00173814 /* AlbumArtService+Remote.swift */,
E41E5311223EF74A00173814 /* AlbumArtService+Filesystem.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
E450AD8922262B420091BED3 /* Operations */ = {
isa = PBXGroup;
children = (
E450AD9422262DF10091BED3 /* AlbumArtQueue.swift */,
);
path = Operations;
sourceTree = "<group>";
};
E47E2FE32220AA0700F747E6 /* Layouts */ = {
isa = PBXGroup;
children = (
@ -366,14 +473,42 @@
E4A642DB220912FA00067D21 /* MPDClient */ = {
isa = PBXGroup;
children = (
E408D3BC220E03D20006D9BE /* Extensions */,
E41B22C521FB932700D544F6 /* MPDClient.swift */,
E408D3BC220E03D20006D9BE /* Extensions */,
E4D1B595220BA27C0026F233 /* Protocols */,
E4D1B594220BA2490026F233 /* Models */,
);
path = MPDClient;
sourceTree = "<group>";
};
E4A83BEC2221F5DD0098FED6 /* Preferences */ = {
isa = PBXGroup;
children = (
E4A83BED2221F5E60098FED6 /* Controllers */,
);
path = Preferences;
sourceTree = "<group>";
};
E4A83BED2221F5E60098FED6 /* Controllers */ = {
isa = PBXGroup;
children = (
E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */,
E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */,
E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */,
E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
E4A83BF2222207BE0098FED6 /* Services */ = {
isa = PBXGroup;
children = (
E41E530C223EF4BA00173814 /* Extensions */,
E4A83BF3222207D50098FED6 /* AlbumArtService.swift */,
);
path = Services;
sourceTree = "<group>";
};
E4D1B594220BA2490026F233 /* Models */ = {
isa = PBXGroup;
children = (
@ -381,6 +516,8 @@
E4A642D922090CBE00067D21 /* Status.swift */,
E4EB2378220F10B8008C70C0 /* Pair.swift */,
E4EB237A220F7CF1008C70C0 /* Album.swift */,
E4C8B53D22349002009A20F3 /* Idle.swift */,
E45962C52241A78500FC1A1E /* Command.swift */,
);
path = Models;
sourceTree = "<group>";
@ -396,9 +533,8 @@
E4D1B597220BA3A20026F233 /* Controllers */ = {
isa = PBXGroup;
children = (
E47E2FD4222071FD00F747E6 /* AlbumItem.swift */,
E47E2FD4222071FD00F747E6 /* AlbumViewItem.swift */,
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */,
E41EA46B221636AF0068EF46 /* PreferencesViewController.swift */,
E4E8CC932206097F0024217A /* NotificationsController.swift */,
E4E8CC912204F4B80024217A /* QueueViewController.swift */,
E465049921E94DF500A70F4C /* WindowController.swift */,
@ -411,7 +547,7 @@
isa = PBXGroup;
children = (
E40786212110CE70006887B1 /* Main.storyboard */,
E408D3C9220E341D0006D9BE /* AlbumItem.xib */,
E408D3C9220E341D0006D9BE /* AlbumViewItem.xib */,
);
path = Resources;
sourceTree = "<group>";
@ -431,6 +567,7 @@
E47E2FDC2220A6D100F747E6 /* Time.swift */,
E40F41F2221EDE27004B6CB8 /* Preferences.swift */,
E4F6B462221E125900ACF42A /* SongItem.swift */,
E450AD7D222620A10091BED3 /* AlbumItem.swift */,
);
path = Models;
sourceTree = "<group>";
@ -550,11 +687,15 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E450AD9122262C780091BED3 /* SwiftyJSON.framework.dSYM in Resources */,
E42A8F3B22176D6400A13ED9 /* LICENSE.md in Resources */,
E450AD98222633920091BED3 /* Alamofire.framework.dSYM in Resources */,
E40786202110CE70006887B1 /* Assets.xcassets in Resources */,
E42A8F3C22176D6400A13ED9 /* README.md in Resources */,
E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */,
E450AD8F22262C620091BED3 /* PromiseKit.framework.dSYM in Resources */,
E408D3CB220E341D0006D9BE /* AlbumViewItem.xib in Resources */,
E40786232110CE70006887B1 /* Main.storyboard in Resources */,
E450ADA12229E7C90091BED3 /* PMKFoundation.framework.dSYM in Resources */,
E47E2FCC2220573500F747E6 /* MediaKeyTap.framework.dSYM in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -600,34 +741,55 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E4A83BEF2221F8CF0098FED6 /* AlbumArtPrefsController.swift in Sources */,
E4F6B467221E233200ACF42A /* AlbumDataSource.swift in Sources */,
E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */,
E40FE71B221B904300A4223F /* NSEvent.swift in Sources */,
E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */,
E4C8B53E22349002009A20F3 /* Idle.swift in Sources */,
E4F6B460221E119B00ACF42A /* QueueDataSource.swift in Sources */,
E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */,
E41E5307223C019100173814 /* MPDClient+Status.swift in Sources */,
E41E5310223EF6CE00173814 /* AlbumArtService+Remote.swift in Sources */,
E41E530B223C033700173814 /* MPDClient+Album.swift in Sources */,
E42410B62241B956005ED6DF /* MPDClient+Database.swift in Sources */,
E4A642DA22090CBE00067D21 /* Status.swift in Sources */,
E47E2FD72220720300F747E6 /* AlbumItemView.swift in Sources */,
E450AD9522262DF10091BED3 /* AlbumArtQueue.swift in Sources */,
E41E52FD223BF87300173814 /* MPDClient+Connection.swift in Sources */,
E450AD7E222620A10091BED3 /* AlbumItem.swift in Sources */,
E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */,
E408D3B6220DD8970006D9BE /* Notification.swift in Sources */,
E45962C62241A78500FC1A1E /* Command.swift in Sources */,
E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */,
E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */,
E4F6B463221E125900ACF42A /* SongItem.swift in Sources */,
E4A83BF12221FAA00098FED6 /* PreferencesViewController.swift in Sources */,
E465049A21E94DF500A70F4C /* WindowController.swift in Sources */,
E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */,
E40F41F3221EDE27004B6CB8 /* Preferences.swift in Sources */,
E47E2FDD2220A6D100F747E6 /* Time.swift in Sources */,
E407861C2110CE6E006887B1 /* AppDelegate.swift in Sources */,
E41E5309223C020400173814 /* MPDClient+Command.swift in Sources */,
E47E2FE52220AA0700F747E6 /* AlbumViewLayout.swift in Sources */,
E41E52FF223BF95E00173814 /* MPDClient+Transport.swift in Sources */,
E47E2FD322205D2500F747E6 /* MainWindow.swift in Sources */,
E47E2FD122205C4600F747E6 /* MainSplitViewController.swift in Sources */,
E4E8CC9A22075D370024217A /* Song.swift in Sources */,
E41EA46C221636AF0068EF46 /* PreferencesViewController.swift in Sources */,
E41EA46C221636AF0068EF46 /* GeneralPrefsViewController.swift in Sources */,
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */,
E47E2FD5222071FD00F747E6 /* AlbumItem.swift in Sources */,
E4A83BF4222207D50098FED6 /* AlbumArtService.swift in Sources */,
E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */,
E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */,
E41E530E223EF4CF00173814 /* AlbumArtService+Caching.swift in Sources */,
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */,
E41E5312223EF74A00173814 /* AlbumArtService+Filesystem.swift in Sources */,
E41E5301223BF99300173814 /* MPDClient+Queue.swift in Sources */,
E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */,
E450AD9D2229B9050091BED3 /* String.swift in Sources */,
E41E5303223BF9C300173814 /* MPDClient+Idle.swift in Sources */,
E435E3E4221CD75D00184CFC /* NSImage.swift in Sources */,
E41E5305223BFB0700173814 /* MPDClient+Error.swift in Sources */,
E435E3E2221CD4E200184CFC /* NSFont.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -731,6 +893,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = Persephone/PersephoneBridgingHeader.h;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
@ -784,6 +947,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OBJC_BRIDGING_HEADER = Persephone/PersephoneBridgingHeader.h;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:../README.md">
</FileRef>
<FileRef
location = "self:">
</FileRef>

View File

@ -17,7 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate {
static let mpdClient = MPDClient(
withDelegate: NotificationsController()
)
func applicationDidFinishLaunching(_ aNotification: Notification) {
connect()
@ -26,6 +26,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate {
mediaKeyTap = MediaKeyTap(delegate: self)
mediaKeyTap?.start()
NotificationCenter.default.addObserver(
self,
selector: #selector(enableUpdateDatabaseMenuItem),
name: Notification.databaseUpdateFinished,
object: AppDelegate.mpdClient
)
}
func applicationWillTerminate(_ aNotification: Notification) {
@ -68,4 +75,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, MediaKeyTapDelegate {
func disconnect() {
AppDelegate.mpdClient.disconnect()
}
@IBAction func updateDatabase(_ sender: NSMenuItem) {
sender.isEnabled = false
AppDelegate.mpdClient.updateDatabase()
}
@objc func enableUpdateDatabaseMenuItem() {
updateDatabaseMenuItem?.isEnabled = true
}
@IBOutlet weak var updateDatabaseMenuItem: NSMenuItem!
}

View File

@ -32,4 +32,4 @@
"template-rendering-intent" : "original",
"preserves-vector-representation" : true
}
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "coverArtPreferencesIcon.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "coverArtPreferencesIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -11,6 +11,8 @@ import Cocoa
class AlbumViewController: NSViewController,
NSCollectionViewDelegate,
NSCollectionViewDelegateFlowLayout {
var preferences = Preferences()
let paddingWidth: CGFloat = 40
let gutterWidth: CGFloat = 20
@ -36,6 +38,8 @@ class AlbumViewController: NSViewController,
)
albumCollectionView.dataSource = dataSource
preferences.addObserver(self, forKeyPath: "mpdLibraryDir")
}
override func viewWillLayout() {
@ -53,11 +57,25 @@ class AlbumViewController: NSViewController,
layout.setScrollPosition()
}
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?
) {
switch keyPath {
case "mpdLibraryDir":
albumCollectionView.reloadData()
default:
break
}
}
@objc func updateAlbums(_ notification: Notification) {
guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album]
else { return }
dataSource.albums = albums
dataSource.albums = albums.map { AlbumItem(album: $0, coverArt: nil) }
albumCollectionView.reloadData()
}

View File

@ -1,5 +1,5 @@
//
// AlbumItem.swift
// AlbumViewItem.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/08.
@ -8,9 +8,9 @@
import Cocoa
class AlbumItem: NSCollectionViewItem {
class AlbumViewItem: NSCollectionViewItem {
var observer: NSKeyValueObservation?
var album: MPDClient.Album?
var album: AlbumItem?
override func viewDidLoad() {
super.viewDidLoad()
@ -27,10 +27,16 @@ class AlbumItem: NSCollectionViewItem {
}
}
func setAlbum(_ album: MPDClient.Album) {
func setAlbum(_ album: AlbumItem) {
self.album = album
albumTitle.stringValue = album.title
albumArtist.stringValue = album.artist
if let coverArt = album.coverArt {
albumCoverView.image = coverArt
} else {
albumCoverView.image = .defaultCoverArt
}
}
func setAppearance() {
@ -47,7 +53,7 @@ class AlbumItem: NSCollectionViewItem {
@IBAction func playAlbum(_ sender: Any) {
guard let album = album else { return }
AppDelegate.mpdClient.playAlbum(album)
AppDelegate.mpdClient.playAlbum(album.album)
}
@IBOutlet var albumCoverView: NSImageView!

View File

@ -36,6 +36,14 @@ class NotificationsController: MPDClientDelegate {
)
}
func willStartDatabaseUpdate(mpdClient: MPDClient) {
sendNotification(name: Notification.databaseUpdateStarted)
}
func didFinishDatabaseUpdate(mpdClient: MPDClient) {
sendNotification(name: Notification.databaseUpdateFinished)
}
func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song]) {
sendNotification(
name: Notification.queueChanged,

View File

@ -36,7 +36,7 @@ class QueueViewController: NSViewController,
let newQueuePos = queueView.selectedRow - 1
if newQueuePos >= 0 {
AppDelegate.mpdClient.playTrack(queuePos: newQueuePos)
AppDelegate.mpdClient.playTrack(at: newQueuePos)
}
}

View File

@ -36,6 +36,20 @@ class WindowController: NSWindowController {
object: AppDelegate.mpdClient
)
NotificationCenter.default.addObserver(
self,
selector: #selector(startDatabaseUpdatingIndicator),
name: Notification.databaseUpdateStarted,
object: AppDelegate.mpdClient
)
NotificationCenter.default.addObserver(
self,
selector: #selector(stopDatabaseUpdatingIndicator),
name: Notification.databaseUpdateFinished,
object: AppDelegate.mpdClient
)
trackProgress.font = .timerFont
trackRemaining.font = .timerFont
}
@ -132,6 +146,14 @@ class WindowController: NSWindowController {
setTimeRemaining()
}
@objc func startDatabaseUpdatingIndicator() {
databaseUpdatingIndicator.startAnimation(self)
}
@objc func stopDatabaseUpdatingIndicator() {
databaseUpdatingIndicator.stopAnimation(self)
}
func setTimeElapsed() {
guard let elapsedTimeMs = elapsedTimeMs else { return }
@ -194,4 +216,5 @@ class WindowController: NSWindowController {
@IBOutlet var trackProgress: NSTextField!
@IBOutlet var trackProgressBar: NSSlider!
@IBOutlet var trackRemaining: NSTextField!
@IBOutlet var databaseUpdatingIndicator: NSProgressIndicator!
}

View File

@ -9,19 +9,29 @@
import Cocoa
class AlbumDataSource: NSObject, NSCollectionViewDataSource {
var albums: [MPDClient.Album] = []
var albums: [AlbumItem] = []
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return albums.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(withIdentifier: .albumItem, for: indexPath)
guard let albumItem = item as? AlbumItem else { return item }
let item = collectionView.makeItem(withIdentifier: .albumViewItem, for: indexPath)
guard let albumViewItem = item as? AlbumViewItem else { return item }
albumItem.view.wantsLayer = true
albumItem.setAlbum(albums[indexPath.item])
albumViewItem.view.wantsLayer = true
albumViewItem.setAlbum(albums[indexPath.item])
return albumItem
if albums[indexPath.item].coverArt == nil {
AlbumArtService(album: albums[indexPath.item]).fetchAlbumArt { image in
self.albums[indexPath.item].coverArt = image
DispatchQueue.main.async {
collectionView.reloadItems(at: [indexPath])
}
}
}
return albumViewItem
}
}

View File

@ -14,9 +14,6 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
var queueIcon: NSImage? = nil
let playIcon = NSImage(named: "playButton")
let pauseIcon = NSImage(named: "pauseButton")
func updateQueue(_ queue: [MPDClient.Song]) {
queuePos = -1
@ -41,9 +38,9 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource {
func setQueueIcon(_ state: MPDClient.Status.State) {
switch state {
case .playing:
queueIcon = playIcon
queueIcon = .playIcon
case .paused:
queueIcon = pauseIcon
queueIcon = .pauseIcon
default:
queueIcon = nil
}

View File

@ -11,4 +11,26 @@ import Cocoa
extension NSImage {
static let playIcon = NSImage(named: "playButton")
static let pauseIcon = NSImage(named: "pauseButton")
static let defaultCoverArt = NSImage(named: "blankAlbum")
func toFitBox(size: NSSize) -> NSImage {
let newImage = NSImage(size: size)
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

@ -16,5 +16,5 @@ extension NSUserInterfaceItemIdentifier {
static let queueSongArtist = NSUserInterfaceItemIdentifier("songArtistCell")
static let queueSongTitle = NSUserInterfaceItemIdentifier("songTitleCell")
static let albumItem = NSUserInterfaceItemIdentifier("AlbumItem")
static let albumViewItem = NSUserInterfaceItemIdentifier("AlbumViewItem")
}

View File

@ -14,6 +14,8 @@ extension Notification {
static let stateChanged = Name("MPDClientStateChanged")
static let timeChanged = Name("MPDClientTimeChanged")
static let databaseUpdateStarted = Name("MPDClientDatabaseUpdateStarted")
static let databaseUpdateFinished = Name("MPDClientDatabaseUpdateFinished")
static let queueChanged = Name("MPDClientQueueChanged")
static let queuePosChanged = Name("MPDClientQueuePosChanged")
static let loadedAlbums = Name("MPDClientLoadedAlbums")

View File

@ -0,0 +1,24 @@
//
// String.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/01.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
extension String {
func sha1() -> String {
let data = self.data(using: String.Encoding.utf8)!
var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA1($0, CC_LONG(data.count), &digest)
}
let hexBytes = digest.map { String(format: "%02hhx", $0) }
return hexBytes.joined()
}
}

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.8.0</string>
<string>0.10.2a</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>

View File

@ -0,0 +1,99 @@
//
// MPDAlbum.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func fetchAllAlbums() {
enqueueCommand(command: .fetchAllAlbums)
}
func playAlbum(_ album: Album) {
enqueueCommand(command: .playAlbum, userData: ["album": album])
}
func getAlbumURI(for album: Album, callback: @escaping (String?) -> Void) {
enqueueCommand(
command: .getAlbumURI,
priority: .low,
userData: ["album": album, "callback": callback]
)
}
func sendPlayAlbum(_ album: Album) {
var songs: [Song] = []
mpd_run_clear(self.connection)
mpd_search_db_songs(self.connection, true)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist)
mpd_search_commit(self.connection)
while let mpdSong = mpd_recv_song(self.connection) {
songs.append(Song(mpdSong))
}
for song in songs {
mpd_run_add(self.connection, song.uri)
}
mpd_run_play_pos(self.connection, 0)
}
func allAlbums() {
var albums: [Album] = []
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_commit(self.connection)
while let mpdPair = mpd_recv_pair(self.connection) {
let pair = Pair(mpdPair)
switch pair.name {
case "AlbumArtist":
artist = pair.value
case "Album":
albums.append(Album(title: pair.value, artist: artist))
default:
break
}
mpd_return_pair(self.connection, pair.mpdPair)
}
self.delegate?.didLoadAlbums(mpdClient: self, albums: albums)
}
func albumURI(for album: Album, callback: (String?) -> Void) {
var songURI: String?
guard isConnected else { return }
mpd_search_db_songs(self.connection, true)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_TRACK, "1")
mpd_search_commit(self.connection)
while let mpdSong = mpd_recv_song(self.connection) {
let song = Song(mpdSong)
if songURI == nil {
songURI = song.uriString
}
}
callback(
songURI?
.split(separator: "/")
.dropLast()
.joined(separator: "/")
)
}
}

View File

@ -0,0 +1,79 @@
//
// CommandQueue.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
extension MPDClient {
func sendCommand(
command: Command,
userData: Dictionary<String, Any> = [:]
) {
switch command {
// Transport commands
case .prevTrack:
sendPreviousTrack()
case .nextTrack:
sendNextTrack()
case .stop:
sendStop()
case .playPause:
sendPlay()
case .seekCurrentSong:
guard let timeInSeconds = userData["timeInSeconds"] as? Float
else { return }
sendSeekCurrentSong(timeInSeconds: timeInSeconds)
// Database commands
case .updateDatabase:
sendUpdateDatabase()
// Status commands
case .fetchStatus:
sendRunStatus()
// Queue commands
case .fetchQueue:
sendFetchQueue()
case .playTrack:
guard let queuePos = userData["queuePos"] as? Int
else { return }
sendPlayTrack(at: queuePos)
// Album commands
case .fetchAllAlbums:
allAlbums()
case .playAlbum:
guard let album = userData["album"] as? Album else { return }
sendPlayAlbum(album)
case .getAlbumURI:
guard let album = userData["album"] as? Album,
let callback = userData["callback"] as? (String?) -> Void
else { return }
albumURI(for: album, callback: callback)
}
}
func enqueueCommand(
command: Command,
priority: BlockOperation.QueuePriority = .normal,
userData: Dictionary<String, Any> = [:]
) {
guard isConnected else { return }
noIdle()
let commandOperation = BlockOperation() { [unowned self] in
self.sendCommand(command: command, userData: userData)
self.idle()
}
commandOperation.queuePriority = priority
commandQueue.addOperation(commandOperation)
}
}

View File

@ -0,0 +1,50 @@
//
// Connection.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func connect(host: String, port: Int) {
commandQueue.addOperation { [unowned self] in
guard let connection = mpd_connection_new(host, UInt32(port), 10000),
mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS
else { return }
self.isConnected = true
guard let status = mpd_run_status(connection)
else { return }
self.connection = connection
self.status = Status(status)
self.fetchQueue()
self.fetchAllAlbums()
self.idle()
self.delegate?.didConnect(mpdClient: self)
self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state)
self.delegate?.didUpdateTime(mpdClient: self, total: self.status!.totalTime, elapsedMs: self.status!.elapsedTimeMs)
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song)
}
}
func disconnect() {
guard isConnected else { return }
noIdle()
commandQueue.addOperation { [unowned self] in
self.delegate?.willDisconnect(mpdClient: self)
mpd_connection_free(self.connection)
self.isConnected = false
}
}
}

View File

@ -0,0 +1,20 @@
//
// MPDClient+Database.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/19.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func updateDatabase() {
enqueueCommand(command: .updateDatabase)
}
func sendUpdateDatabase() {
mpd_run_update(connection, nil)
}
}

View File

@ -0,0 +1,24 @@
//
// Error.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func getLastErrorMessage() -> String? {
if mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS {
return nil
}
if let errorMessage = mpd_connection_get_error_message(connection) {
return String(cString: errorMessage)
}
return nil
}
}

View File

@ -0,0 +1,64 @@
//
// Idle.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func noIdle() {
if isIdle {
mpd_send_noidle(connection)
isIdle = false
}
}
func idle() {
if !self.isIdle && self.commandQueue.operationCount == 1 {
mpd_send_idle(self.connection)
self.isIdle = true
let result = mpd_recv_idle(self.connection, true)
self.handleIdleResult(result)
}
}
func handleIdleResult(_ result: mpd_idle) {
isIdle = false
let mpdIdle = Idle(rawValue: result.rawValue)
if mpdIdle.contains(.database) {
self.fetchAllAlbums()
}
if mpdIdle.contains(.queue) {
self.fetchQueue()
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
}
if mpdIdle.contains(.player) {
self.fetchStatus()
if let status = self.status {
self.delegate?.didUpdateState(mpdClient: self, state: status.state)
self.delegate?.didUpdateTime(mpdClient: self, total: status.totalTime, elapsedMs: status.elapsedTimeMs)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song)
}
}
if mpdIdle.contains(.update) {
self.fetchStatus()
if self.status?.updating ?? false {
self.delegate?.willStartDatabaseUpdate(mpdClient: self)
} else {
self.delegate?.didFinishDatabaseUpdate(mpdClient: self)
}
}
if !mpdIdle.isEmpty {
self.idle()
}
}
}

View File

@ -0,0 +1,34 @@
//
// Queue.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func fetchQueue() {
sendCommand(command: .fetchQueue)
}
func playTrack(at queuePos: Int) {
enqueueCommand(command: .playTrack, userData: ["queuePos": queuePos])
}
func sendPlayTrack(at queuePos: Int) {
mpd_run_play_pos(self.connection, UInt32(queuePos))
}
func sendFetchQueue() {
self.queue = []
mpd_send_list_queue_meta(connection)
while let mpdSong = mpd_recv_song(connection) {
let song = Song(mpdSong)
self.queue.append(song)
}
}
}

View File

@ -0,0 +1,21 @@
//
// Status.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func fetchStatus() {
sendCommand(command: .fetchStatus)
}
func sendRunStatus() {
guard let status = mpd_run_status(connection) else { return }
self.status = Status(status)
}
}

View File

@ -0,0 +1,67 @@
//
// Transport.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/15.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
extension MPDClient {
func playPause() {
enqueueCommand(command: .playPause)
}
func stop() {
enqueueCommand(command: .stop)
}
func prevTrack() {
enqueueCommand(command: .prevTrack)
}
func nextTrack() {
enqueueCommand(command: .nextTrack)
}
func seekCurrentSong(timeInSeconds: Float) {
enqueueCommand(
command: .seekCurrentSong,
userData: ["timeInSeconds": timeInSeconds]
)
}
func sendNextTrack() {
guard let state = status?.state,
state.isOneOf([.playing, .paused])
else { return }
mpd_run_next(connection)
}
func sendPreviousTrack() {
guard let state = status?.state,
state.isOneOf([.playing, .paused])
else { return }
mpd_run_previous(connection)
}
func sendStop() {
mpd_run_stop(connection)
}
func sendPlay() {
if status?.state == .stopped {
mpd_run_play(connection)
} else {
mpd_run_toggle_pause(connection)
}
}
func sendSeekCurrentSong(timeInSeconds: Float) {
mpd_run_seek_current(self.connection, timeInSeconds, false)
}
}

View File

@ -12,285 +12,16 @@ import mpdclient
class MPDClient {
var delegate: MPDClientDelegate?
private var connection: OpaquePointer?
private var isConnected: Bool = false
private var status: Status?
private var queue: [Song] = []
var connection: OpaquePointer?
var isConnected: Bool = false
var isIdle: Bool = false
var status: Status?
var queue: [Song] = []
private let commandQueue = DispatchQueue(label: "commandQueue")
enum Command {
case prevTrack, nextTrack, playPause, stop,
fetchStatus, fetchQueue, fetchAllAlbums
}
struct Idle: OptionSet {
let rawValue: UInt32
static let database = Idle(rawValue: 0x1)
static let storedPlaylist = Idle(rawValue: 0x2)
static let queue = Idle(rawValue: 0x4)
static let player = Idle(rawValue: 0x8)
static let mixer = Idle(rawValue: 0x10)
static let output = Idle(rawValue: 0x20)
static let options = Idle(rawValue: 0x40)
static let update = Idle(rawValue: 0x80)
static let sticker = Idle(rawValue: 0x100)
static let subscription = Idle(rawValue: 0x200)
static let message = Idle(rawValue: 0x400)
}
let commandQueue = OperationQueue()
init(withDelegate delegate: MPDClientDelegate?) {
commandQueue.maxConcurrentOperationCount = 1
self.delegate = delegate
}
func connect(host: String, port: Int) {
commandQueue.async { [unowned self] in
guard let connection = mpd_connection_new(host, UInt32(port), 10000),
mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS
else { return }
self.isConnected = true
guard let status = mpd_run_status(connection)
else { return }
self.connection = connection
self.status = Status(status)
self.fetchQueue()
self.fetchAllAlbums()
self.idle()
self.delegate?.didConnect(mpdClient: self)
self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state)
self.delegate?.didUpdateTime(mpdClient: self, total: self.status!.totalTime, elapsedMs: self.status!.elapsedTimeMs)
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song)
}
}
func disconnect() {
guard isConnected else { return }
noIdle()
commandQueue.async { [unowned self] in
self.delegate?.willDisconnect(mpdClient: self)
mpd_connection_free(self.connection)
self.isConnected = false
}
}
func fetchStatus() {
sendCommand(command: .fetchStatus)
}
func fetchQueue() {
sendCommand(command: .fetchQueue)
}
func fetchAllAlbums() {
sendCommand(command: .fetchAllAlbums)
}
func playPause() {
queueCommand(command: .playPause)
}
func stop() {
queueCommand(command: .stop)
}
func prevTrack() {
queueCommand(command: .prevTrack)
}
func nextTrack() {
queueCommand(command: .nextTrack)
}
func playTrack(queuePos: Int) {
guard isConnected else { return }
noIdle()
commandQueue.async { [unowned self] in
mpd_run_play_pos(self.connection, UInt32(queuePos))
}
idle()
}
func seekCurrentSong(timeInSeconds: Float) {
noIdle()
commandQueue.async { [unowned self] in
mpd_run_seek_current(self.connection, timeInSeconds, false)
}
idle()
}
func playAlbum(_ album: Album) {
noIdle()
commandQueue.async { [unowned self] in
var songs: [Song] = []
mpd_run_clear(self.connection)
mpd_search_db_songs(self.connection, true)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM, album.title)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ALBUM_ARTIST, album.artist)
mpd_search_commit(self.connection)
while let mpdSong = mpd_recv_song(self.connection) {
songs.append(Song(mpdSong))
}
for song in songs {
mpd_run_add(self.connection, song.uri)
}
mpd_run_play_pos(self.connection, 0)
}
idle()
}
func queueCommand(command: Command) {
guard isConnected else { return }
noIdle()
commandQueue.async { [unowned self] in
self.sendCommand(command: command)
}
idle()
}
func sendCommand(command: Command) {
switch command {
// Transport commands
case .prevTrack:
sendPreviousTrack()
case .nextTrack:
sendNextTrack()
case .stop:
sendStop()
case .playPause:
sendPlay()
case .fetchStatus:
sendRunStatus()
case .fetchQueue:
sendFetchQueue()
case .fetchAllAlbums:
allAlbums()
}
}
func sendNextTrack() {
guard let state = status?.state,
state.isOneOf([.playing, .paused])
else { return }
mpd_run_next(connection)
}
func sendPreviousTrack() {
guard let state = status?.state,
state.isOneOf([.playing, .paused])
else { return }
mpd_run_previous(connection)
}
func sendStop() {
mpd_run_stop(connection)
}
func sendPlay() {
if status?.state == .stopped {
mpd_run_play(connection)
} else {
mpd_run_toggle_pause(connection)
}
}
func sendRunStatus() {
guard let status = mpd_run_status(connection) else { return }
self.status = Status(status)
}
func sendFetchQueue() {
self.queue = []
mpd_send_list_queue_meta(connection)
while let mpdSong = mpd_recv_song(connection) {
let song = Song(mpdSong)
self.queue.append(song)
}
}
func allAlbums() {
var albums: [Album] = []
var artist: String = ""
mpd_search_db_tags(connection, MPD_TAG_ALBUM)
mpd_search_add_group_tag(connection, MPD_TAG_ALBUM_ARTIST)
mpd_search_commit(connection)
while let mpdPair = mpd_recv_pair(connection) {
let pair = Pair(mpdPair)
switch pair.name {
case "AlbumArtist":
artist = pair.value
case "Album":
albums.append(Album(title: pair.value, artist: artist))
default:
break
}
mpd_return_pair(connection, pair.mpdPair)
}
delegate?.didLoadAlbums(mpdClient: self, albums: albums)
}
func noIdle() {
mpd_send_noidle(connection)
}
func idle() {
commandQueue.async { [unowned self] in
mpd_send_idle(self.connection)
let result = mpd_recv_idle(self.connection, true)
self.handleIdleResult(result)
}
}
func handleIdleResult(_ result: mpd_idle) {
let mpdIdle = Idle(rawValue: result.rawValue)
if mpdIdle.contains(.queue) {
self.fetchQueue()
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
}
if mpdIdle.contains(.player) {
self.fetchStatus()
if let status = self.status {
self.delegate?.didUpdateState(mpdClient: self, state: status.state)
self.delegate?.didUpdateTime(mpdClient: self, total: status.totalTime, elapsedMs: status.elapsedTimeMs)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: status.song)
}
}
if !mpdIdle.isEmpty {
self.idle()
}
}
func getLastErrorMessage() -> String? {
if mpd_connection_get_error(connection) == MPD_ERROR_SUCCESS {
return nil
}
if let errorMessage = mpd_connection_get_error_message(connection) {
return String(cString: errorMessage)
}
return nil
}
}

View File

@ -0,0 +1,35 @@
//
// Command.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/19.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
extension MPDClient {
enum Command {
// Transport commands
case prevTrack
case nextTrack
case playPause
case stop
case seekCurrentSong
// Database commands
case updateDatabase
// Status commands
case fetchStatus
// Queue commands
case fetchQueue
case playTrack
// Album commands
case fetchAllAlbums
case playAlbum
case getAlbumURI
}
}

View File

@ -0,0 +1,27 @@
//
// Idle.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/09.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
extension MPDClient {
struct Idle: OptionSet {
let rawValue: UInt32
static let database = Idle(rawValue: 0x1)
static let storedPlaylist = Idle(rawValue: 0x2)
static let queue = Idle(rawValue: 0x4)
static let player = Idle(rawValue: 0x8)
static let mixer = Idle(rawValue: 0x10)
static let output = Idle(rawValue: 0x20)
static let options = Idle(rawValue: 0x40)
static let update = Idle(rawValue: 0x80)
static let sticker = Idle(rawValue: 0x100)
static let subscription = Idle(rawValue: 0x200)
static let message = Idle(rawValue: 0x400)
}
}

View File

@ -45,6 +45,10 @@ extension MPDClient {
return mpd_song_get_uri(mpdSong)
}
var uriString: String {
return String(cString: uri)
}
func getTag(_ tagType: TagType) -> String {
let mpdTagType = mpd_tag_type(rawValue: Int32(tagType.rawValue))

View File

@ -49,5 +49,11 @@ extension MPDClient {
var song: Int {
return Int(mpd_status_get_song_pos(mpdStatus))
}
var updating: Bool {
let updating = mpd_status_get_update_id(mpdStatus)
return updating > 0
}
}
}

View File

@ -15,6 +15,9 @@ protocol MPDClientDelegate {
func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State)
func didUpdateTime(mpdClient: MPDClient, total: UInt, elapsedMs: UInt)
func willStartDatabaseUpdate(mpdClient: MPDClient)
func didFinishDatabaseUpdate(mpdClient: MPDClient)
func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song])
func didUpdateQueuePos(mpdClient: MPDClient, song: Int)

View File

@ -0,0 +1,26 @@
//
// AlbumItem.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/26.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
struct AlbumItem {
var album: MPDClient.Album
var coverArt: NSImage?
var title: String {
return album.title
}
var artist: String {
return album.artist
}
var hash: String {
return "\(title) - \(artist)".sha1()
}
}

View File

@ -9,6 +9,10 @@
import Foundation
struct Preferences {
let mpdHostDefault = "127.0.0.1"
let mpdPortDefault = 6600
let mpdLibraryDirDefault = "~/Music"
let preferences = UserDefaults.standard
var mpdHost: String? {
@ -20,6 +24,10 @@ struct Preferences {
}
}
var mpdHostOrDefault: String {
return mpdHost ?? mpdHostDefault
}
var mpdPort: Int? {
get {
return preferences.value(forKey: "mpdPort") as? Int
@ -33,12 +41,34 @@ struct Preferences {
}
}
var mpdHostOrDefault: String {
return mpdHost ?? "127.0.0.1"
var mpdPortOrDefault: Int {
return mpdPort ?? mpdPortDefault
}
var mpdPortOrDefault: Int {
return mpdPort ?? 6600
var mpdLibraryDir: String? {
get {
return preferences.string(forKey: "mpdLibraryDir")
}
set {
preferences.set(newValue, forKey: "mpdLibraryDir")
}
}
var mpdLibraryDirOrDefault: String {
return mpdLibraryDir ?? mpdLibraryDirDefault
}
var expandedMpdLibraryDir: String {
return NSString(string: mpdLibraryDirOrDefault).expandingTildeInPath
}
var fetchMissingArtworkFromInternet: Bool {
get {
return preferences.bool(forKey: "fetchMissingArtworkFromInternet")
}
set {
preferences.set(newValue, forKey: "fetchMissingArtworkFromInternet")
}
}
func addObserver(_ observer: NSObject, forKeyPath keyPath: String) {

View File

@ -0,0 +1,23 @@
//
// AlbumArtOperationQueue.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/26.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class AlbumArtQueue {
static let shared = AlbumArtQueue()
let queue = DispatchQueue(label: "AlbumArtQueue")
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,15 @@
//
// PersephoneBridgingHeader.h
// Persephone
//
// Created by Daniel Barber on 2019/3/01.
// Copyright © 2019 Dan Barber. All rights reserved.
//
#ifndef PersephoneBridgingHeader_h
#define PersephoneBridgingHeader_h
#endif /* PersephoneBridgingHeader_h */
#import <CommonCrypto/CommonCrypto.h>

View File

@ -0,0 +1,53 @@
//
// AlbumArtPrefsController.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/23.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class AlbumArtPrefsController: NSViewController {
var preferences = Preferences()
override func viewDidLoad() {
super.viewDidLoad()
if let mpdLibraryDir = preferences.mpdLibraryDir {
mpdLibraryDirField.stringValue = mpdLibraryDir
}
if preferences.fetchMissingArtworkFromInternet {
fetchMissingArtworkFromInternet.state = .on
} else {
fetchMissingArtworkFromInternet.state = .off
}
preferredContentSize = NSMakeSize(view.frame.size.width, view.frame.size.height)
}
override func viewDidAppear() {
super.viewDidAppear()
guard let title = title
else { return }
self.parent?.view.window?.title = title
}
@IBAction func updateMpdLibraryDir(_ sender: NSTextField) {
preferences.mpdLibraryDir = sender.stringValue
}
@IBOutlet var mpdLibraryDirField: NSTextField!
@IBAction func updateFetchMissingArtworkFromInternet(_ sender: NSButton) {
if sender.state == .on {
preferences.fetchMissingArtworkFromInternet = true
} else {
preferences.fetchMissingArtworkFromInternet = false
}
}
@IBOutlet var fetchMissingArtworkFromInternet: NSButton!
}

View File

@ -8,7 +8,7 @@
import Cocoa
class PreferencesViewController: NSViewController {
class GeneralPrefsViewController: NSViewController {
var preferences = Preferences()
override func viewDidLoad() {
@ -21,6 +21,16 @@ class PreferencesViewController: NSViewController {
if let mpdPort = preferences.mpdPort {
mpdPortField.stringValue = "\(mpdPort)"
}
preferredContentSize = NSMakeSize(view.frame.size.width, view.frame.size.height)
}
override func viewDidAppear() {
super.viewDidAppear()
guard let title = title
else { return }
self.parent?.view.window?.title = title
}
@IBAction func updateMpdHost(_ sender: NSTextField) {

View File

@ -0,0 +1,59 @@
//
// PreferencesController.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/23.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class PreferencesViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
super.viewDidLoad()
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.25
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(
from: fromViewController,
to: toViewController,
options: [.crossfade, .allowUserInteraction],
completionHandler: completion
)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title,
let window = view.window
else { return }
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
} else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}

View File

@ -0,0 +1,20 @@
//
// PreferencesWindowController.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/09.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class PreferencesWindowController: NSWindowController, NSWindowDelegate {
override func windowDidLoad() {
super.windowDidLoad()
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
self.window?.orderOut(sender)
return false
}
}

View File

@ -6,7 +6,7 @@
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AlbumItem" customModule="Persephone" customModuleProvider="target">
<customObject id="-2" userLabel="File's Owner" customClass="AlbumViewItem" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="albumArtist" destination="5Uu-j1-qyT" id="2Et-tX-InT"/>
<outlet property="albumCoverView" destination="Kfb-8f-ean" id="CXx-gB-gz8"/>
@ -76,7 +76,7 @@
</connections>
<point key="canvasLocation" x="-22" y="125.5"/>
</customView>
<collectionViewItem id="Qgu-aI-55A" customClass="AlbumItem" customModule="Persephone" customModuleProvider="target"/>
<collectionViewItem id="Qgu-aI-55A" customClass="AlbumViewItem" customModule="Persephone" customModuleProvider="target"/>
</objects>
<resources>
<image name="blankAlbum" width="128" height="128"/>

View File

@ -62,6 +62,20 @@
</items>
</menu>
</menuItem>
<menuItem title="Database" id="usv-UH-vkC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Database" autoenablesItems="NO" id="rFP-zL-1X4">
<items>
<menuItem title="Update" keyEquivalent="u" id="EJg-93-1F6">
<attributedString key="attributedTitle"/>
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="updateDatabase:" target="Voe-Tx-rLC" id="FO1-Ge-TUL"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
@ -105,7 +119,11 @@
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Persephone" customModuleProvider="target"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="updateDatabaseMenuItem" destination="EJg-93-1F6" id="gMf-SQ-lyI"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
@ -185,9 +203,17 @@
</textFieldCell>
</textField>
</toolbarItem>
<toolbarItem implicitItemIdentifier="6B3CE282-B6C1-48BF-8962-1A02892D8DF8" label="" paletteLabel="" tag="-1" sizingBehavior="auto" id="fw4-Lp-bWJ">
<nil key="toolTip"/>
<progressIndicator key="view" identifier="databaseUpdatingIndicator" wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" id="LpV-iM-o6t">
<rect key="frame" x="0.0" y="14" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="p3r-ty-Pxf"/>
<toolbarItem reference="fw4-Lp-bWJ"/>
<toolbarItem reference="9ol-aR-mzv"/>
<toolbarItem reference="n52-8S-6kR"/>
<toolbarItem reference="s1h-EC-nvL"/>
@ -200,6 +226,7 @@
</connections>
</window>
<connections>
<outlet property="databaseUpdatingIndicator" destination="LpV-iM-o6t" id="y0T-eR-ygY"/>
<outlet property="trackProgress" destination="kx6-xm-TAN" id="XDv-Th-Agj"/>
<outlet property="trackProgressBar" destination="KMy-xf-rmN" id="a67-JU-cyQ"/>
<outlet property="trackRemaining" destination="9WZ-ij-lrb" id="0pH-d7-wvD"/>
@ -239,7 +266,7 @@
<!--Window Controller-->
<scene sceneID="Rpk-bo-5kf">
<objects>
<windowController id="xYu-7w-E5x" sceneMemberID="viewController">
<windowController id="xYu-7w-E5x" customClass="PreferencesWindowController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<window key="window" title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="3FN-my-6kU">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<rect key="contentRect" x="245" y="301" width="416" height="100"/>
@ -249,24 +276,107 @@
</connections>
</window>
<connections>
<segue destination="nYi-sw-ZNp" kind="relationship" relationship="window.shadowedContentViewController" id="607-3F-gJf"/>
<segue destination="zhe-qh-Mal" kind="relationship" relationship="window.shadowedContentViewController" id="iWi-v3-HxM"/>
</connections>
</windowController>
<customObject id="0sd-8B-etN" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="915" y="-89"/>
<point key="canvasLocation" x="915" y="-155"/>
</scene>
<!--Preferences View Controller-->
<!--General-->
<scene sceneID="5er-B6-hoB">
<objects>
<tabViewController title="General" selectedTabViewItemIndex="0" tabStyle="toolbar" id="zhe-qh-Mal" customClass="PreferencesViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<tabViewItems>
<tabViewItem label="General" identifier="generalPreferencesTab" image="NSPreferencesGeneral" id="kn0-fa-vM3"/>
<tabViewItem label="Album Art" identifier="albumArtPreferencesTab" image="coverArtPreferencesIcon" id="4Lj-dz-bOK"/>
</tabViewItems>
<viewControllerTransitionOptions key="transitionOptions" allowUserInteraction="YES"/>
<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"/>
<connections>
<outlet property="delegate" destination="zhe-qh-Mal" id="LUL-qN-JlP"/>
</connections>
</tabView>
<connections>
<outlet property="tabView" destination="6dC-M0-oC5" id="jFQ-3f-s5E"/>
<segue destination="nYi-sw-ZNp" kind="relationship" relationship="tabItems" id="Jr4-ql-vhk"/>
<segue destination="3C9-vU-zjZ" kind="relationship" relationship="tabItems" id="zOQ-NT-Tof"/>
</connections>
</tabViewController>
<customObject id="XtF-QO-9W0" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="916" y="236"/>
</scene>
<!--Album Art-->
<scene sceneID="pQx-0G-WVt">
<objects>
<viewController title="Album Art" id="3C9-vU-zjZ" customClass="AlbumArtPrefsController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="PyK-v2-kus">
<rect key="frame" x="0.0" y="0.0" width="524" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zZn-Rm-e1f">
<rect key="frame" x="53" y="62" width="104" height="17"/>
<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="58" width="288" height="22"/>
<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">
<rect key="frame" x="160" y="27" width="265" height="18"/>
<buttonCell key="cell" type="check" title="Fetch missing artwork from MusicBrainz" bezelStyle="regularSquare" imagePosition="left" inset="2" id="LpD-Ew-HMd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="updateFetchMissingArtworkFromInternet:" target="3C9-vU-zjZ" id="I7x-9V-xJr"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="zZn-Rm-e1f" firstAttribute="leading" secondItem="PyK-v2-kus" secondAttribute="leading" constant="55" id="F9T-mO-lMa"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="top" secondItem="PyK-v2-kus" secondAttribute="top" constant="20" symbolic="YES" id="NSz-Xf-KZS"/>
<constraint firstAttribute="trailing" secondItem="gDk-ca-eOa" secondAttribute="trailing" constant="74" id="QMb-TP-IdQ"/>
<constraint firstItem="pRL-MG-1Be" firstAttribute="top" secondItem="gDk-ca-eOa" secondAttribute="bottom" constant="15" id="bD6-hA-Wz5"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="leading" secondItem="zZn-Rm-e1f" secondAttribute="trailing" constant="7" id="oZ5-45-Pe5"/>
<constraint firstItem="gDk-ca-eOa" firstAttribute="leading" secondItem="pRL-MG-1Be" secondAttribute="leading" id="sBG-Yb-ii6"/>
<constraint firstItem="zZn-Rm-e1f" firstAttribute="top" secondItem="PyK-v2-kus" secondAttribute="top" constant="21" id="wHW-jd-TaG"/>
</constraints>
</view>
<connections>
<outlet property="fetchMissingArtworkFromInternet" destination="pRL-MG-1Be" id="Xcp-sb-iZm"/>
<outlet property="mpdLibraryDirField" destination="gDk-ca-eOa" id="myi-BQ-0NS"/>
</connections>
</viewController>
<customObject id="KzD-E3-lpA" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1626" y="339"/>
</scene>
<!--General-->
<scene sceneID="xTC-Y5-Agk">
<objects>
<viewController id="nYi-sw-ZNp" customClass="PreferencesViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<viewController title="General" id="nYi-sw-ZNp" customClass="GeneralPrefsViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="Uwt-Lw-ILP">
<rect key="frame" x="0.0" y="0.0" width="420" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wPm-sJ-e9E">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wPm-sJ-e9E">
<rect key="frame" x="162" y="58" width="184" height="22"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" alignment="left" placeholderString="127.0.0.1" drawsBackground="YES" id="MSX-mn-2ma">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -276,9 +386,11 @@
<action selector="updateMpdHost:" target="nYi-sw-ZNp" id="Y7x-N9-6ag"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="IbX-oV-soD">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="IbX-oV-soD">
<rect key="frame" x="162" y="26" width="80" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<constraints>
<constraint firstAttribute="width" constant="80" id="WAb-PB-Z1Y"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="6600" drawsBackground="YES" id="i9j-nB-bqq">
<numberFormatter key="formatter" formatterBehavior="custom10_4" numberStyle="decimal" usesGroupingSeparator="NO" minimumIntegerDigits="1" maximumIntegerDigits="2000000000" maximumFractionDigits="3" id="UiQ-gi-Hbp">
<real key="minimum" value="0.0"/>
@ -292,18 +404,19 @@
<action selector="updateMpdPort:" target="nYi-sw-ZNp" id="406-EC-aO2"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kvB-99-zwY">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kvB-99-zwY">
<rect key="frame" x="76" y="62" width="80" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<constraints>
<constraint firstAttribute="width" constant="76" id="4VR-n5-bGr"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Server Host:" id="AVi-g9-Irz">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AU9-wN-kbU">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AU9-wN-kbU">
<rect key="frame" x="77" y="30" width="77" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Server Port:" id="DgA-xT-2ir">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -311,6 +424,18 @@
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="wPm-sJ-e9E" firstAttribute="top" secondItem="Uwt-Lw-ILP" secondAttribute="top" constant="20" symbolic="YES" id="1u9-dh-XrA"/>
<constraint firstItem="wPm-sJ-e9E" firstAttribute="leading" secondItem="kvB-99-zwY" secondAttribute="trailing" constant="8" symbolic="YES" id="655-3c-mJH"/>
<constraint firstItem="AU9-wN-kbU" firstAttribute="top" secondItem="kvB-99-zwY" secondAttribute="bottom" constant="15" id="FPR-mZ-SUo"/>
<constraint firstItem="IbX-oV-soD" firstAttribute="top" secondItem="wPm-sJ-e9E" secondAttribute="bottom" constant="10" symbolic="YES" id="G7a-VX-OLJ"/>
<constraint firstItem="kvB-99-zwY" firstAttribute="top" secondItem="Uwt-Lw-ILP" secondAttribute="top" constant="21" id="ITg-Ag-wpl"/>
<constraint firstItem="kvB-99-zwY" firstAttribute="leading" secondItem="Uwt-Lw-ILP" secondAttribute="leading" constant="78" id="Kw0-2i-oST"/>
<constraint firstItem="IbX-oV-soD" firstAttribute="leading" secondItem="AU9-wN-kbU" secondAttribute="trailing" constant="10" id="Y6y-25-qRM"/>
<constraint firstAttribute="trailing" secondItem="wPm-sJ-e9E" secondAttribute="trailing" constant="74" id="cX3-Sz-RXR"/>
<constraint firstItem="kvB-99-zwY" firstAttribute="centerX" secondItem="AU9-wN-kbU" secondAttribute="centerX" id="fdg-fL-UzL"/>
<constraint firstItem="wPm-sJ-e9E" firstAttribute="leading" secondItem="IbX-oV-soD" secondAttribute="leading" id="qBw-Ri-Z4l"/>
</constraints>
</view>
<connections>
<outlet property="mpdHostField" destination="wPm-sJ-e9E" id="PR7-oL-tVQ"/>
@ -319,7 +444,7 @@
</viewController>
<customObject id="lzf-yO-5pP" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="917" y="217"/>
<point key="canvasLocation" x="1574" y="69"/>
</scene>
<!--Queue View Controller-->
<scene sceneID="QcX-dC-cTZ">
@ -478,7 +603,7 @@
</viewController>
<customObject id="du4-e9-TfX" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="848" y="635"/>
<point key="canvasLocation" x="834" y="679"/>
</scene>
<!--Album View Controller-->
<scene sceneID="7Ua-Hj-zWt">
@ -494,7 +619,7 @@
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<collectionView id="lfq-AB-epE">
<collectionView identifier="albumCollectionView" id="lfq-AB-epE">
<rect key="frame" x="0.0" y="0.0" width="450" height="158"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
<collectionViewLayout key="collectionViewLayout" id="YE8-sD-l5P" customClass="AlbumViewLayout" customModule="Persephone" customModuleProvider="target"/>
@ -533,6 +658,8 @@
</scene>
</scenes>
<resources>
<image name="NSPreferencesGeneral" width="32" height="32"/>
<image name="coverArtPreferencesIcon" width="32" height="32"/>
<image name="nextTrackButton" width="17" height="17"/>
<image name="playButton" width="17" height="17"/>
<image name="prevTrackButton" width="17" height="17"/>

View File

@ -0,0 +1,54 @@
//
// AlbumArtService.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/23.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
import PromiseKit
class AlbumArtService {
var preferences = Preferences()
let album: AlbumItem
let cachedArtworkSize = 180
let cachedArtworkQuality: CGFloat = 0.5
var session = URLSession(configuration: .default)
let cacheQueue = DispatchQueue(label: "albumArtCacheQueue")
init(album: AlbumItem) {
self.album = album
}
func fetchAlbumArt(callback: @escaping (_ image: NSImage?) -> Void) {
cacheQueue.async {
firstly {
self.getCachedArtwork()
}.then { artwork -> Promise<NSImage?> in
artwork.map(Promise.value) ?? self.cacheIfNecessary(self.getArtworkFromFilesystem())
}.then { artwork -> Promise<NSImage?> in
artwork.map(Promise.value) ?? self.cacheIfNecessary(self.getArtworkFromMusicBrainz().map(Optional.some))
}.tap { result in
switch result {
case .fulfilled(nil), .rejected(MusicBrainzError.noArtworkAvailable):
self.cacheArtwork(data: Data())
default:
break
}
}.recover { error in
.value(nil)
}.done(callback)
}
}
func cacheIfNecessary(_ promise: Promise<NSImage?>) -> Promise<NSImage?> {
return promise.get { image in
if let data = image?.jpegData(compressionQuality: self.cachedArtworkQuality) {
self.cacheArtwork(data: data)
}
}
}
}

View File

@ -0,0 +1,35 @@
//
// AlbumArtService+Caching.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/17.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
import PromiseKit
extension AlbumArtService {
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
let cacheFilePath = AlbumArtService.cacheDir.appendingPathComponent(album.hash).path
let data = FileManager.default.contents(atPath: cacheFilePath)
let image = data.flatMap(NSImage.init(data:))
seal.fulfill(image)
}
}
func cacheArtwork(data: Data?) {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(bundleIdentifier)
else { return }
let cacheFilePath = cacheDir.appendingPathComponent(album.hash).path
FileManager.default.createFile(atPath: cacheFilePath, contents: data, attributes: nil)
}
}

View File

@ -0,0 +1,49 @@
//
// AlbumArtService+Filesystem.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/17.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
import PromiseKit
extension AlbumArtService {
func getArtworkFromFilesystem() -> Promise<NSImage?> {
let coverArtFilenames = [
"folder.jpg",
"cover.jpg",
"\(album.artist) - \(album.title).jpg"
]
return getAlbumURI().map { albumURI in
let musicDir = self.preferences.expandedMpdLibraryDir
return coverArtFilenames
.lazy
.map { "\(musicDir)/\($0)" }
.compactMap(self.tryImage)
.first
}
}
func getAlbumURI() -> Promise<String> {
return Promise { seal in
AppDelegate.mpdClient.getAlbumURI(for: album.album, callback: seal.fulfill)
}
.compactMap { $0 }
}
func tryImage(_ filePath: String) -> NSImage? {
guard let data = FileManager.default.contents(atPath: filePath),
let image = NSImage(data: data)
else { return nil }
let imageThumb = image.toFitBox(
size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize)
)
return imageThumb
}
}

View File

@ -0,0 +1,54 @@
//
// AlbumArtService+Remote.swift
// Persephone
//
// Created by Daniel Barber on 2019/3/17.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
import SwiftyJSON
import PromiseKit
import PMKFoundation
extension AlbumArtService {
enum MusicBrainzError: Error {
case noArtworkAvailable
}
func getRemoteArtwork() -> Promise<NSImage> {
return Promise { seal in
let albumArtWorkItem = DispatchWorkItem {
self.getArtworkFromMusicBrainz().pipe(to: seal.resolve)
}
AlbumArtQueue.shared.addToQueue(workItem: albumArtWorkItem)
}
}
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 URLSession.shared.dataTask(.promise, with: search.url!).validate()
.compactMap {
JSON($0.data)
}.compactMap {
$0["releases"][0]["id"].string
}.compactMap {
URLComponents(string: "https://coverartarchive.org/release/\($0)/front-500")?.url
}.then { (url: URL?) -> Promise<(data: Data, response: URLResponse)> in
return URLSession.shared.dataTask(.promise, with: url!).validate()
}.compactMap {
NSImage(data: $0.data)?.toFitBox(
size: NSSize(width: self.cachedArtworkSize, height: self.cachedArtworkSize)
)
}.recover { error -> Promise<NSImage> in
if case PMKHTTPError.badStatusCode(404, _, _) = error {
throw MusicBrainzError.noArtworkAvailable
} else {
throw error
}
}
}
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 784 KiB

10
bin/release Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
set -o pipefail && \
xcodebuild \
-project Persephone.xcodeproj \
-scheme Persephone \
-destination platform\=macOS build \
-configuration Release \
| xcpretty