1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 08:56: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.
# 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):

View File

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

View File

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

View File

@ -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)
@ -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,9 +227,8 @@ 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)
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:

View File

@ -9,7 +9,6 @@
import logging
import os, os.path
import mimetypes
import mutagen
import time
@ -224,7 +223,6 @@ class Scanner(Thread):
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)
@ -293,6 +291,9 @@ class Scanner(Thread):
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

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,
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,6 @@ class PlaylistTestCase(FrontendTestBase):
duration = 2,
disc = 1,
number = 1,
content_type = 'audio/mpeg',
bitrate = 320,
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,
root_folder = root,
duration = 2,
content_type = 'audio/mpeg',
bitrate = 320,
last_modification = 0
)

View File

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