Subsonic/navidrome support
Hey, I've been looking for a Navidrome player for a while, but having one in emacs would be great.
https://github.com/isamert/empv.el has support, but I can only access 50 random songs in a completing read interface, and the look of your setup in listen.el looks like exactly what I'd want.
I don't know the first thing about implementing it myself in a pull request, so I humbly ask you if you'd be interested in having a go.
Thanks a lot!!
Hi,
I'm afraid that I don't have time to work on new backends, but if someone else writes one, I'll be glad to advise and merge it.
I've been trying to get something working: https://github.com/kaibagley/listen.el/tree/subsonic
Definitely not ready yet, but been fun to try and work it out (with a little guidance from LLMs)
@kaibagley That looks interesting. Please note, though, that I will politely decline to merge code generated by an LLM. I have legal and ethical concerns which I do not want to trouble my projects with.
By the way, for HTTP, you might like to use https://github.com/alphapapa/plz.el
Also, you might want to base your branch on this one, which adds support for playing remote files via HTTP: https://github.com/alphapapa/listen.el/compare/master...wip/remote-tracks
@kaibagley That looks interesting. Please note, though, that I will politely decline to merge code generated by an LLM. I have legal and ethical concerns which I do not want to trouble my projects with.
No problem at all. I'll explain my process so you have a bit more info. I'm pretty new to elisp, so when I come across a problem that I don't know how to approach in elisp, i'll ask an LLM through https://github.com/karthink/gptel. I'll then try and understand the methodology it used and then use that in the project. I never copy-paste from gptel, the purpose is for me to learn, so even in the worst case where my version looks very similar, its still typed out manually, using variable names that make sense to me, structured in a way that is logical to me, etc. etc.
But I'm not going to be offended or anything if you don't want the code, I understand
By the way, for HTTP, you might like to use https://github.com/alphapapa/plz.el
This is interesting, I had actually looked at this ages ago, but forgot about it when working on this. I'll give it a go!
Also, you might want to base your branch on this one, which adds support for playing remote files via HTTP: master...wip/remote-tracks
Alright I'll move it over, thanks
Of course, I can't object to using it for learning; I've done that myself in other contexts.
But I'm not going to be offended or anything if you don't want the code, I understand
Well, it's not like this is some kind of "100% organic, free-range code" project. :) It's definitely a gray area. And other than really obvious cases, all I can do is take the submitter's word for it, anyway. In the end it's just bytes. So who knows whether I've already, unknowingly accepted LLM-written contributions. And the same could be said before LLMs were a thing: the submitter attests to provenance and copyright, so how would I know if it were copied from elsewhere? But if I become aware, then I will prefer my preferences, and try to follow the GPL, of course (especially since this is distributed in GNU ELPA as "part of GNU Emacs").
In the end, I want good code that is useful and usable decades from now, and to have fun and learn along the way. So, in general, of course your contributions are welcome.
By the way, another Elisp tip that might help you: since nil represents an empty list, which is a sequence, nil can be an argument to concat. Can help to make some code more concise.
Well, it's not like this is some kind of "100% organic, free-range code" project. :) It's definitely a gray area. And other than really obvious cases, all I can do is take the submitter's word for it, anyway. In the end it's just bytes. So who knows whether I've already, unknowingly accepted LLM-written contributions...
I mentioned it because I know that a lot of people have hard-line positions on it. I have a lot of respect for open-source software, and want to contribute to free software in an ethical way.
By the way, another Elisp tip that might help you: since nil represents an empty list, which is a sequence, nil can be an argument to
concat. Can help to make some code more concise.
Thanks a lot! I'm very appreciative of any tips and improvements you can give me. I'll work more on this soon.
I've tried to keep it minimally "invasive", but I've had to change a few things in your code sorry..
- Handling of "filenames" is different, since an MPV "filename" can now be a URL
- Changed the deduplication code, it was very slow using regex for so many URLs, so it uses a hash table at the moment and WOW it is fast!
There's actually a lot of issues at the moment and I am unsure of how to go about solving them. The most pressing at the moment is a significant unpredictable stuttering when the player is running and playing a song over Subsonic. It's very hard to pin down what is causing it but I think it's:
- In major mode def, a theres a timer running listen-mode--update every 1 sec
- This calls listen--status and listen-mode-lighter (the function).
- One or both of these sometimes takes a second or two, and blocks emacs becuase they are synchronous.
This doesn't happen every second, most of the time it is not too bad.
Well, it's not like this is some kind of "100% organic, free-range code" project. :) It's definitely a gray area. And other than really obvious cases, all I can do is take the submitter's word for it, anyway. In the end it's just bytes. So who knows whether I've already, unknowingly accepted LLM-written contributions...
I mentioned it because I know that a lot of people have hard-line positions on it. I have a lot of respect for open-source software, and want to contribute to free software in an ethical way.
I appreciate that.
By the way, another Elisp tip that might help you: since nil represents an empty list, which is a sequence, nil can be an argument to
concat. Can help to make some code more concise.Thanks a lot! I'm very appreciative of any tips and improvements you can give me. I'll work more on this soon.
I've tried to keep it minimally "invasive", but I've had to change a few things in your code sorry..
- Handling of "filenames" is different, since an MPV "filename" can now be a URL
I may be mistaken, but it doesn't look like you're making use of the MPV support I already added. See listen-mpv.el. That might also help explain the timer issues.
- Changed the deduplication code, it was very slow using regex for so many URLs, so it uses a hash table at the moment and WOW it is fast!
How many URLs are you talking about?
I looked at the git history, and I see that you had modified the key function passed to cl-delete-duplicates. It is true that cl-delete-duplicates doesn't use a hash table, although the delete-dups function does. But I'm not sure why you were using that code that compared only the ID in a URL parameter. Anyway, I am curious about the number of tracks you're removing duplicates of.
I haven't had much trouble with the status update timer and code, but I have kept an eye on it, wondering if it would ever be an issue. Probably, I should make use of a custom filter function so we could avoid calling accept-process-output.
I'd also be curious to see whether you see any improvement by using the VLC backend (though I've found MPV better in some ways, also).
- Handling of "filenames" is different, since an MPV "filename" can now be a URL
I may be mistaken, but it doesn't look like you're making use of the MPV support I already added. See
listen-mpv.el. That might also help explain the timer issues.
I think I'm using it properly, I'm basically building up a URL like this:
https://music.subsonic.com/rest/stream.view?u=<username>&t=<token>&s=<salt>&v=1.16.1&c=listen-el&f=json&id=<song-id>
And passing that to MPV in the same way as filepaths are passed with local files. I just had to change some of the logic because functions like listen--play called expand-file-name which ruined the URL.
- Changed the deduplication code, it was very slow using regex for so many URLs, so it uses a hash table at the moment and WOW it is fast!
How many URLs are you talking about?
This brings up another question i wanted to ask: Should the user be able to have a library view of their entire Subsonic library? This could be potentially tens of thousands of songs, which might not be the greatest thing to just slam into memory. In this case, should there be some sqlite database or something? This would be handy as it would make getting song ids and metadata basically instant, and listen-subsonic.el wouldn't need to send search requests for song ids anymore.
But in this case, I've only got 2 functions that both add songs to the queue: a search function limited to 50 results, and a "queue starred songs" function, which for me is about 1000 songs. This takes about 1 second for MPV to request all the URLs and other metadata. I tried a regex matching the id part which was horrible, and thought I was gonna have to rewrite, but I just reverted and simply using equal as the test, and this as the key:
(defun listen-queue-track-id-key (track)
"Generate unique string key for TRACK."
(or (map-elt (listen-track-etc track) 'id)
(expand-file-name (listen-track-filename track))))
the cl-delete-duplicates is just about the same as my hash map method, so I'll bring back the cl-delete-duplicates since its simpler
... But I'm not sure why you were using that code that compared only the ID in a URL parameter...
Can't compare the whole url due to the username and token+salt auth. But it's faster (probably) to just access the id from the track.
I haven't had much trouble with the status update timer and code, but I have kept an eye on it, wondering if it would ever be an issue. Probably, I should make use of a custom filter function so we could avoid calling
accept-process-output.
I was wondering if just caching this information upon calling MPV on the new file would be a good idea, then emacs can keep track of the time, and sync the state whenver the user plays, pauses etc. but I don't know if this will affect automatic playing of the next song. I can see that VLC and MPV support playlists, so maybe that will help?
I'd also be curious to see whether you see any improvement by using the VLC backend (though I've found MPV better in some ways, also).
VLC experience seems identical to MPV in my small amount of testing.
And passing that to MPV in the same way as filepaths are passed with local files. I just had to change some of the logic because functions like
listen--playcalledexpand-file-namewhich ruined the URL.
That's why I said that you should rebase your branch onto the wip/remote-tracks branch and look carefully at what's changed, e.g. https://github.com/alphapapa/listen.el/blob/c9469a34125dbedb570ef40cb896a8d6d3235644/listen.el#L163
This brings up another question i wanted to ask: Should the user be able to have a library view of their entire Subsonic library? This could be potentially tens of thousands of songs, which might not be the greatest thing to just slam into memory. In this case, should there be some sqlite database or something? This would be handy as it would make getting song ids and metadata basically instant, and listen-subsonic.el wouldn't need to send search requests for song ids anymore.
I don't know. It depends on performance. Having a local database could help, but that would be a lot of work. I've tried to keep this player very simple, by doing things like just scanning the filesystem on demand. I'm not strictly opposed to adding a local database, but it would have to be done very carefully.
Can't compare the whole url due to the username and token+salt auth. But it's faster (probably) to just access the id from the track.
I don't understand what you mean. Each URL is unique, right? Why can't it be compared as a whole when testing for duplicates?
This takes about 1 second for MPV to request all the URLs and other metadata.
I don't understand. I thought MPV was doing the playing of individual tracks, not fetching metadata and other URLs.
I was wondering if just caching this information upon calling MPV on the new file would be a good idea, then emacs can keep track of the time, and sync the state whenver the user plays, pauses etc. but I don't know if this will affect automatic playing of the next song. I can see that VLC and MPV support playlists, so maybe that will help?
I'm not sure what you mean. Anyway, the current implementation keeps the playlist ("queue") locally in Emacs, and only uses MPV/VLC as the backend to play individual tracks. (Trying to keep the playlist in sync would be complicated and not that useful IMO.)
That's why I said that you should rebase your branch onto the
wip/remote-tracksbranch and look carefully at what's changed...
Yeah I rebased yesterday but haven't looked too deep yet. Will work on that soon.
I'm not strictly opposed to adding a local database, but it would have to be done very carefully.
I'll leave it for now, I don't have a need for a library view so maybe it's not needed at the moment.
I don't understand what you mean. Each URL is unique, right? Why can't it be compared as a whole when testing for duplicates?
Each URL is unique including duplicate songs. Since the token and salt are random, those parts of the URL are always different. A URL to stream the same song 10 times in a row in a queue, would have the same id parameters, but different tokens and salts. So comparing the URL string is extremely unlikely (ideally impossible) to work. Two streaming URL's for the same song:
https://subsonic.example.com/rest/stream.view?u=<username>&t=813a35d12d0c403c3469c81295deb746&s=vrsrop&v=1.16.1&c=listen.el&f=json&id=cd5e94c8bf50df36a60fd489d0ca57ca
https://subsonic.example.com/rest/stream.view?u=<username>&t=a8b7dcf0175b54fba4dd6aed935139e8&s=fymxue&v=1.16.1&c=listen.el&f=json&id=cd5e94c8bf50df36a60fd489d0ca57ca
I don't understand. I thought MPV was doing the playing of individual tracks, not fetching metadata and other URLs.
Sorry! I meant to say the listen-subsonic code takes 1 second. The code returns a list of ids, these get turned into streaming URLs which are given to MPV. This is the only time url is used to make a request, do you think plz would be overkill?
I was wondering if just caching this information upon calling MPV on the new file would be a good idea...
I'm not sure what you mean...
Theres a timer calling listen-mode--update once a second, which calls listen--elapsed and listen-mode-lighter, which then calls listen--elapsed, listen-info, listen-length, listen-status. I think all of this IPC is causing stuttering for me.
So my suggestion was: Can't we just keep a cache of this info, or even better just what's in the track alist? Song info, length and duration are in the alist, so emacs should just have to keep track of the time elapsed while playing, and whenever the song is paused, played etc, it updates the cached elapsed time by running listen--elapsed.
Regarding the mpv queue feature, my suggestion was that this could help with auto-playing the next song, without the use of listen-mode--update every 1 second. Perhaps it's not necessary to sync the whole queue, but just the current and next song. This way, MPV will handle when to play the next song (because it knows the songs duration). When emacs's self-timed elapsed variable indicates the song is over, it IPC's the MPV process and sees that there is a new song playing. Emacs then adds the new next song to MPV's queue from the listen-queue, and this loops. This may even allow gapless playback of songs.
Sorry if this doesn't make sense, I'm still not very familiar with all this compared to you so forgive me if this is a bad idea.
That's why I said that you should rebase your branch onto the
wip/remote-tracksbranch and look carefully at what's changed...Yeah I rebased yesterday but haven't looked too deep yet. Will work on that soon.
No problem. No rush. This is all for fun.
I'm not strictly opposed to adding a local database, but it would have to be done very carefully.
I'll leave it for now, I don't have a need for a library view so maybe it's not needed at the moment.
Sounds good.
I don't understand what you mean. Each URL is unique, right? Why can't it be compared as a whole when testing for duplicates?
Each URL is unique including duplicate songs. Since the token and salt are random, those parts of the URL are always different. A URL to stream the same song 10 times in a row in a queue, would have the same id parameters, but different tokens and salts. So comparing the URL string is extremely unlikely (ideally impossible) to work. Two streaming URL's for the same song:
https://subsonic.example.com/rest/stream.view?u=<username>&t=813a35d12d0c403c3469c81295deb746&s=vrsrop&v=1.16.1&c=listen.el&f=json&id=cd5e94c8bf50df36a60fd489d0ca57ca https://subsonic.example.com/rest/stream.view?u=<username>&t=a8b7dcf0175b54fba4dd6aed935139e8&s=fymxue&v=1.16.1&c=listen.el&f=json&id=cd5e94c8bf50df36a60fd489d0ca57ca
I get it now.
I think I would suggest defining a new track type, specific to Subsonic URLs. Then you could implement methods to uniquify the URLs for deduplication, etc. Then we could use a custom hash table test function, which would handle the track types accordingly. For example, I just put together this, which I'll probably merge to master, and you could use it:
(define-hash-table-test
'listen-track-equal
#'sxhash-equal
(lambda (track)
(sxhash (expand-file-name (listen-track-filename track)))))
(cl-defun listen-delete-dups (list &optional (test 'listen-track-equal))
"Return LIST having destructively removed duplicates.
Similar to `delete-dups', but TEST may be specified.
Unlike `delete-dups', this function always uses a hash table to find
duplicates; therefore TEST should be compatible with `make-hash-table',
which see."
;; Copies the body of `delete-dups', passing through TEST, and removing the length-based
;; non-hash-table case..
(let ((hash (make-hash-table :test test))
(tail list) retail)
(puthash (car list) t hash)
(while (setq retail (cdr tail))
(let ((elt (car retail)))
(if (gethash elt hash)
(setcdr tail (cdr retail))
(puthash elt t hash)
(setq tail retail))))
list))
This already seems to improve performance significantly, though I haven't tested it thoroughly.
I don't understand. I thought MPV was doing the playing of individual tracks, not fetching metadata and other URLs.
Sorry! I meant to say the listen-subsonic code takes 1 second. The code returns a list of ids, these get turned into streaming URLs which are given to MPV. This is the only time
urlis used to make a request, do you thinkplzwould be overkill?
In the end, it doesn't matter that much. But the point of using plz would be to make the code easier to understand, and secondarily, url has a few bugs that plz doesn't (which might never be encountered here, but anyway...).
I was wondering if just caching this information upon calling MPV on the new file would be a good idea...
I'm not sure what you mean...
Theres a timer calling
listen-mode--updateonce a second, which callslisten--elapsedandlisten-mode-lighter, which then callslisten--elapsed,listen-info,listen-length,listen-status. I think all of this IPC is causing stuttering for me.So my suggestion was: Can't we just keep a cache of this info, or even better just what's in the track alist? Song info, length and duration are in the alist, so emacs should just have to keep track of the time elapsed while playing, and whenever the song is paused, played etc, it updates the cached elapsed time by running
listen--elapsed.
Indeed, there is more IPC happening there than one would want, especially at that frequency. I had to do that to work around issues in VLC's IPC protocol and implementation.
I avoided implementing a cache for that because caching is one of the "hard problems," and it's simpler to just query the player and get the current information (e.g. what happens if the player stops but Emacs doesn't know?).
Maybe if the player backend were improved to use a bespoke process filter and listen for incoming information, we wouldn't need to query it on a timer; we could just react to incoming commands.
Another possibility, maybe in the interim, would be to try to use a single IPC call to get as much info as possible, and return that in the various specific commands when feasible, rather than using multiple IPC calls for specific info.
Regarding the mpv queue feature, my suggestion was that this could help with auto-playing the next song, without the use of
listen-mode--updateevery 1 second. Perhaps it's not necessary to sync the whole queue, but just the current and next song. This way, MPV will handle when to play the next song (because it knows the songs duration). When emacs's self-timedelapsedvariable indicates the song is over, it IPC's the MPV process and sees that there is a new song playing. Emacs then adds the new next song to MPV's queue from the listen-queue, and this loops. This may even allow gapless playback of songs.
There's nothing wrong with that idea, except that it requires us to keep two queues in sync: that in MPV/VLC and that in listen.el. That becomes a problem in and of itself, and it's one I'd rather not have to deal with. The idea of listen.el is that the backend that does the playing should be irrelevant and invisible. We have our own notion of a queue and tracks, and we just want to call a function and have audio start playing, and have it seek and stop when we tell it to.
Ideally we could do something like load libvlc (or some other audio library) into Emacs and skip the whole IPC thing altogether. But since there's no Elisp FFI (not merged into Emacs, anyway), that would require us to write a wrapper library, and that would limit us to using backends that have wrappers.
Sorry if this doesn't make sense, I'm still not very familiar with all this compared to you so forgive me if this is a bad idea.
Not at all. I'm glad to have someone interested in the project! It may not sound humble of me to say this, but I think that listen.el has the potential to be the best Emacs music player/manager. It just needs more work to expand its functionality and integration.
I think I would suggest defining a new track type, specific to Subsonic URLs. Then you could implement methods to uniquify the URLs for deduplication, etc. Then we could use a custom hash table test function, which would handle the track types accordingly. For example, I just put together this, which I'll probably merge to master, and you could use it:
Looks cool!
Maybe if the player backend were improved to use a bespoke process filter and listen for incoming information, we wouldn't need to query it on a timer; we could just react to incoming commands.
Another possibility, maybe in the interim, would be to try to use a single IPC call to get as much info as possible, and return that in the various specific commands when feasible, rather than using multiple IPC calls for specific info.
I'll try and get the "one big IPC" approach working, since (at least for me) the stuttering is a bit too disruptive to call the Subsonic implementation polished. The process filter approach sounds good, but I wouldn't know how to go about it just yet.
There's nothing wrong with that idea, except that it requires us to keep two queues in sync: that in MPV/VLC and that in listen.el. That becomes a problem in and of itself, and it's one I'd rather not have to deal with. The idea of listen.el is that the backend that does the playing should be irrelevant and invisible. We have our own notion of a queue and tracks, and we just want to call a function and have audio start playing, and have it seek and stop when we tell it to.
This makes sense, I think it would be cool to implement gapless playback at some stage though, and I'm not sure how else to do it in this application.
Not at all. I'm glad to have someone interested in the project! It may not sound humble of me to say this, but I think that listen.el has the potential to be the best Emacs music player/manager. It just needs more work to expand its functionality and integration.
I would agree, the interface is very nice and while the "multiple queue" idea took a little while to appreciate I think its a very good approach. I'd be proud to say I helped out on a part of it
I've pushed a branch that updates MPV support to use an event-driven model and avoids polling every time the lighter is updated. It's not finished yet, but it's mostly functional. See https://github.com/alphapapa/listen.el/compare/wip/mpv-event-driven?expand=1 An obvious remaining bug is that, when the player is paused, the displayed remaining time in the lighter continues to count down.
I would agree, the interface is very nice and while the "multiple queue" idea took a little while to appreciate I think its a very good approach. I'd be proud to say I helped out on a part of it
I enjoyed using players like Amarok and Clementine in the past, and they work a bit differently, by having multiple playlists open, and a single queue can exist within a playlist. Now that I think about it, that's probably the best model to follow. But the approach Listen follows is similar, and pretty simple, it's just that playlists are called queues. (Maybe it also helps to distinguish playlist files (e.g. M3U files) from in-memory queues.)
Awesome, I'm keen to check it out soon!
I have a couple more questions:
How should I go about implementing starring/favouriting tracks? I have a function that can star/unstar tracks, but how should i indicate this. A simple way would be to have a new column in the queue vtables, but I don't want to edit stuff too much without asking first. I'm not sure how to implement this with local files.
Should the user also be able to edit ratings too? Very simple to do for Subsonic, but like the favouriting I wouldn't know much about using ffmpeg to edit metadata like that (and I'm a bit scared of all your common lisp stuff).
Let's take it one thing at a time. First you'll want to get playback working, and probably a library backend for selecting tracks. Details like that can come later.
Of course, you're right.
It's working pretty well at the moment, I've been daily driving it for a week now and it works well when the songs are in the queue. It's easy to add songs using "queue starred" which puts all starred songs into a queue, and songs can be added using "search Subsonic". I have a library view from the queue, and from a subsonic search too.
My main issue now is the quantity of songs on the server, so I'm still trying to decide on the best type of library views and "add to queue" functionality. I'll maybe experiment with a simple sqlite database just in case that magically solves all my problems
Sounds nice. Fair warning, at this time, I'd be hesitant to merge anything using SQLite, just for the sake of simplicity. One of the reasons I added MPD support is that its database works well and is updated automatically (and I was already using it, so Listen just gives me another frontend to the MPD library, essentially).
OTOH if your SQLite solution were very well implemented and self-contained in your listen-subsonic.el library (i.e. if it doesn't make all of Listen depend on SQLite), then I probably wouldn't refuse to merge that.
FYI, I've pushed more improvements for MPV asynchronous support, and I merged it to master. So if you're using MPV, updating the lighter should be very "lightweight" compared to with VLC, as there's much less IPC, and it's mostly async (and updating the duration/elapsed doesn't require constant IPC).
The VLC library could be similarly improved (at least, calculating of the duration/elapsed instead of constantly doing IPC), but I'm not sure to what extent, because VLC's IPC support seems more awkward and rudimentary.
Anyway, the MPV support seems pretty solid now, so if you try it, let me know what you think.
I'll stay away from sqlite then, I'll just see how I go with folder, artist and album libraries for now. By the way, I don't really understand the library view. Is it intended that I can only view it organised by genre? I'm not familiar with taxy at all. I also can't get font lock to colour the library view like it does in the README screenshots.
The async stuff seems great so far, thank you very much for getting these improvements out so quick. I'll have to test drive it for a while but I haven't noticed any stuttering at all
I have a dired-like interface, and a completing-read interface for exploring a subsonic library. Which option do you think fits in more?
dired:
RETto go into an artist/album^to go up a levelAto recursively add all tracks under the current level
Completing-read
- Select an option to go into the album/artist
- Choose
..to go back up a level - Choose
[All]to recursively all all tracks under the current level
I started with the completing read, which I really like. But, it maybe doesn't feel very "emacsy", since I'm trying to basically have directory navigation without using dired. So, i tried to make a dired style version. It seems good, but its not as nice and quick as completing-read.
Alright I think it's getting close to PR ready (i.e. polished enough to not be embarrassed showing you).
I have 2 branches, subsonic and subsonic-rebase. It's up to you how accurate you want the history to be, I just wasn't sure if you wanted me to dump a billion ugly commits that rewrite the same lines of code 100 times into your repo.
-
subsonichas like 100 commits, with commits fromwip/remote-tracksandwip/mpv-event-drivenjust chucked into it willy-nilly. Was messy and I thought I should clean it up. -
subsonic-rebaseismaster-> mergewip/remote-tracks-> mergewip/mpv-event-driven-> squash mergesubsonic(reduced to about 5 commits).
I have some other things that should be implemented as some stage, such as displaying a star in the queue and library views for favourited tracks, but the basic Subsonic functionality is there and as far as I can tell is up to spec with the local functionality.
Let me know what you think and I'll move on from there