From deaf17a0052156b91232129cee6ac57938dfd08c Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 11 May 2019 16:08:04 +0200 Subject: [PATCH 1/4] Fixes #148 (and other possible related issues) --- supysonic/db.py | 8 ++++---- tests/__init__.py | 2 ++ tests/issue148.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/issue148.py diff --git a/supysonic/db.py b/supysonic/db.py index f283e8e..6d4bb49 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -74,11 +74,11 @@ class Folder(PathMixin, db.Entity): id = PrimaryKey(UUID, default = uuid4) root = Required(bool, default = False) - name = Required(str) - path = Required(str, 4096) # unique + name = Required(str, autostrip = False) + path = Required(str, 4096, autostrip = False) # unique _path_hash = Required(buffer, column = 'path_hash') created = Required(datetime, precision = 0, default = now) - cover_art = Optional(str, nullable = True) + cover_art = Optional(str, nullable = True, autostrip = False) last_scan = Required(int, default = 0) parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id') @@ -227,7 +227,7 @@ class Track(PathMixin, db.Entity): bitrate = Required(int) - path = Required(str, 4096) # unique + path = Required(str, 4096, autostrip = False) # unique _path_hash = Required(buffer, column = 'path_hash') content_type = Required(str) created = Required(datetime, precision = 0, default = now) diff --git a/tests/__init__.py b/tests/__init__.py index 6df5548..cdc0b64 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -19,6 +19,7 @@ from .issue101 import Issue101TestCase from .issue129 import Issue129TestCase from .issue133 import Issue133TestCase from .issue139 import Issue139TestCase +from .issue148 import Issue148TestCase def suite(): suite = unittest.TestSuite() @@ -31,6 +32,7 @@ def suite(): suite.addTest(unittest.makeSuite(Issue129TestCase)) suite.addTest(unittest.makeSuite(Issue133TestCase)) suite.addTest(unittest.makeSuite(Issue139TestCase)) + suite.addTest(unittest.makeSuite(Issue148TestCase)) return suite diff --git a/tests/issue148.py b/tests/issue148.py new file mode 100644 index 0000000..6b6c706 --- /dev/null +++ b/tests/issue148.py @@ -0,0 +1,47 @@ +# coding: utf-8 +# +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2019 Alban 'spl0k' Féron +# +# Distributed under terms of the GNU AGPLv3 license. + +import os.path +import shutil +import tempfile +import unittest + +from pony.orm import db_session + +from supysonic.db import init_database, release_database +from supysonic.db import Folder +from supysonic.managers.folder import FolderManager +from supysonic.scanner import Scanner + +class Issue148TestCase(unittest.TestCase): + def setUp(self): + self.__dir = tempfile.mkdtemp() + init_database('sqlite:') + with db_session: + FolderManager.add('folder', self.__dir) + + def tearDown(self): + release_database() + shutil.rmtree(self.__dir) + + def test_issue(self): + subdir = os.path.join(self.__dir, ' ') + os.makedirs(subdir) + shutil.copyfile('tests/assets/folder/silence.mp3', os.path.join(subdir, 'silence.mp3')) + + scanner = Scanner() + with db_session: + folder = Folder.select(lambda f: f.root).first() + scanner.scan(folder) + scanner.finish() + + +if __name__ == '__main__': + unittest.main() + From 10df0ada0717df7d8776c098379ef4681a5d6d11 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 18 May 2019 14:53:26 +0200 Subject: [PATCH 2/4] Don't store the mimetype in database That's useless, it can be deduced from the path Fixes #150 --- supysonic/api/media.py | 6 +-- supysonic/db.py | 11 +++-- supysonic/scanner.py | 4 +- supysonic/schema/migration/mysql/20190518.sql | 1 + .../schema/migration/postgres/20190518.sql | 1 + .../schema/migration/sqlite/20190518.sql | 43 +++++++++++++++++++ supysonic/schema/mysql.sql | 1 - supysonic/schema/postgres.sql | 1 - supysonic/schema/sqlite.sql | 1 - tests/api/test_album_songs.py | 1 - tests/api/test_annotation.py | 1 - tests/api/test_browse.py | 1 - tests/api/test_media.py | 4 -- tests/api/test_playlist.py | 1 - tests/api/test_search.py | 1 - tests/base/test_db.py | 3 -- tests/frontend/test_playlist.py | 1 - tests/managers/test_manager_folder.py | 1 - tests/managers/test_manager_user.py | 1 - 19 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 supysonic/schema/migration/mysql/20190518.sql create mode 100644 supysonic/schema/migration/postgres/20190518.sql create mode 100644 supysonic/schema/migration/sqlite/20190518.sql diff --git a/supysonic/api/media.py b/supysonic/api/media.py index 98cd7cd..49ae9a7 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2018 Alban 'spl0k' Féron +# Copyright (C) 2013-2019 Alban 'spl0k' Féron # 2018-2019 Carey 'pR0Ps' Metcalfe # # Distributed under terms of the GNU AGPLv3 license. @@ -65,7 +65,7 @@ def stream_media(): src_suffix = res.suffix() dst_suffix = res.suffix() dst_bitrate = res.bitrate - dst_mimetype = res.content_type + dst_mimetype = res.mimetype prefs = request.client if prefs.format: @@ -153,7 +153,7 @@ def download_media(): try: # Track -> direct download rv = Track[uid] - return send_file(rv.path, mimetype = rv.content_type, conditional=True) + return send_file(rv.path, mimetype = rv.mimetype, conditional=True) except ObjectNotFound: pass diff --git a/supysonic/db.py b/supysonic/db.py index 6d4bb49..cb85005 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2018 Alban 'spl0k' Féron +# Copyright (C) 2013-2019 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -31,7 +31,7 @@ try: except ImportError: from urlparse import urlparse, parse_qsl -SCHEMA_VERSION = '20190324' +SCHEMA_VERSION = '20190518' def now(): return datetime.now().replace(microsecond = 0) @@ -229,7 +229,6 @@ class Track(PathMixin, db.Entity): path = Required(str, 4096, autostrip = False) # unique _path_hash = Required(buffer, column = 'path_hash') - content_type = Required(str) created = Required(datetime, precision = 0, default = now) last_modification = Required(int) @@ -254,7 +253,7 @@ class Track(PathMixin, db.Entity): artist = self.artist.name, track = self.number, size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1, - contentType = self.content_type, + contentType = self.mimetype, suffix = self.suffix(), duration = self.duration, bitRate = self.bitrate, @@ -296,6 +295,10 @@ class Track(PathMixin, db.Entity): return info + @property + def mimetype(self): + return mimetypes.guess_type(self.path, False)[0] or 'application/octet-stream' + def duration_str(self): ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60) if self.duration >= 3600: diff --git a/supysonic/scanner.py b/supysonic/scanner.py index d00b76c..5493603 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -3,12 +3,11 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2018 Alban 'spl0k' Féron +# Copyright (C) 2013-2019 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. import os, os.path -import mimetypes import mutagen import time @@ -145,7 +144,6 @@ class Scanner: trdict['has_art'] = bool(Track._extract_cover_art(path)) trdict['bitrate'] = int(tag.info.bitrate if hasattr(tag.info, 'bitrate') else os.path.getsize(path) * 8 / tag.info.length) // 1000 - trdict['content_type'] = mimetypes.guess_type(path, False)[0] or 'application/octet-stream' trdict['last_modification'] = mtime tralbum = self.__find_album(albumartist, album) diff --git a/supysonic/schema/migration/mysql/20190518.sql b/supysonic/schema/migration/mysql/20190518.sql new file mode 100644 index 0000000..9e4ce8e --- /dev/null +++ b/supysonic/schema/migration/mysql/20190518.sql @@ -0,0 +1 @@ +ALTER TABLE track DROP COLUMN content_type; diff --git a/supysonic/schema/migration/postgres/20190518.sql b/supysonic/schema/migration/postgres/20190518.sql new file mode 100644 index 0000000..9e4ce8e --- /dev/null +++ b/supysonic/schema/migration/postgres/20190518.sql @@ -0,0 +1 @@ +ALTER TABLE track DROP COLUMN content_type; diff --git a/supysonic/schema/migration/sqlite/20190518.sql b/supysonic/schema/migration/sqlite/20190518.sql new file mode 100644 index 0000000..5c4be5e --- /dev/null +++ b/supysonic/schema/migration/sqlite/20190518.sql @@ -0,0 +1,43 @@ +DROP INDEX index_track_album_id_fk; +DROP INDEX index_track_artist_id_fk; +DROP INDEX index_track_folder_id_fk; +DROP INDEX index_track_root_folder_id_fk; +DROP INDEX index_track_path; + +ALTER TABLE track RENAME TO track_old; + +CREATE TABLE IF NOT EXISTS track ( + id CHAR(36) PRIMARY KEY, + disc INTEGER NOT NULL, + number INTEGER NOT NULL, + title VARCHAR(256) NOT NULL COLLATE NOCASE, + year INTEGER, + genre VARCHAR(256), + duration INTEGER NOT NULL, + has_art BOOLEAN NOT NULL DEFAULT false, + album_id CHAR(36) NOT NULL REFERENCES album, + artist_id CHAR(36) NOT NULL REFERENCES artist, + bitrate INTEGER NOT NULL, + path VARCHAR(4096) NOT NULL, + path_hash BLOB NOT NULL, + created DATETIME NOT NULL, + last_modification INTEGER NOT NULL, + play_count INTEGER NOT NULL, + last_play DATETIME, + root_folder_id CHAR(36) NOT NULL REFERENCES folder, + folder_id CHAR(36) NOT NULL REFERENCES folder +); +CREATE INDEX IF NOT EXISTS index_track_album_id_fk ON track(album_id); +CREATE INDEX IF NOT EXISTS index_track_artist_id_fk ON track(artist_id); +CREATE INDEX IF NOT EXISTS index_track_folder_id_fk ON track(folder_id); +CREATE INDEX IF NOT EXISTS index_track_root_folder_id_fk ON track(root_folder_id); +CREATE UNIQUE INDEX IF NOT EXISTS index_track_path ON track(path_hash); + +INSERT INTO track(id, disc, number, title, year, genre, duration, has_art, album_id, artist_id, bitrate, path, path_hash, created, last_modification, play_count, last_play, root_folder_id, folder_id) +SELECT id, disc, number, title, year, genre, duration, has_art, album_id, artist_id, bitrate, path, path_hash, created, last_modification, play_count, last_play, root_folder_id, folder_id +FROM track_old; + +DROP TABLE track_old; + +COMMIT; +VACUUM; diff --git a/supysonic/schema/mysql.sql b/supysonic/schema/mysql.sql index aad2101..864b2a8 100644 --- a/supysonic/schema/mysql.sql +++ b/supysonic/schema/mysql.sql @@ -37,7 +37,6 @@ CREATE TABLE IF NOT EXISTS track ( bitrate INTEGER NOT NULL, path VARCHAR(4096) NOT NULL, path_hash BINARY(20) UNIQUE NOT NULL, - content_type VARCHAR(32) NOT NULL, created DATETIME NOT NULL, last_modification INTEGER NOT NULL, play_count INTEGER NOT NULL, diff --git a/supysonic/schema/postgres.sql b/supysonic/schema/postgres.sql index 32ee459..95c9e94 100644 --- a/supysonic/schema/postgres.sql +++ b/supysonic/schema/postgres.sql @@ -37,7 +37,6 @@ CREATE TABLE IF NOT EXISTS track ( bitrate INTEGER NOT NULL, path VARCHAR(4096) NOT NULL, path_hash BYTEA UNIQUE NOT NULL, - content_type VARCHAR(32) NOT NULL, created TIMESTAMP NOT NULL, last_modification INTEGER NOT NULL, play_count INTEGER NOT NULL, diff --git a/supysonic/schema/sqlite.sql b/supysonic/schema/sqlite.sql index fae2591..23a7ff8 100644 --- a/supysonic/schema/sqlite.sql +++ b/supysonic/schema/sqlite.sql @@ -38,7 +38,6 @@ CREATE TABLE IF NOT EXISTS track ( bitrate INTEGER NOT NULL, path VARCHAR(4096) NOT NULL, path_hash BLOB NOT NULL, - content_type VARCHAR(32) NOT NULL, created DATETIME NOT NULL, last_modification INTEGER NOT NULL, play_count INTEGER NOT NULL, diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index e77d5e7..7ff080e 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -39,7 +39,6 @@ class AlbumSongsTestCase(ApiTestBase): root_folder = folder, duration = 2, bitrate = 320, - content_type = 'audio/mpeg', last_modification = 0 ) diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py index 52f81f7..b9aae03 100644 --- a/tests/api/test_annotation.py +++ b/tests/api/test_annotation.py @@ -37,7 +37,6 @@ class AnnotationTestCase(ApiTestBase): root_folder = root, duration = 2, bitrate = 320, - content_type = 'audio/mpeg', last_modification = 0 ) diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py index 05ca9cb..990590f 100644 --- a/tests/api/test_browse.py +++ b/tests/api/test_browse.py @@ -54,7 +54,6 @@ class BrowseTestCase(ApiTestBase): artist = artist, bitrate = 320, path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), - content_type = 'audio/mpeg', last_modification = 0, root_folder = root, folder = afolder diff --git a/tests/api/test_media.py b/tests/api/test_media.py index 6348e70..fec96d7 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -48,7 +48,6 @@ class MediaTestCase(ApiTestBase): folder = folder, duration = 2, bitrate = 320, - content_type = 'audio/mpeg', last_modification = 0 ) self.trackid = track.id @@ -66,7 +65,6 @@ class MediaTestCase(ApiTestBase): folder = folder, duration = 2, bitrate = 320, - content_type = 'audio/{0}'.format(self.formats[i][1]), last_modification = 0 ) self.formats[i] = track_embeded_art.id @@ -82,7 +80,6 @@ class MediaTestCase(ApiTestBase): 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) - self.assertEqual(rv.mimetype, 'audio/mpeg') self.assertEqual(len(rv.data), 23) with db_session: self.assertEqual(Track[self.trackid].play_count, 1) @@ -95,7 +92,6 @@ class MediaTestCase(ApiTestBase): # download single file rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'audio/mpeg') self.assertEqual(len(rv.data), 23) with db_session: self.assertEqual(Track[self.trackid].play_count, 0) diff --git a/tests/api/test_playlist.py b/tests/api/test_playlist.py index 0e9b063..5e3e682 100644 --- a/tests/api/test_playlist.py +++ b/tests/api/test_playlist.py @@ -36,7 +36,6 @@ class PlaylistTestCase(ApiTestBase): artist = artist, bitrate = 320, path = 'tests/assets/' + song, - content_type = 'audio/mpeg', last_modification = 0, root_folder = root, folder = root diff --git a/tests/api/test_search.py b/tests/api/test_search.py index 8216257..1f6b55d 100644 --- a/tests/api/test_search.py +++ b/tests/api/test_search.py @@ -47,7 +47,6 @@ class SearchTestCase(ApiTestBase): artist = artist, bitrate = 320, path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), - content_type = 'audio/mpeg', last_modification = 0, root_folder = root, folder = afolder diff --git a/tests/base/test_db.py b/tests/base/test_db.py index e99c7a5..1095599 100644 --- a/tests/base/test_db.py +++ b/tests/base/test_db.py @@ -74,7 +74,6 @@ class DbTestCase(unittest.TestCase): has_art = True, bitrate = 320, path = 'tests/assets/formats/silence.ogg', - content_type = 'audio/ogg', last_modification = 1234, root_folder = root, folder = child @@ -89,7 +88,6 @@ class DbTestCase(unittest.TestCase): duration = 5, bitrate = 96, path = 'tests/assets/23bytes', - content_type = 'audio/mpeg', last_modification = 1234, root_folder = root, folder = child @@ -110,7 +108,6 @@ class DbTestCase(unittest.TestCase): has_art = has_art, bitrate = 96, path = 'tests/assets/formats/silence.flac', - content_type = 'audio/flac', last_modification = 1234, root_folder = root, folder = folder diff --git a/tests/frontend/test_playlist.py b/tests/frontend/test_playlist.py index 67bdb22..453c460 100644 --- a/tests/frontend/test_playlist.py +++ b/tests/frontend/test_playlist.py @@ -35,7 +35,6 @@ class PlaylistTestCase(FrontendTestBase): duration = 2, disc = 1, number = 1, - content_type = 'audio/mpeg', bitrate = 320, last_modification = 0 ) diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py index 7070734..9198057 100644 --- a/tests/managers/test_manager_folder.py +++ b/tests/managers/test_manager_folder.py @@ -59,7 +59,6 @@ class FolderManagerTestCase(unittest.TestCase): folder = root, root_folder = root, duration = 2, - content_type = 'audio/mpeg', bitrate = 320, last_modification = 0 ) diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index dcb52ee..07a5b89 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -48,7 +48,6 @@ class UserManagerTestCase(unittest.TestCase): path = 'tests/assets/empty', folder = folder, root_folder = folder, - content_type = 'audio/mpeg', bitrate = 320, last_modification = 0 ) From ebea356901bfb57fa617fa9045b8acb8525b8b2c Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 18 May 2019 16:43:32 +0200 Subject: [PATCH 3/4] Fix for werkzeug 0.15 --- supysonic/api/errors.py | 4 ++-- supysonic/api/exceptions.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/supysonic/api/errors.py b/supysonic/api/errors.py index 6d03629..55da786 100644 --- a/supysonic/api/errors.py +++ b/supysonic/api/errors.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2018 Alban 'spl0k' Féron +# Copyright (C) 2018-2019 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -23,7 +23,7 @@ def value_error(e): @api.errorhandler(BadRequestKeyError) def key_error(e): rollback() - return MissingParameter(e.args[0]) + return MissingParameter() @api.errorhandler(ObjectNotFound) def not_found(e): diff --git a/supysonic/api/exceptions.py b/supysonic/api/exceptions.py index 62cf477..6bfec0e 100644 --- a/supysonic/api/exceptions.py +++ b/supysonic/api/exceptions.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2018 Alban 'spl0k' Féron +# Copyright (C) 2018-2019 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -42,9 +42,9 @@ class UnsupportedParameter(GenericError): class MissingParameter(SubsonicAPIException): api_code = 10 - def __init__(self, param, *args, **kwargs): + def __init__(self, *args, **kwargs): super(MissingParameter, self).__init__(*args, **kwargs) - self.message = "Required parameter '{}' is missing.".format(param) + self.message = "A required parameter is missing." class ClientMustUpgrade(SubsonicAPIException): api_code = 20 From e5716b417a6f60384a5a7c449c76b06c856bb051 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 1 Jun 2019 14:53:06 +0200 Subject: [PATCH 4/4] Fixed watcher errors when moving/deleting folders containing a cover --- supysonic/scanner.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 5493603..e7e790b 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -212,6 +212,9 @@ class Scanner: if not isinstance(dirpath, strtype): # pragma: nocover raise TypeError('Expecting string, got ' + str(type(dirpath))) + if not os.path.exists(dirpath): + return + folder = Folder.get(path = dirpath) if folder is None: return