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

WIP: Refactor MPDClient

This should make handling the queuing side work more reliably.
This commit is contained in:
Daniel Barber 2019-03-16 18:07:44 -04:00
parent 537a66d6aa
commit fe748e2c61
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
20 changed files with 632 additions and 327 deletions

View File

@ -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 = "<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>"; };
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>"; };
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>"; };
E42A8F3922176D6400A13ED9 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
@ -199,6 +217,8 @@
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>"; };
@ -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 = "<group>";
@ -465,6 +494,7 @@
E4A642D922090CBE00067D21 /* Status.swift */,
E4EB2378220F10B8008C70C0 /* Pair.swift */,
E4EB237A220F7CF1008C70C0 /* Album.swift */,
E4C8B53D22349002009A20F3 /* Idle.swift */,
);
path = Models;
sourceTree = "<group>";
@ -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;

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

@ -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
}
}

View File

@ -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<String, Any> = [:]
) {
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)
}
}
}

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,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,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()
}
}
}

View File

@ -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)
}
}
}

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,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)
}
}
}

View File

@ -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<String, Any> = [:]
) {
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
}
}

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

@ -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) {

View File

@ -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!
}

View File

@ -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) {

View File

@ -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)
}
}

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

@ -249,7 +249,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"/>
@ -293,54 +293,63 @@
</objects>
<point key="canvasLocation" x="916" y="236"/>
</scene>
<!--Album Art Prefs Controller-->
<!--Album Art-->
<scene sceneID="pQx-0G-WVt">
<objects>
<viewController id="3C9-vU-zjZ" customClass="AlbumArtPrefsController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<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="52" y="63" width="104" height="17"/>
<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" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gDk-ca-eOa">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gDk-ca-eOa">
<rect key="frame" x="162" y="58" width="288" height="22"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="MPD music library path" drawsBackground="YES" id="7WZ-b7-GUs">
<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" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pRL-MG-1Be">
<rect key="frame" x="160" y="27" width="253" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Get missing artwork from MusicBrainz" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="LpD-Ew-HMd">
<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>
</button>
</subviews>
<constraints>
<constraint firstItem="zZn-Rm-e1f" firstAttribute="leading" secondItem="PyK-v2-kus" secondAttribute="leading" constant="54" id="GOH-Mx-w1M"/>
<constraint firstItem="zZn-Rm-e1f" firstAttribute="top" secondItem="PyK-v2-kus" secondAttribute="top" constant="20" symbolic="YES" id="Ou1-BH-AgV"/>
<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="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 Prefs View Controller-->
<!--General-->
<scene sceneID="xTC-Y5-Agk">
<objects>
<viewController id="nYi-sw-ZNp" customClass="GeneralPrefsViewController" 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"/>

View File

@ -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)