owncloud / music

:notes: Music app for ownCloud
GNU Affero General Public License v3.0
569 stars 199 forks source link

Music api ability to add new playlist and new tracks to the playlist #1012

Closed sanfx closed 2 years ago

sanfx commented 2 years ago

Hello,

It would be really useful to have an api to add a new playlist

/nextcloud/index.php/apps/music/api/playlist/add/<name>

or/add a way to add new track to the playlist.

Tried to look into ths : https://github.com/owncloud/music#api but did not find anything relevant.

Thanks in advance.

paulijar commented 2 years ago

Yeah, the https://github.com/owncloud/music#api is a really barebone documentation especially on what comes to the playlist API. But you can see the whole REST API from the file https://github.com/owncloud/music/blob/master/appinfo/routes.php.

To create a new playlist, you can use https://github.com/owncloud/music/blob/fdba21e1aded8ff8333104694c011365d8997751/appinfo/routes.php#L67, and to add tracks to the list, you can use https://github.com/owncloud/music/blob/fdba21e1aded8ff8333104694c011365d8997751/appinfo/routes.php#L71.

The implementation of those REST methods can be found from https://github.com/owncloud/music/blob/fdba21e1aded8ff8333104694c011365d8997751/lib/Controller/PlaylistApiController.php#L94 and https://github.com/owncloud/music/blob/fdba21e1aded8ff8333104694c011365d8997751/lib/Controller/PlaylistApiController.php#L205 (Edit: I had a wrong link here, originally).

I'm just curious but where are you planning to use this API? Are you developing your own app for ownCloud and/or Nextcloud which would interface with the Music app?

sanfx commented 2 years ago

I see, I ran in the browser (for testing) https://website.com/nextcloud/index.php/apps/music/api/playlists/1 and it worked but when I logged out I started getting error:

app":"index","method":"GET","url":"/nextcloud/index.php/apps/music/api/playlists/1","message":"OCA\\Music\\Controller\\PlaylistApiController::__construct(): Argument #10 ($userId) must be of type string, null given, called in /var/www/html/nextcloud/apps/music/lib/App/Music.php on line 159",

from nextcloud.log

so create wont work as I am writing a python tool and I believe this is why I get Internal Server error

>>> url = "https://website.com/nextcloud/index.php/apps/music/api/playlists#create"
>>> r = requests.post(url)
>>> print(r.status_code, r.reason)
500 Internal Server Error
paulijar commented 2 years ago

As you can see, the playlist API itself has no way to identify or authenticate the Nextcloud user to be targeted. It is essentially an internal API intended to be used within the Nextcloud server and not by an external script or other client, and it relies on the Nextcloud core to handle the user authentication.

You probably could use the Nextcloud authentication mechanisms from your script and receive some kind of access token to be included in your other API calls but I'm not familiar with the details of this. But you might find it easier to utilize the Ampache or Subsonic APIs provided by the Music app as those are actually designed solely to be used by the external clients. Both of them provide methods for the playlist manipulation. In my opinion, the Ampache API is neater and better designed but the user authentication there requires a bit of effort while that part is extremely plain and simple on the Subsonic API.

The Ampache API can be found from the URL https://<your_cloud_url>/index.php/apps/music/ampache/server/xml.server.php and the Subsonic API from the URL https://<your_cloud_url>/index.php/apps/music/subsonic/rest/{method}. For both of them, you need to use a specifically generated password/APIKEY as described here.

sanfx commented 2 years ago

Thank you @paulijar

I will try to stick to the api you shared initialy. Now using the appPassword I am able to create a new playlist

>>> data = {"name": "test2"}
>>> r = requests.post(url, data=data)
>>> r.text
'{"name":"test2","trackIds":[],"id":4,"created":"2022-09-22 17:44:12.219","updated":"2022-09-22 17:44:12.219","comment":null}'

Now I want to add a track. And I have a track with index 2 (I am sure) So, my new url to add tracks looks like this: /nextcloud/index.php/apps/music/api/playlists/4/add#addTracks'

So I tried the following ways:


>>> data = {"name": "2"}
>>> r = requests.post(url_add_tracks, data=data)
>>> r.text
'{"name":"test2","trackIds":[0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 17:49:24.347","comment":null}'
>>> data = {"id": "2"}
>>> r = requests.post(url_add_tracks, data=data)
>>> r.text
'{"name":"test2","trackIds":[0,0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 17:50:54.425","comment":null}'
>>> data = {"name": "Someday"}
>>> r = requests.post(url_add_tracks, data=data)
>>> r.text
'{"name":"test2","trackIds":[0,0,0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 17:52:14.744","comment":null}'

then looked at https://shiva.readthedocs.io/en/latest/playlists.html

tried :

>>> data = {"track": "2"}
>>> r = requests.post(url_add_tracks, data=data)
>>> r.text
'{"name":"test2","trackIds":[0,0,0,0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 17:57:34.939","comment":null}'

I realized the #addTracks part is useless, so I removed it and tried again as below:

>>> add_url = url_add_tracks+"?track=2"
>>> data = {"track": 2} # kept for sake of testing
>>> r = requests.post(add_url, data=data)
>>> r.text
'{"name":"test2","trackIds":[0,0,0,0,0,0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 17:59:30.438","comment":null}'
>>> data = {"index": 2}
>>> r = requests.post(add_url, data=data)
>>> r.text
'{"name":"test2","trackIds":[0,0,0,0,0,0,0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 18:00:52.169","comment":null}'
>>> data = {"tracks.index": 2}
>>> r = requests.post(add_url, data=data)
>>> r.text
'{"name":"test2","trackIds":[0,0,0,0,0,0,0,0],"id":4,"created":"2022-09-22 17:44:12","updated":"2022-09-22 18:03:01.138","comment":null}'

Still now success with adding tracks :-( what am I doing wrong?

paulijar commented 2 years ago

Our playlist API doesn't exactly follow the Shiva specification although it is somewhat similar. And as our playlist API isn't really documented anywhere, it may be a bit difficult to use.

In your attempts, probably the only thing wrong is that you should use the property name "trackIds". So I believe that this should work:

data = {"trackIds": "2"}
r = requests.post(url_add_tracks, data=data)

The same API can be used to add multiple tracks with a single request, like data = {"trackIds": "2,3,4,10,14,27"}.

The fact that your incorrect attempts to add tracks each added one bogus track with ID 0, is a small bug or an oversight in the Music app.

When trying to figure out how to use the API, one good way is to use the developer tools of your web browser and observe, what kind of requests and responses there are between the web UI and the server of the Music app.

sanfx commented 2 years ago

Thanks, I would really like to know how to figure out the property names for whatever api/# I use. Now for example I tried to do a scan although it is a post request it doesnt hint from the api listing page what are properties available !

So in case of scan I tried this but it threw csrf check failed error


/nextcloud/index.php/apps/music/api/scan'
>>> r = requests.post(scan_url)
>>> r.text
'{"message":"CSRF check failed"}'
>>> 
paulijar commented 2 years ago

To figure out the supported arguments of each REST API method, you need to look into the controller implementations behind those methods. For example, let's look at the previously discussed API endpoint ['name' => 'playlistApi#addTracks', 'url' => '/api/playlists/{id}/add', 'verb' => 'POST'], Here, 'playlistApi#addTracks' tells the location of the implementation. The part before # refers to the controller name and the part after it to the method name. That is, the implementation can be found from the class named PlaylistApiController and there from the method named addTracks. This method is defined here: https://github.com/owncloud/music/blob/fdba21e1aded8ff8333104694c011365d8997751/lib/Controller/PlaylistApiController.php#L205 Now we can see, that the method has two arguments, $id and $trackIds. The first of these gets bound to the {id} value given as part of the URL. The second argument has no corresponding placeholder in the URL and this means that this argument has to be given in the POST data of the request using the key 'trackIds'.

To do the scanning via the REST API, you first need to obtain the list of IDs of the unscanned audio files with using the endpoint /api/scanstate. Then you should pass the IDs of the files to scan to /api/scan in the argument 'files', probably doing this in batches if there are many files to scan.

But now, the CSRF check is another issue. Nextcloud and ownCloud come with built-in mechanisms to prevent Cross-Site Request Forgery by malicious 3rd parties and these are active on all API endpoints by default. I don't know the details of these mechanisms but probably they are based on request headers and/or cookies. The Music app has disabled these mechanisms on most API endpoints but not on /api/scan. Not disabling it there is probably just an oversight because there should be no possibility for an attacker to cause any real harm with a CSRF call to this API. As I have understood it, the CSRF attacks are a real risk only on such API calls which e.g. modify the user password or transfer funds.

paulijar commented 2 years ago

The new release Music v1.7.0 now disables the CSRF prevention also on the API endpoint /api/scan. As I see it, there are no more open topics here and the issue can be closed.