1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 17:06:17 +00:00

Merge branch 'master' into daemon-rework

This commit is contained in:
spl0k 2019-06-01 16:13:16 +02:00
commit e6a192483c
23 changed files with 115 additions and 36 deletions

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
@ -23,7 +23,7 @@ def value_error(e):
@api.errorhandler(BadRequestKeyError) @api.errorhandler(BadRequestKeyError)
def key_error(e): def key_error(e):
rollback() rollback()
return MissingParameter(e.args[0]) return MissingParameter()
@api.errorhandler(ObjectNotFound) @api.errorhandler(ObjectNotFound)
def not_found(e): def not_found(e):

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
@ -42,9 +42,9 @@ class UnsupportedParameter(GenericError):
class MissingParameter(SubsonicAPIException): class MissingParameter(SubsonicAPIException):
api_code = 10 api_code = 10
def __init__(self, param, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MissingParameter, self).__init__(*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): class ClientMustUpgrade(SubsonicAPIException):
api_code = 20 api_code = 20

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 2018-2019 Carey 'pR0Ps' Metcalfe
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
@ -65,7 +65,7 @@ def stream_media():
src_suffix = res.suffix() src_suffix = res.suffix()
dst_suffix = res.suffix() dst_suffix = res.suffix()
dst_bitrate = res.bitrate dst_bitrate = res.bitrate
dst_mimetype = res.content_type dst_mimetype = res.mimetype
prefs = request.client prefs = request.client
if prefs.format: if prefs.format:
@ -153,7 +153,7 @@ def download_media():
try: # Track -> direct download try: # Track -> direct download
rv = Track[uid] 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: except ObjectNotFound:
pass pass

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
@ -31,7 +31,7 @@ try:
except ImportError: except ImportError:
from urlparse import urlparse, parse_qsl from urlparse import urlparse, parse_qsl
SCHEMA_VERSION = '20190324' SCHEMA_VERSION = '20190518'
def now(): def now():
return datetime.now().replace(microsecond = 0) return datetime.now().replace(microsecond = 0)
@ -74,11 +74,11 @@ class Folder(PathMixin, db.Entity):
id = PrimaryKey(UUID, default = uuid4) id = PrimaryKey(UUID, default = uuid4)
root = Required(bool, default = False) root = Required(bool, default = False)
name = Required(str) name = Required(str, autostrip = False)
path = Required(str, 4096) # unique path = Required(str, 4096, autostrip = False) # unique
_path_hash = Required(buffer, column = 'path_hash') _path_hash = Required(buffer, column = 'path_hash')
created = Required(datetime, precision = 0, default = now) 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) last_scan = Required(int, default = 0)
parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id') parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id')
@ -227,9 +227,8 @@ class Track(PathMixin, db.Entity):
bitrate = Required(int) bitrate = Required(int)
path = Required(str, 4096) # unique path = Required(str, 4096, autostrip = False) # unique
_path_hash = Required(buffer, column = 'path_hash') _path_hash = Required(buffer, column = 'path_hash')
content_type = Required(str)
created = Required(datetime, precision = 0, default = now) created = Required(datetime, precision = 0, default = now)
last_modification = Required(int) last_modification = Required(int)
@ -254,7 +253,7 @@ class Track(PathMixin, db.Entity):
artist = self.artist.name, artist = self.artist.name,
track = self.number, track = self.number,
size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1, size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1,
contentType = self.content_type, contentType = self.mimetype,
suffix = self.suffix(), suffix = self.suffix(),
duration = self.duration, duration = self.duration,
bitRate = self.bitrate, bitRate = self.bitrate,
@ -296,6 +295,10 @@ class Track(PathMixin, db.Entity):
return info return info
@property
def mimetype(self):
return mimetypes.guess_type(self.path, False)[0] or 'application/octet-stream'
def duration_str(self): def duration_str(self):
ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60) ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60)
if self.duration >= 3600: if self.duration >= 3600:

View File

@ -9,7 +9,6 @@
import logging import logging
import os, os.path import os, os.path
import mimetypes
import mutagen import mutagen
import time import time
@ -224,7 +223,6 @@ class Scanner(Thread):
trdict['has_art'] = bool(Track._extract_cover_art(path)) 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['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 trdict['last_modification'] = mtime
tralbum = self.__find_album(albumartist, album) tralbum = self.__find_album(albumartist, album)
@ -293,6 +291,9 @@ class Scanner(Thread):
if not isinstance(dirpath, strtype): # pragma: nocover if not isinstance(dirpath, strtype): # pragma: nocover
raise TypeError('Expecting string, got ' + str(type(dirpath))) raise TypeError('Expecting string, got ' + str(type(dirpath)))
if not os.path.exists(dirpath):
return
folder = Folder.get(path = dirpath) folder = Folder.get(path = dirpath)
if folder is None: if folder is None:
return return

View File

@ -0,0 +1 @@
ALTER TABLE track DROP COLUMN content_type;

View File

@ -0,0 +1 @@
ALTER TABLE track DROP COLUMN content_type;

View File

@ -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;

View File

@ -37,7 +37,6 @@ CREATE TABLE IF NOT EXISTS track (
bitrate INTEGER NOT NULL, bitrate INTEGER NOT NULL,
path VARCHAR(4096) NOT NULL, path VARCHAR(4096) NOT NULL,
path_hash BINARY(20) UNIQUE NOT NULL, path_hash BINARY(20) UNIQUE NOT NULL,
content_type VARCHAR(32) NOT NULL,
created DATETIME NOT NULL, created DATETIME NOT NULL,
last_modification INTEGER NOT NULL, last_modification INTEGER NOT NULL,
play_count INTEGER NOT NULL, play_count INTEGER NOT NULL,

View File

@ -37,7 +37,6 @@ CREATE TABLE IF NOT EXISTS track (
bitrate INTEGER NOT NULL, bitrate INTEGER NOT NULL,
path VARCHAR(4096) NOT NULL, path VARCHAR(4096) NOT NULL,
path_hash BYTEA UNIQUE NOT NULL, path_hash BYTEA UNIQUE NOT NULL,
content_type VARCHAR(32) NOT NULL,
created TIMESTAMP NOT NULL, created TIMESTAMP NOT NULL,
last_modification INTEGER NOT NULL, last_modification INTEGER NOT NULL,
play_count INTEGER NOT NULL, play_count INTEGER NOT NULL,

View File

@ -38,7 +38,6 @@ CREATE TABLE IF NOT EXISTS track (
bitrate INTEGER NOT NULL, bitrate INTEGER NOT NULL,
path VARCHAR(4096) NOT NULL, path VARCHAR(4096) NOT NULL,
path_hash BLOB NOT NULL, path_hash BLOB NOT NULL,
content_type VARCHAR(32) NOT NULL,
created DATETIME NOT NULL, created DATETIME NOT NULL,
last_modification INTEGER NOT NULL, last_modification INTEGER NOT NULL,
play_count INTEGER NOT NULL, play_count INTEGER NOT NULL,

View File

@ -19,6 +19,7 @@ from .issue101 import Issue101TestCase
from .issue129 import Issue129TestCase from .issue129 import Issue129TestCase
from .issue133 import Issue133TestCase from .issue133 import Issue133TestCase
from .issue139 import Issue139TestCase from .issue139 import Issue139TestCase
from .issue148 import Issue148TestCase
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -31,6 +32,7 @@ def suite():
suite.addTest(unittest.makeSuite(Issue129TestCase)) suite.addTest(unittest.makeSuite(Issue129TestCase))
suite.addTest(unittest.makeSuite(Issue133TestCase)) suite.addTest(unittest.makeSuite(Issue133TestCase))
suite.addTest(unittest.makeSuite(Issue139TestCase)) suite.addTest(unittest.makeSuite(Issue139TestCase))
suite.addTest(unittest.makeSuite(Issue148TestCase))
return suite return suite

View File

@ -39,7 +39,6 @@ class AlbumSongsTestCase(ApiTestBase):
root_folder = folder, root_folder = folder,
duration = 2, duration = 2,
bitrate = 320, bitrate = 320,
content_type = 'audio/mpeg',
last_modification = 0 last_modification = 0
) )

View File

@ -37,7 +37,6 @@ class AnnotationTestCase(ApiTestBase):
root_folder = root, root_folder = root,
duration = 2, duration = 2,
bitrate = 320, bitrate = 320,
content_type = 'audio/mpeg',
last_modification = 0 last_modification = 0
) )

View File

@ -54,7 +54,6 @@ class BrowseTestCase(ApiTestBase):
artist = artist, artist = artist,
bitrate = 320, bitrate = 320,
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
content_type = 'audio/mpeg',
last_modification = 0, last_modification = 0,
root_folder = root, root_folder = root,
folder = afolder folder = afolder

View File

@ -48,7 +48,6 @@ class MediaTestCase(ApiTestBase):
folder = folder, folder = folder,
duration = 2, duration = 2,
bitrate = 320, bitrate = 320,
content_type = 'audio/mpeg',
last_modification = 0 last_modification = 0
) )
self.trackid = track.id self.trackid = track.id
@ -66,7 +65,6 @@ class MediaTestCase(ApiTestBase):
folder = folder, folder = folder,
duration = 2, duration = 2,
bitrate = 320, bitrate = 320,
content_type = 'audio/{0}'.format(self.formats[i][1]),
last_modification = 0 last_modification = 0
) )
self.formats[i] = track_embeded_art.id 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) }) 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.status_code, 200)
self.assertEqual(rv.mimetype, 'audio/mpeg')
self.assertEqual(len(rv.data), 23) self.assertEqual(len(rv.data), 23)
with db_session: with db_session:
self.assertEqual(Track[self.trackid].play_count, 1) self.assertEqual(Track[self.trackid].play_count, 1)
@ -95,7 +92,6 @@ class MediaTestCase(ApiTestBase):
# download single file # download single file
rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) 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.status_code, 200)
self.assertEqual(rv.mimetype, 'audio/mpeg')
self.assertEqual(len(rv.data), 23) self.assertEqual(len(rv.data), 23)
with db_session: with db_session:
self.assertEqual(Track[self.trackid].play_count, 0) self.assertEqual(Track[self.trackid].play_count, 0)

View File

@ -36,7 +36,6 @@ class PlaylistTestCase(ApiTestBase):
artist = artist, artist = artist,
bitrate = 320, bitrate = 320,
path = 'tests/assets/' + song, path = 'tests/assets/' + song,
content_type = 'audio/mpeg',
last_modification = 0, last_modification = 0,
root_folder = root, root_folder = root,
folder = root folder = root

View File

@ -47,7 +47,6 @@ class SearchTestCase(ApiTestBase):
artist = artist, artist = artist,
bitrate = 320, bitrate = 320,
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
content_type = 'audio/mpeg',
last_modification = 0, last_modification = 0,
root_folder = root, root_folder = root,
folder = afolder folder = afolder

View File

@ -74,7 +74,6 @@ class DbTestCase(unittest.TestCase):
has_art = True, has_art = True,
bitrate = 320, bitrate = 320,
path = 'tests/assets/formats/silence.ogg', path = 'tests/assets/formats/silence.ogg',
content_type = 'audio/ogg',
last_modification = 1234, last_modification = 1234,
root_folder = root, root_folder = root,
folder = child folder = child
@ -89,7 +88,6 @@ class DbTestCase(unittest.TestCase):
duration = 5, duration = 5,
bitrate = 96, bitrate = 96,
path = 'tests/assets/23bytes', path = 'tests/assets/23bytes',
content_type = 'audio/mpeg',
last_modification = 1234, last_modification = 1234,
root_folder = root, root_folder = root,
folder = child folder = child
@ -110,7 +108,6 @@ class DbTestCase(unittest.TestCase):
has_art = has_art, has_art = has_art,
bitrate = 96, bitrate = 96,
path = 'tests/assets/formats/silence.flac', path = 'tests/assets/formats/silence.flac',
content_type = 'audio/flac',
last_modification = 1234, last_modification = 1234,
root_folder = root, root_folder = root,
folder = folder folder = folder

View File

@ -35,7 +35,6 @@ class PlaylistTestCase(FrontendTestBase):
duration = 2, duration = 2,
disc = 1, disc = 1,
number = 1, number = 1,
content_type = 'audio/mpeg',
bitrate = 320, bitrate = 320,
last_modification = 0 last_modification = 0
) )

46
tests/issue148.py Normal file
View File

@ -0,0 +1,46 @@
# 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()
scanner.queue_folder('folder')
scanner.run()
del scanner
if __name__ == '__main__':
unittest.main()

View File

@ -59,7 +59,6 @@ class FolderManagerTestCase(unittest.TestCase):
folder = root, folder = root,
root_folder = root, root_folder = root,
duration = 2, duration = 2,
content_type = 'audio/mpeg',
bitrate = 320, bitrate = 320,
last_modification = 0 last_modification = 0
) )

View File

@ -48,7 +48,6 @@ class UserManagerTestCase(unittest.TestCase):
path = 'tests/assets/empty', path = 'tests/assets/empty',
folder = folder, folder = folder,
root_folder = folder, root_folder = folder,
content_type = 'audio/mpeg',
bitrate = 320, bitrate = 320,
last_modification = 0 last_modification = 0
) )