From a6b894c586319bfbcf5a0da96c5cbab77eef19b0 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 10 Mar 2018 22:15:40 +0100 Subject: [PATCH] API: marked explicitly unsupported methods/parameters as such --- docs/api.md | 82 ++++++++++++++++++------------------ supysonic/api/__init__.py | 1 + supysonic/api/browse.py | 4 -- supysonic/api/errors.py | 2 +- supysonic/api/exceptions.py | 5 +++ supysonic/api/media.py | 15 ++++--- supysonic/api/unsupported.py | 22 ++++++++++ tests/api/test_api_setup.py | 16 +++++-- tests/api/test_media.py | 2 + 9 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 supysonic/api/unsupported.py diff --git a/docs/api.md b/docs/api.md index 47fd5c8..9832d90 100644 --- a/docs/api.md +++ b/docs/api.md @@ -69,15 +69,15 @@ or with version 1.8.0. | [`getCaptions`](#getcaptions) | 1.15.0 | πŸ”΄ | | [`getCoverArt`](#getcoverart) | | βœ”οΈ | | [`getLyrics`](#getlyrics) | | βœ”οΈ | -| [`getAvatar`](#getavatar) | | πŸ”΄ | +| [`getAvatar`](#getavatar) | | ❌ | | [`star`](#star) | | βœ”οΈ | | [`unstar`](#unstar) | | βœ”οΈ | | [`setRating`](#setrating) | | βœ”οΈ | | [`scrobble`](#scrobble) | | βœ”οΈ | -| [`getShares`](#getshares) | | πŸ”΄ | -| [`createShare`](#createshare) | | πŸ”΄ | -| [`updateShare`](#updateshare) | | πŸ”΄ | -| [`deleteShare`](#deleteshare) | | πŸ”΄ | +| [`getShares`](#getshares) | | ❌ | +| [`createShare`](#createshare) | | ❌ | +| [`updateShare`](#updateshare) | | ❌ | +| [`deleteShare`](#deleteshare) | | ❌ | | [`getPodcasts`](#getpodcasts) | | ❔ | | [`getNewestPodcasts`](#getnewestpodcasts) | 1.14.0 | ❔ | | [`refreshPodcasts`](#refreshpodcasts) | 1.9.0 | ❔ | @@ -432,8 +432,8 @@ No parameter | `id` | | βœ”οΈ | | `maxBitRate` | | βœ”οΈ | | `format` | | βœ”οΈ | -| `timeOffset` | | πŸ”΄ | -| `size` | | πŸ”΄ | +| `timeOffset` | | ❌ | +| `size` | | ❌ | | `estimateContentLength` | | πŸ“… | | `converted` | 1.15.0 | πŸ”΄ | @@ -478,11 +478,11 @@ No parameter | `title` | | βœ”οΈ | #### `getAvatar` -πŸ”΄ +❌ | Parameter | Vers. | | |------------|-------|---| -| `username` | | πŸ”΄ | +| `username` | | ❌ | ### Media annotation @@ -524,33 +524,33 @@ No parameter ### Sharing #### `getShares` -πŸ”΄ +❌ No parameter #### `createShare` -πŸ”΄ +❌ | Parameter | Vers. | | |---------------|-------|---| -| `id` | | πŸ”΄ | -| `description` | | πŸ”΄ | -| `expires` | | πŸ”΄ | +| `id` | | ❌ | +| `description` | | ❌ | +| `expires` | | ❌ | #### `updateShare` -πŸ”΄ +❌ | Parameter | Vers. | | |---------------|-------|---| -| `id` | | πŸ”΄ | -| `description` | | πŸ”΄ | -| `expires` | | πŸ”΄ | +| `id` | | ❌ | +| `description` | | ❌ | +| `expires` | | ❌ | #### `deleteShare` -πŸ”΄ +❌ | Parameter | Vers. | | |-----------|-------|---| -| `id` | | πŸ”΄ | +| `id` | | ❌ | ### Podcast @@ -687,19 +687,19 @@ No parameter | `username` | | βœ”οΈ | | `password` | | βœ”οΈ | | `email` | | βœ”οΈ | -| `ldapAuthenticated` | | ❔ | +| `ldapAuthenticated` | | | | `adminRole` | | βœ”οΈ | -| `settingsRole` | | ❔ | -| `streamRole` | | ❔ | +| `settingsRole` | | | +| `streamRole` | | | | `jukeboxRole` | | πŸ“… | -| `downloadRole` | | ❔ | -| `uploadRole` | | ❔ | -| `playlistRole` | | ❔ | -| `coverArtRole` | | ❔ | -| `commentRole` | | ❔ | -| `podcastRole` | | ❔ | -| `shareRole` | | πŸ”΄ | -| `videoConversionRole` | 1.14.0 | πŸ”΄ | +| `downloadRole` | | | +| `uploadRole` | | | +| `playlistRole` | | | +| `coverArtRole` | | | +| `commentRole` | | | +| `podcastRole` | | | +| `shareRole` | | | +| `videoConversionRole` | 1.14.0 | | | `musicFolderId` | 1.12.0 | πŸ“… | #### `updateUser` @@ -710,18 +710,18 @@ No parameter | `username` | 1.10.2 | πŸ“… | | `password` | 1.10.2 | πŸ“… | | `email` | 1.10.2 | πŸ“… | -| `ldapAuthenticated` | 1.10.2 | ❔ | +| `ldapAuthenticated` | 1.10.2 | | | `adminRole` | 1.10.2 | πŸ“… | -| `settingsRole` | 1.10.2 | ❔ | -| `streamRole` | 1.10.2 | ❔ | +| `settingsRole` | 1.10.2 | | +| `streamRole` | 1.10.2 | | | `jukeboxRole` | 1.10.2 | πŸ“… | -| `downloadRole` | 1.10.2 | ❔ | -| `uploadRole` | 1.10.2 | ❔ | -| `coverArtRole` | 1.10.2 | ❔ | -| `commentRole` | 1.10.2 | ❔ | -| `podcastRole` | 1.10.2 | ❔ | -| `shareRole` | 1.10.2 | πŸ”΄ | -| `videoConversionRole` | 1.14.0 | πŸ”΄ | +| `downloadRole` | 1.10.2 | | +| `uploadRole` | 1.10.2 | | +| `coverArtRole` | 1.10.2 | | +| `commentRole` | 1.10.2 | | +| `podcastRole` | 1.10.2 | | +| `shareRole` | 1.10.2 | | +| `videoConversionRole` | 1.14.0 | | | `musicFolderId` | 1.12.0 | πŸ“… | | `maxBitRate` | 1.13.0 | πŸ“… | diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index aeab867..3d52c93 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -88,4 +88,5 @@ from .annotation import * from .chat import * from .search import * from .playlists import * +from .unsupported import * diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index 4e4a9f7..5c36547 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -139,7 +139,3 @@ def track_info(): res = get_entity(Track) return request.formatter('song', res.as_subsonic_child(request.user, request.client)) -@api.route('/getVideos.view', methods = [ 'GET', 'POST' ]) -def list_videos(): - return request.formatter.error(0, 'Video streaming not supported'), 501 - diff --git a/supysonic/api/errors.py b/supysonic/api/errors.py index 7eef4f2..818ec39 100644 --- a/supysonic/api/errors.py +++ b/supysonic/api/errors.py @@ -38,5 +38,5 @@ def generic_error(e): # pragma: nocover #@api.errorhandler(404) @api.route('/', methods = [ 'GET', 'POST' ]) # blueprint 404 workaround def not_found(*args, **kwargs): - return GenericError('Not implemented'), 501 + return GenericError('Unknown method'), 404 diff --git a/supysonic/api/exceptions.py b/supysonic/api/exceptions.py index fbee276..62cf477 100644 --- a/supysonic/api/exceptions.py +++ b/supysonic/api/exceptions.py @@ -34,6 +34,11 @@ class GenericError(SubsonicAPIException): class ServerError(GenericError): code = 500 +class UnsupportedParameter(GenericError): + def __init__(self, parameter, *args, **kwargs): + message = "Unsupported parameter '{}'".format(parameter) + super(UnsupportedParameter, self).__init__(message, *args, **kwargs) + class MissingParameter(SubsonicAPIException): api_code = 10 diff --git a/supysonic/api/media.py b/supysonic/api/media.py index d01a228..19c64ff 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -23,7 +23,7 @@ from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now from ..py23 import dict from . import api, get_entity -from .exceptions import GenericError, MissingParameter, NotFound, ServerError +from .exceptions import GenericError, MissingParameter, NotFound, ServerError, UnsupportedParameter def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): if not base_cmdline: @@ -39,7 +39,12 @@ def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_f def stream_media(): res = get_entity(Track) - maxBitRate, format, timeOffset, size, estimateContentLength = map(request.values.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength' ]) + if 'timeOffset' in request.values: + raise UnsupportedParameter('timeOffset') + if 'size' in request.values: + raise UnsupportedParameter('size') + + maxBitRate, format, estimateContentLength = map(request.values.get, [ 'maxBitRate', 'format', 'estimateContentLength' ]) if format: format = format.lower() @@ -94,7 +99,7 @@ def stream_media(): if not data: break yield data - except: + except: # pragma: nocover if dec_proc != None: dec_proc.terminate() proc.terminate() @@ -184,10 +189,10 @@ def lyrics(): title = root.find('cl:LyricSong', namespaces = ns).text, _value_ = root.find('cl:Lyric', namespaces = ns).text )) - except requests.exceptions.RequestException as e: + except requests.exceptions.RequestException as e: # pragma: nocover current_app.logger.warning('Error while requesting the ChartLyrics API: ' + str(e)) - return request.formatter('lyrics', dict()) + return request.formatter('lyrics', dict()) # pragma: nocover def read_file_as_unicode(path): """ Opens a file trying with different encodings and returns the contents as a unicode string """ diff --git a/supysonic/api/unsupported.py b/supysonic/api/unsupported.py new file mode 100644 index 0000000..6e12a5c --- /dev/null +++ b/supysonic/api/unsupported.py @@ -0,0 +1,22 @@ +# coding: utf-8 +# +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2018 Alban 'spl0k' FΓ©ron +# +# Distributed under terms of the GNU AGPLv3 license. + +from . import api +from .exceptions import GenericError + +methods = ( + 'getVideos', 'getAvatar', 'getShares', 'createShare', 'updateShare', 'deleteShare', +) + +def unsupported(): + return GenericError('Not supported by Supysonic'), 501 + +for m in methods: + api.add_url_rule('/{}.view'.format(m), 'unsupported', unsupported, methods = [ 'GET', 'POST' ]) + diff --git a/tests/api/test_api_setup.py b/tests/api/test_api_setup.py index a4ea0b9..e328f32 100644 --- a/tests/api/test_api_setup.py +++ b/tests/api/test_api_setup.py @@ -128,13 +128,23 @@ class ApiSetupTestCase(TestBase): self.assertIn('license', json['subsonic-response']) def test_not_implemented(self): - # Access to not implemented endpoint - rv = self.client.get('/rest/not-implemented', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + # Access to not implemented/unknown endpoint + rv = self.client.get('/rest/unknown', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + self.assertEqual(rv.status_code, 404) + self.assertIn('status="failed"', rv.data) + self.assertIn('code="0"', rv.data) + + rv = self.client.post('/rest/unknown', data = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + self.assertEqual(rv.status_code, 404) + self.assertIn('status="failed"', rv.data) + self.assertIn('code="0"', rv.data) + + rv = self.client.get('/rest/getVideos.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) self.assertEqual(rv.status_code, 501) self.assertIn('status="failed"', rv.data) self.assertIn('code="0"', rv.data) - rv = self.client.post('/rest/not-implemented', data = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + rv = self.client.post('/rest/getVideos.view', data = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) self.assertEqual(rv.status_code, 501) self.assertIn('status="failed"', rv.data) self.assertIn('code="0"', rv.data) diff --git a/tests/api/test_media.py b/tests/api/test_media.py index f169e0d..ec6b5bd 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -57,6 +57,8 @@ class MediaTestCase(ApiTestBase): self._make_request('stream', { 'id': str(uuid.uuid4()) }, error = 70) self._make_request('stream', { 'id': str(self.folderid) }, error = 70) self._make_request('stream', { 'id': str(self.trackid), 'maxBitRate': 'string' }, error = 0) + self._make_request('stream', { 'id': str(self.trackid), 'timeOffset': 2 }, error = 0) + self._make_request('stream', { 'id': str(self.trackid), 'size': '640x480' }, error = 0) rv = self.client.get('/rest/stream.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) self.assertEqual(rv.status_code, 200)