mirror of
https://github.com/danbee/persephone
synced 2025-03-04 08:39:11 +00:00
Compare commits
4 Commits
17ed1cdecc
...
0e71714260
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e71714260 | |||
| fc8e2a5bd7 | |||
| de048a9e9e | |||
| 0306f9e9b5 |
@ -11,6 +11,8 @@ import Cocoa
|
|||||||
class AlbumViewController: NSViewController,
|
class AlbumViewController: NSViewController,
|
||||||
NSCollectionViewDelegate,
|
NSCollectionViewDelegate,
|
||||||
NSCollectionViewDelegateFlowLayout {
|
NSCollectionViewDelegateFlowLayout {
|
||||||
|
var preferences = Preferences()
|
||||||
|
|
||||||
let paddingWidth: CGFloat = 40
|
let paddingWidth: CGFloat = 40
|
||||||
let gutterWidth: CGFloat = 20
|
let gutterWidth: CGFloat = 20
|
||||||
|
|
||||||
@ -36,6 +38,8 @@ class AlbumViewController: NSViewController,
|
|||||||
)
|
)
|
||||||
|
|
||||||
albumCollectionView.dataSource = dataSource
|
albumCollectionView.dataSource = dataSource
|
||||||
|
|
||||||
|
preferences.addObserver(self, forKeyPath: "mpdLibraryDir")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillLayout() {
|
override func viewWillLayout() {
|
||||||
@ -53,6 +57,20 @@ class AlbumViewController: NSViewController,
|
|||||||
layout.setScrollPosition()
|
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) {
|
@objc func updateAlbums(_ notification: Notification) {
|
||||||
guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album]
|
guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album]
|
||||||
else { return }
|
else { return }
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class QueueViewController: NSViewController,
|
|||||||
let newQueuePos = queueView.selectedRow - 1
|
let newQueuePos = queueView.selectedRow - 1
|
||||||
|
|
||||||
if newQueuePos >= 0 {
|
if newQueuePos >= 0 {
|
||||||
AppDelegate.mpdClient.playTrack(queuePos: newQueuePos)
|
AppDelegate.mpdClient.playTrack(at: newQueuePos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,4 +13,24 @@ extension NSImage {
|
|||||||
static let pauseIcon = NSImage(named: "pauseButton")
|
static let pauseIcon = NSImage(named: "pauseButton")
|
||||||
|
|
||||||
static let defaultCoverArt = NSImage(named: "blankAlbum")
|
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]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,12 @@ extension MPDClient {
|
|||||||
queueCommand(command: .playAlbum, userData: ["album": album])
|
queueCommand(command: .playAlbum, userData: ["album": album])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlbumURI(for album: Album) {
|
func getAlbumURI(for album: Album, callback: @escaping (String?) -> Void) {
|
||||||
queueCommand(command: .getAlbumURI, userData: ["album": album])
|
queueCommand(
|
||||||
|
command: .getAlbumURI,
|
||||||
|
priority: .low,
|
||||||
|
userData: ["album": album, "callback": callback]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPlayAlbum(_ album: Album) {
|
func sendPlayAlbum(_ album: Album) {
|
||||||
@ -65,30 +69,31 @@ extension MPDClient {
|
|||||||
self.delegate?.didLoadAlbums(mpdClient: self, albums: albums)
|
self.delegate?.didLoadAlbums(mpdClient: self, albums: albums)
|
||||||
}
|
}
|
||||||
|
|
||||||
func albumURI(for album: Album) -> String? {
|
func albumURI(for album: Album, callback: (String?) -> Void) {
|
||||||
var songURI: String?
|
var songURI: String?
|
||||||
|
|
||||||
guard isConnected else { return nil }
|
guard isConnected else { return }
|
||||||
|
|
||||||
print("Getting URI")
|
|
||||||
mpd_search_db_songs(self.connection, true)
|
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, 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_ALBUM_ARTIST, album.artist)
|
||||||
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_TRACK, "1")
|
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_TRACK, "1")
|
||||||
|
|
||||||
mpd_search_commit(self.connection)
|
mpd_search_commit(self.connection)
|
||||||
print("Performed search")
|
|
||||||
|
|
||||||
while let mpdSong = mpd_recv_song(self.connection) {
|
while let mpdSong = mpd_recv_song(self.connection) {
|
||||||
let song = Song(mpdSong)
|
let song = Song(mpdSong)
|
||||||
print(song)
|
|
||||||
|
|
||||||
if songURI == nil {
|
if songURI == nil {
|
||||||
songURI = song.uriString
|
songURI = song.uriString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print("Got URI")
|
|
||||||
|
|
||||||
return songURI
|
callback(
|
||||||
|
songURI?
|
||||||
|
.split(separator: "/")
|
||||||
|
.dropLast()
|
||||||
|
.joined(separator: "/")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,12 +24,22 @@ extension MPDClient {
|
|||||||
sendStop()
|
sendStop()
|
||||||
case .playPause:
|
case .playPause:
|
||||||
sendPlay()
|
sendPlay()
|
||||||
|
case .seekCurrentSong:
|
||||||
|
guard let timeInSeconds = userData["timeInSeconds"] as? Float
|
||||||
|
else { return }
|
||||||
|
sendSeekCurrentSong(timeInSeconds: timeInSeconds)
|
||||||
|
|
||||||
// Status commands
|
// Status commands
|
||||||
case .fetchStatus:
|
case .fetchStatus:
|
||||||
sendRunStatus()
|
sendRunStatus()
|
||||||
|
|
||||||
|
// Queue commands
|
||||||
case .fetchQueue:
|
case .fetchQueue:
|
||||||
sendFetchQueue()
|
sendFetchQueue()
|
||||||
|
case .playTrack:
|
||||||
|
guard let queuePos = userData["queuePos"] as? Int
|
||||||
|
else { return }
|
||||||
|
sendPlayTrack(at: queuePos)
|
||||||
|
|
||||||
// Album commands
|
// Album commands
|
||||||
case .fetchAllAlbums:
|
case .fetchAllAlbums:
|
||||||
@ -38,8 +48,10 @@ extension MPDClient {
|
|||||||
guard let album = userData["album"] as? Album else { return }
|
guard let album = userData["album"] as? Album else { return }
|
||||||
sendPlayAlbum(album)
|
sendPlayAlbum(album)
|
||||||
case .getAlbumURI:
|
case .getAlbumURI:
|
||||||
guard let album = userData["album"] as? Album else { return }
|
guard let album = userData["album"] as? Album,
|
||||||
_ = getAlbumURI(for: album)
|
let callback = userData["callback"] as? (String?) -> Void
|
||||||
|
else { return }
|
||||||
|
albumURI(for: album, callback: callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,17 +18,13 @@ extension MPDClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func idle() {
|
func idle() {
|
||||||
let idleOperation = BlockOperation {
|
if !self.isIdle && self.commandsQueued == 0 {
|
||||||
if !self.isIdle && self.commandsQueued == 0 {
|
mpd_send_idle(self.connection)
|
||||||
mpd_send_idle(self.connection)
|
self.isIdle = true
|
||||||
self.isIdle = true
|
|
||||||
|
|
||||||
let result = mpd_recv_idle(self.connection, true)
|
let result = mpd_recv_idle(self.connection, true)
|
||||||
self.handleIdleResult(result)
|
self.handleIdleResult(result)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
idleOperation.queuePriority = .veryLow
|
|
||||||
commandQueue.addOperation(idleOperation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleIdleResult(_ result: mpd_idle) {
|
func handleIdleResult(_ result: mpd_idle) {
|
||||||
|
|||||||
@ -14,16 +14,12 @@ extension MPDClient {
|
|||||||
sendCommand(command: .fetchQueue)
|
sendCommand(command: .fetchQueue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func playTrack(queuePos: Int) {
|
func playTrack(at queuePos: Int) {
|
||||||
guard isConnected else { return }
|
queueCommand(command: .playTrack, userData: ["queuePos": queuePos])
|
||||||
|
}
|
||||||
|
|
||||||
noIdle()
|
func sendPlayTrack(at queuePos: Int) {
|
||||||
let commandOperation = BlockOperation { [unowned self] in
|
mpd_run_play_pos(self.connection, UInt32(queuePos))
|
||||||
mpd_run_play_pos(self.connection, UInt32(queuePos))
|
|
||||||
}
|
|
||||||
commandOperation.queuePriority = .veryHigh
|
|
||||||
commandQueue.addOperation(commandOperation)
|
|
||||||
idle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendFetchQueue() {
|
func sendFetchQueue() {
|
||||||
|
|||||||
@ -27,11 +27,10 @@ extension MPDClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seekCurrentSong(timeInSeconds: Float) {
|
func seekCurrentSong(timeInSeconds: Float) {
|
||||||
noIdle()
|
queueCommand(
|
||||||
commandQueue.addOperation { [unowned self] in
|
command: .seekCurrentSong,
|
||||||
mpd_run_seek_current(self.connection, timeInSeconds, false)
|
userData: ["timeInSeconds": timeInSeconds]
|
||||||
}
|
)
|
||||||
idle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendNextTrack() {
|
func sendNextTrack() {
|
||||||
@ -62,4 +61,7 @@ extension MPDClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendSeekCurrentSong(timeInSeconds: Float) {
|
||||||
|
mpd_run_seek_current(self.connection, timeInSeconds, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,8 @@ class MPDClient {
|
|||||||
var commandsQueued: UInt = 0
|
var commandsQueued: UInt = 0
|
||||||
|
|
||||||
enum Command {
|
enum Command {
|
||||||
case prevTrack, nextTrack, playPause, stop,
|
case prevTrack, nextTrack, playPause, stop, seekCurrentSong,
|
||||||
fetchStatus, fetchQueue, fetchAllAlbums,
|
fetchStatus, fetchQueue, playTrack, fetchAllAlbums,
|
||||||
playAlbum, getAlbumURI
|
playAlbum, getAlbumURI
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,13 +40,15 @@ class MPDClient {
|
|||||||
guard isConnected else { return }
|
guard isConnected else { return }
|
||||||
|
|
||||||
noIdle()
|
noIdle()
|
||||||
|
|
||||||
let commandOperation = BlockOperation() { [unowned self] in
|
let commandOperation = BlockOperation() { [unowned self] in
|
||||||
self.commandsQueued -= 1
|
self.commandsQueued -= 1
|
||||||
self.sendCommand(command: command, userData: userData)
|
self.sendCommand(command: command, userData: userData)
|
||||||
|
|
||||||
|
self.idle()
|
||||||
}
|
}
|
||||||
commandOperation.queuePriority = priority
|
commandOperation.queuePriority = priority
|
||||||
commandsQueued += 1
|
commandsQueued += 1
|
||||||
commandQueue.addOperation(commandOperation)
|
commandQueue.addOperation(commandOperation)
|
||||||
idle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,10 @@ struct Preferences {
|
|||||||
return mpdLibraryDir ?? mpdLibraryDirDefault
|
return mpdLibraryDir ?? mpdLibraryDirDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expandedMpdLibraryDir: String {
|
||||||
|
return NSString(string: mpdLibraryDirOrDefault).expandingTildeInPath
|
||||||
|
}
|
||||||
|
|
||||||
func addObserver(_ observer: NSObject, forKeyPath keyPath: String) {
|
func addObserver(_ observer: NSObject, forKeyPath keyPath: String) {
|
||||||
preferences.addObserver(observer, forKeyPath: keyPath, options: .new, context: nil)
|
preferences.addObserver(observer, forKeyPath: keyPath, options: .new, context: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,18 +13,16 @@ import PMKFoundation
|
|||||||
|
|
||||||
class AlbumArtService: NSObject {
|
class AlbumArtService: NSObject {
|
||||||
static var shared = AlbumArtService()
|
static var shared = AlbumArtService()
|
||||||
|
var preferences = Preferences()
|
||||||
|
|
||||||
var session = URLSession(configuration: .default)
|
var session = URLSession(configuration: .default)
|
||||||
let cacheQueue = 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) {
|
func fetchAlbumArt(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) {
|
||||||
cacheQueue.async { [unowned self] in
|
cacheQueue.async { [unowned self] in
|
||||||
//print("Trying cache")
|
//print("Trying cache")
|
||||||
if !self.getCachedArtwork(for: album, callback: callback) {
|
if !self.getCachedArtwork(for: album, callback: callback) {
|
||||||
// self.filesystemQueue.async {
|
self.getArtworkFromFilesystem(for: album, callback: callback)
|
||||||
// _ = self.getArtworkFromFilesystem(for: album, callback: callback)
|
|
||||||
// }
|
|
||||||
// if !self.getArtworkFromFilesystem(for: album, callback: callback) {
|
// if !self.getArtworkFromFilesystem(for: album, callback: callback) {
|
||||||
// // self.getRemoteArtwork(for: album, callback: callback)
|
// // self.getRemoteArtwork(for: album, callback: callback)
|
||||||
// }
|
// }
|
||||||
@ -53,14 +51,48 @@ class AlbumArtService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getArtworkFromFilesystem(for album: AlbumItem, callback: @escaping (_ image: NSImage) -> Void) -> Bool {
|
func getArtworkFromFilesystem(
|
||||||
print("No cache trying filesystem")
|
for album: AlbumItem,
|
||||||
let uri = AppDelegate.mpdClient.getAlbumURI(for: album.album)
|
callback: @escaping (_ image: NSImage) -> Void
|
||||||
print(uri)
|
) {
|
||||||
return false
|
let coverArtFilenames = [
|
||||||
|
"folder.jpg",
|
||||||
|
"cover.jpg",
|
||||||
|
"\(album.artist) - \(album.title).jpg"
|
||||||
|
]
|
||||||
|
|
||||||
|
AppDelegate.mpdClient.getAlbumURI(
|
||||||
|
for: album.album,
|
||||||
|
callback: { (_ albumURI: String?) in
|
||||||
|
guard let albumURI = albumURI
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let musicDir = self.preferences.expandedMpdLibraryDir
|
||||||
|
let fullAlbumURI = "\(musicDir)/\(albumURI)"
|
||||||
|
|
||||||
|
for coverArtFilename in coverArtFilenames {
|
||||||
|
let coverArtURI = "\(fullAlbumURI)/\(coverArtFilename)"
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: coverArtURI),
|
||||||
|
let data = FileManager.default.contents(atPath: coverArtURI),
|
||||||
|
let image = NSImage(data: data) {
|
||||||
|
|
||||||
|
let imageThumb = image.toFitBox(
|
||||||
|
size: NSSize(width: 180, height: 180)
|
||||||
|
)
|
||||||
|
self.cacheArtwork(
|
||||||
|
for: album,
|
||||||
|
data: imageThumb.jpegData(compressionQuality: 0.5)
|
||||||
|
)
|
||||||
|
callback(imageThumb)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheArtwork(for album: AlbumItem, data: Data) {
|
func cacheArtwork(for album: AlbumItem, data: Data?) {
|
||||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
|
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
|
||||||
let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
.appendingPathComponent(bundleIdentifier)
|
.appendingPathComponent(bundleIdentifier)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user