diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 4847861..557b937 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -22,6 +22,14 @@ 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 */; }; + 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 */; }; 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, ); }; }; @@ -55,6 +63,8 @@ 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 */; }; @@ -168,6 +178,14 @@ E41B22E921FB966C00D544F6 /* capabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = capabilities.h; sourceTree = ""; }; E41B22EA21FB966C00D544F6 /* queue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = queue.h; sourceTree = ""; }; E41B22EB21FB966C00D544F6 /* playlist.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = playlist.h; sourceTree = ""; }; + E41E52FC223BF87300173814 /* MPDClient+Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Connection.swift"; sourceTree = ""; }; + E41E52FE223BF95E00173814 /* MPDClient+Transport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Transport.swift"; sourceTree = ""; }; + E41E5300223BF99300173814 /* MPDClient+Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Queue.swift"; sourceTree = ""; }; + E41E5302223BF9C300173814 /* MPDClient+Idle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Idle.swift"; sourceTree = ""; }; + E41E5304223BFB0700173814 /* MPDClient+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Error.swift"; sourceTree = ""; }; + E41E5306223C019100173814 /* MPDClient+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Status.swift"; sourceTree = ""; }; + E41E5308223C020400173814 /* MPDClient+Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Command.swift"; sourceTree = ""; }; + E41E530A223C033700173814 /* MPDClient+Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPDClient+Album.swift"; sourceTree = ""; }; E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPrefsViewController.swift; sourceTree = ""; }; E421ACA1221F73B8008B2449 /* MediaKeyTap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaKeyTap.framework; path = Carthage/Build/Mac/MediaKeyTap.framework; sourceTree = ""; }; E42A8F3922176D6400A13ED9 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; @@ -199,6 +217,8 @@ E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtPrefsController.swift; sourceTree = ""; }; E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; E4A83BF3222207D50098FED6 /* AlbumArtService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtService.swift; sourceTree = ""; }; + E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; + E4C8B53D22349002009A20F3 /* Idle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Idle.swift; sourceTree = ""; }; E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = ""; }; E4E8CC932206097F0024217A /* NotificationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsController.swift; sourceTree = ""; }; @@ -322,6 +342,14 @@ E408D3BC220E03D20006D9BE /* Extensions */ = { isa = PBXGroup; children = ( + E41E52FC223BF87300173814 /* MPDClient+Connection.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; @@ -424,8 +452,8 @@ E4A642DB220912FA00067D21 /* MPDClient */ = { isa = PBXGroup; children = ( - E408D3BC220E03D20006D9BE /* Extensions */, E41B22C521FB932700D544F6 /* MPDClient.swift */, + E408D3BC220E03D20006D9BE /* Extensions */, E4D1B595220BA27C0026F233 /* Protocols */, E4D1B594220BA2490026F233 /* Models */, ); @@ -446,6 +474,7 @@ E41EA46B221636AF0068EF46 /* GeneralPrefsViewController.swift */, E4A83BEE2221F8CF0098FED6 /* AlbumArtPrefsController.swift */, E4A83BF02221FAA00098FED6 /* PreferencesViewController.swift */, + E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */, ); path = Controllers; sourceTree = ""; @@ -465,6 +494,7 @@ E4A642D922090CBE00067D21 /* Status.swift */, E4EB2378220F10B8008C70C0 /* Pair.swift */, E4EB237A220F7CF1008C70C0 /* Album.swift */, + E4C8B53D22349002009A20F3 /* Idle.swift */, ); path = Models; sourceTree = ""; @@ -693,10 +723,15 @@ 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 */, + E41E530B223C033700173814 /* MPDClient+Album.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 */, @@ -709,7 +744,9 @@ 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 */, @@ -719,9 +756,12 @@ E47E2FD5222071FD00F747E6 /* AlbumViewItem.swift in Sources */, E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */, E4E8CC922204F4B80024217A /* QueueViewController.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; diff --git a/Persephone.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Persephone.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 31c29ea..919434a 100644 --- a/Persephone.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Persephone.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -1,9 +1,6 @@ - - diff --git a/Persephone/MPDClient/Extensions/MPDClient+Album.swift b/Persephone/MPDClient/Extensions/MPDClient+Album.swift new file mode 100644 index 0000000..12240bf --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Album.swift @@ -0,0 +1,94 @@ +// +// 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() { + queueCommand(command: .fetchAllAlbums) + } + + func playAlbum(_ album: Album) { + queueCommand(command: .playAlbum, userData: ["album": album]) + } + + func getAlbumURI(for album: Album) { + queueCommand(command: .getAlbumURI, userData: ["album": album]) + } + + 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) -> String? { + var songURI: String? + + guard isConnected else { return nil } + + print("Getting URI") + 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) + print("Performed search") + + while let mpdSong = mpd_recv_song(self.connection) { + let song = Song(mpdSong) + print(song) + + if songURI == nil { + songURI = song.uriString + } + } + print("Got URI") + + return songURI + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Command.swift b/Persephone/MPDClient/Extensions/MPDClient+Command.swift new file mode 100644 index 0000000..8a857ef --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Command.swift @@ -0,0 +1,45 @@ +// +// 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 = [:] + ) { + switch command { + + // Transport commands + case .prevTrack: + sendPreviousTrack() + case .nextTrack: + sendNextTrack() + case .stop: + sendStop() + case .playPause: + sendPlay() + + // Status commands + case .fetchStatus: + sendRunStatus() + case .fetchQueue: + sendFetchQueue() + + // 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 else { return } + _ = getAlbumURI(for: album) + } + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Connection.swift b/Persephone/MPDClient/Extensions/MPDClient+Connection.swift new file mode 100644 index 0000000..d345863 --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Connection.swift @@ -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 + } + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Error.swift b/Persephone/MPDClient/Extensions/MPDClient+Error.swift new file mode 100644 index 0000000..2c97286 --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Error.swift @@ -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 + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Idle.swift b/Persephone/MPDClient/Extensions/MPDClient+Idle.swift new file mode 100644 index 0000000..60e6df5 --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Idle.swift @@ -0,0 +1,56 @@ +// +// 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() { + let idleOperation = BlockOperation { + if !self.isIdle && self.commandsQueued == 0 { + mpd_send_idle(self.connection) + self.isIdle = true + + let result = mpd_recv_idle(self.connection, true) + self.handleIdleResult(result) + } + } + idleOperation.queuePriority = .veryLow + commandQueue.addOperation(idleOperation) + } + + func handleIdleResult(_ result: mpd_idle) { + isIdle = false + + 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() + } + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Queue.swift b/Persephone/MPDClient/Extensions/MPDClient+Queue.swift new file mode 100644 index 0000000..9224395 --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Queue.swift @@ -0,0 +1,38 @@ +// +// 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(queuePos: Int) { + guard isConnected else { return } + + noIdle() + let commandOperation = BlockOperation { [unowned self] in + mpd_run_play_pos(self.connection, UInt32(queuePos)) + } + commandOperation.queuePriority = .veryHigh + commandQueue.addOperation(commandOperation) + idle() + } + + 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) + } + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Status.swift b/Persephone/MPDClient/Extensions/MPDClient+Status.swift new file mode 100644 index 0000000..be1e7e4 --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Status.swift @@ -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) + } +} diff --git a/Persephone/MPDClient/Extensions/MPDClient+Transport.swift b/Persephone/MPDClient/Extensions/MPDClient+Transport.swift new file mode 100644 index 0000000..c9d95e7 --- /dev/null +++ b/Persephone/MPDClient/Extensions/MPDClient+Transport.swift @@ -0,0 +1,65 @@ +// +// 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() { + queueCommand(command: .playPause) + } + + func stop() { + queueCommand(command: .stop) + } + + func prevTrack() { + queueCommand(command: .prevTrack) + } + + func nextTrack() { + queueCommand(command: .nextTrack) + } + + func seekCurrentSong(timeInSeconds: Float) { + noIdle() + commandQueue.addOperation { [unowned self] in + mpd_run_seek_current(self.connection, timeInSeconds, false) + } + idle() + } + + 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) + } + } + +} diff --git a/Persephone/MPDClient/MPDClient.swift b/Persephone/MPDClient/MPDClient.swift index 86dbf8a..5eb7309 100644 --- a/Persephone/MPDClient/MPDClient.swift +++ b/Persephone/MPDClient/MPDClient.swift @@ -12,285 +12,41 @@ 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") + let commandQueue = OperationQueue() + var commandsQueued: UInt = 0 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) + fetchStatus, fetchQueue, fetchAllAlbums, + playAlbum, getAlbumURI } 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() { + func queueCommand( + command: Command, + priority: BlockOperation.QueuePriority = .normal, + userData: Dictionary = [:] + ) { 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)) + let commandOperation = BlockOperation() { [unowned self] in + self.commandsQueued -= 1 + self.sendCommand(command: command, userData: userData) } + commandOperation.queuePriority = priority + commandsQueued += 1 + commandQueue.addOperation(commandOperation) 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 - } } diff --git a/Persephone/MPDClient/Models/Idle.swift b/Persephone/MPDClient/Models/Idle.swift new file mode 100644 index 0000000..62781db --- /dev/null +++ b/Persephone/MPDClient/Models/Idle.swift @@ -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) + } +} diff --git a/Persephone/MPDClient/Models/Song.swift b/Persephone/MPDClient/Models/Song.swift index 902dd21..0a4cce2 100644 --- a/Persephone/MPDClient/Models/Song.swift +++ b/Persephone/MPDClient/Models/Song.swift @@ -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)) diff --git a/Persephone/Models/Preferences.swift b/Persephone/Models/Preferences.swift index acb630f..1ab491f 100644 --- a/Persephone/Models/Preferences.swift +++ b/Persephone/Models/Preferences.swift @@ -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,21 @@ 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 } func addObserver(_ observer: NSObject, forKeyPath keyPath: String) { diff --git a/Persephone/Preferences/Controllers/AlbumArtPrefsController.swift b/Persephone/Preferences/Controllers/AlbumArtPrefsController.swift index ab4833f..97c33ad 100644 --- a/Persephone/Preferences/Controllers/AlbumArtPrefsController.swift +++ b/Persephone/Preferences/Controllers/AlbumArtPrefsController.swift @@ -9,9 +9,29 @@ import Cocoa class AlbumArtPrefsController: NSViewController { + var preferences = Preferences() + override func viewDidLoad() { super.viewDidLoad() + if let mpdLibraryDir = preferences.mpdLibraryDir { + mpdLibraryDirField.stringValue = mpdLibraryDir + } + 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! } diff --git a/Persephone/Preferences/Controllers/GeneralPrefsViewController.swift b/Persephone/Preferences/Controllers/GeneralPrefsViewController.swift index 3d34a07..b037c76 100644 --- a/Persephone/Preferences/Controllers/GeneralPrefsViewController.swift +++ b/Persephone/Preferences/Controllers/GeneralPrefsViewController.swift @@ -27,6 +27,10 @@ class GeneralPrefsViewController: NSViewController { override func viewDidAppear() { super.viewDidAppear() + + guard let title = title + else { return } + self.parent?.view.window?.title = title } @IBAction func updateMpdHost(_ sender: NSTextField) { diff --git a/Persephone/Preferences/Controllers/PreferencesViewController.swift b/Persephone/Preferences/Controllers/PreferencesViewController.swift index a8b3fcd..fcddb11 100644 --- a/Persephone/Preferences/Controllers/PreferencesViewController.swift +++ b/Persephone/Preferences/Controllers/PreferencesViewController.swift @@ -12,45 +12,48 @@ 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 } - - super.viewDidLoad() } -// override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) { -// NSAnimationContext.runAnimationGroup({ context in -// context.duration = 0.5 -// 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) -// } + 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) + } } diff --git a/Persephone/Preferences/Controllers/PreferencesWindowController.swift b/Persephone/Preferences/Controllers/PreferencesWindowController.swift new file mode 100644 index 0000000..e778d64 --- /dev/null +++ b/Persephone/Preferences/Controllers/PreferencesWindowController.swift @@ -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 + } +} diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Resources/Base.lproj/Main.storyboard index 29d5bd5..786cf4f 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Resources/Base.lproj/Main.storyboard @@ -249,7 +249,7 @@ - + @@ -293,54 +293,63 @@ - + - + - + - + - - + + + + - - - + + + + + + + + + + - + - + diff --git a/Persephone/Services/AlbumArtService.swift b/Persephone/Services/AlbumArtService.swift index ce27749..b40eadf 100644 --- a/Persephone/Services/AlbumArtService.swift +++ b/Persephone/Services/AlbumArtService.swift @@ -13,13 +13,21 @@ import PMKFoundation class AlbumArtService: NSObject { static var shared = AlbumArtService() + var session = URLSession(configuration: .default) - let albumArtQueue = DispatchQueue(label: "albumArtCacheQueue", attributes: .concurrent) + let cacheQueue = DispatchQueue(label: "albumArtCacheQueue", attributes: .concurrent) + let filesystemQueue = DispatchQueue(label: "albumArtFilesystemQueue", attributes: .concurrent) func fetchAlbumArt(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) { - albumArtQueue.async { + cacheQueue.async { [unowned self] in + //print("Trying cache") if !self.getCachedArtwork(for: album, callback: callback) { - self.getRemoteArtwork(for: album, callback: callback) +// self.filesystemQueue.async { +// _ = self.getArtworkFromFilesystem(for: album, callback: callback) +// } +// if !self.getArtworkFromFilesystem(for: album, callback: callback) { +// // self.getRemoteArtwork(for: album, callback: callback) +// } } } } @@ -45,6 +53,13 @@ class AlbumArtService: NSObject { } } + func getArtworkFromFilesystem(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) -> Bool { + print("No cache trying filesystem") + let uri = AppDelegate.mpdClient.getAlbumURI(for: album.album) + print(uri) + return false + } + func cacheArtwork(for album: AlbumItem, data: Data) { guard let bundleIdentifier = Bundle.main.bundleIdentifier, let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)