From af50e8bf911a55892391ba5bf490c2f78e2af866 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 29 Apr 2018 14:51:30 +0200 Subject: [PATCH 01/17] Rewritten dependencies and don't use pip as a library Closes #97, #98 --- .travis.yml | 3 +-- README.md | 10 ++++++++-- requirements.txt | 6 ------ setup.py | 21 +++++++++++++++------ travis-requirements.txt | 6 ++++++ 5 files changed, 30 insertions(+), 16 deletions(-) delete mode 100644 requirements.txt create mode 100644 travis-requirements.txt diff --git a/.travis.yml b/.travis.yml index 90f182c..fab5014 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ python: - 3.5 - 3.6 install: - - pip install -r requirements.txt - - pip install lxml coverage codecov + - pip install -r travis-requirements.txt script: coverage run setup.py test after_script: codecov diff --git a/README.md b/README.md index 815e058..9d0accc 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,12 @@ You'll need these to run _Supysonic_: * [Python Imaging Library](https://github.com/python-pillow/Pillow) * [requests](http://docs.python-requests.org/) * [mutagen](https://mutagen.readthedocs.io/en/latest/) -* [watchdog](https://github.com/gorakhargosh/watchdog) +* [watchdog](https://github.com/gorakhargosh/watchdog) (if you want to use the + [watcher](#watching-library-changes)) You can install all of them using `pip`: - $ pip install -r requirements.txt + $ pip install . You may also need a database specific package: @@ -218,6 +219,11 @@ print anything to the console. If you want to keep it running in background, either use the old `nohup` or `screen` methods, or start it as a simple _systemd_ unit (unit file not included). +It needs some additional dependencies which can be installed with the following +command: + + $ pip install -e .[watcher] + ## Upgrading Some commits might introduce changes in the database schema. When that's the diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7c1623b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -flask>=0.11 -pony>=0.7.2 -Pillow -requests>=1.0.0 -mutagen>=1.33 -watchdog>=0.8.0 diff --git a/setup.py b/setup.py index 98bea82..352b9e3 100755 --- a/setup.py +++ b/setup.py @@ -14,10 +14,19 @@ import supysonic as project from setuptools import setup from setuptools import find_packages -from pip.req import parse_requirements -from pip.download import PipSession +reqs = [ + 'flask>=0.11', + 'pony>=0.7.2', + 'Pillow', + 'requests>=1.0.0', + 'mutagen>=1.33' +] +extras = { + 'watcher': [ 'watchdog>=0.8.0' ] +} + setup( name=project.NAME, version=project.VERSION, @@ -29,13 +38,13 @@ setup( url=project.URL, license=project.LICENSE, packages=find_packages(), - install_requires=[str(x.req) for x in - parse_requirements('requirements.txt', session=PipSession())], + install_requires = reqs, + extras_require = extras, scripts=['bin/supysonic-cli', 'bin/supysonic-watcher'], zip_safe=False, include_package_data=True, - test_suite="tests.suite", - tests_require = [ 'lxml' ], + test_suite='tests.suite', + tests_require = [ 'lxml' ] + [ r for er in extras.values() for r in er ], classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Console', diff --git a/travis-requirements.txt b/travis-requirements.txt new file mode 100644 index 0000000..f8a0e25 --- /dev/null +++ b/travis-requirements.txt @@ -0,0 +1,6 @@ +-e .[watcher] + +lxml +coverage +codecov + From a7cdeb0b2d43e0e0007a95e94359571a8b83a2ea Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 20 May 2018 13:14:28 +0200 Subject: [PATCH 02/17] Don't bulk delete folders Fixes issue with MariaDB<10.3.1 Ref #99 --- supysonic/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supysonic/db.py b/supysonic/db.py index a716069..6b3c56f 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -107,7 +107,7 @@ class Folder(PathMixin, db.Entity): not exists(f for f in Folder if f.parent == self) and not self.root) total = 0 while True: - count = query.delete(bulk = True) + count = query.delete() # Non-bulk, MariaDB<10.3.1 doesn't like it total += count if not count: return total From 918cd11262a15c581d9ea88bb399955e912fe940 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 20 May 2018 17:35:32 +0200 Subject: [PATCH 03/17] Allow missing/empty tags Closes #94, closes #99 --- supysonic/scanner.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 8bef577..ac6fcff 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -113,27 +113,24 @@ class Scanner: return tag = self.__try_load_tag(path) - if not tag: + if tag is None: self.remove_file(path) return trdict = {} else: tag = self.__try_load_tag(path) - if not tag: + if tag is None: return trdict = { 'path': path } - artist = self.__try_read_tag(tag, 'artist') - if not artist: - return - + artist = self.__try_read_tag(tag, 'artist', '[unknown]') album = self.__try_read_tag(tag, 'album', '[non-album tracks]') albumartist = self.__try_read_tag(tag, 'albumartist', artist) trdict['disc'] = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0])) trdict['number'] = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0])) - trdict['title'] = self.__try_read_tag(tag, 'title', '') + trdict['title'] = self.__try_read_tag(tag, 'title', os.path.basename(path)) trdict['year'] = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0])) trdict['genre'] = self.__try_read_tag(tag, 'genre') trdict['duration'] = int(tag.info.length) @@ -252,10 +249,10 @@ class Scanner: def __try_load_tag(self, path): try: return mutagen.File(path, easy = True) - except: + except mutagen.MutagenError: return None - def __try_read_tag(self, metadata, field, default = None, transform = lambda x: x[0]): + def __try_read_tag(self, metadata, field, default = None, transform = lambda x: x[0].strip()): try: value = metadata[field] if not value: From 405a26a20aa43aa4a11e39e26bc5f77bfebf3395 Mon Sep 17 00:00:00 2001 From: spl0k Date: Mon, 21 May 2018 16:16:06 +0200 Subject: [PATCH 04/17] Don't restrict cover art to 'cover.jpg' files Ref #81 --- README.md | 2 +- schema/migration/20180521.mysql.sql | 12 ++++ schema/migration/20180521.postgresql.sql | 12 ++++ schema/migration/20180521.sqlite.sql | 27 ++++++++ schema/mysql.sql | 2 +- schema/postgresql.sql | 2 +- schema/sqlite.sql | 2 +- supysonic/api/media.py | 17 ++--- supysonic/covers.py | 80 ++++++++++++++++++++++++ supysonic/db.py | 8 +-- supysonic/scanner.py | 11 +++- tests/api/test_media.py | 2 +- tests/base/test_db.py | 2 +- 13 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 schema/migration/20180521.mysql.sql create mode 100644 schema/migration/20180521.postgresql.sql create mode 100644 schema/migration/20180521.sqlite.sql create mode 100644 supysonic/covers.py diff --git a/README.md b/README.md index 9d0accc..9fa313e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Current supported features are: * streaming of various audio file formats * [transcoding] * user or random playlists -* cover arts (`cover.jpg` files in the same folder as music files) +* cover arts (as image files in the same folder as music files) * starred tracks/albums and ratings * [Last.FM][lastfm] scrobbling diff --git a/schema/migration/20180521.mysql.sql b/schema/migration/20180521.mysql.sql new file mode 100644 index 0000000..285c827 --- /dev/null +++ b/schema/migration/20180521.mysql.sql @@ -0,0 +1,12 @@ +START TRANSACTION; + +ALTER TABLE folder ADD cover_art VARCHAR(256) AFTER has_cover_art; + +UPDATE folder +SET cover_art = 'cover.jpg' +WHERE has_cover_art; + +ALTER TABLE folder DROP COLUMN has_cover_art; + +COMMIT; + diff --git a/schema/migration/20180521.postgresql.sql b/schema/migration/20180521.postgresql.sql new file mode 100644 index 0000000..2c7da54 --- /dev/null +++ b/schema/migration/20180521.postgresql.sql @@ -0,0 +1,12 @@ +START TRANSACTION; + +ALTER TABLE folder ADD cover_art VARCHAR(256); + +UPDATE folder +SET cover_art = 'cover.jpg' +WHERE has_cover_art; + +ALTER TABLE folder DROP COLUMN has_cover_art; + +COMMIT; + diff --git a/schema/migration/20180521.sqlite.sql b/schema/migration/20180521.sqlite.sql new file mode 100644 index 0000000..4ffbdc2 --- /dev/null +++ b/schema/migration/20180521.sqlite.sql @@ -0,0 +1,27 @@ +BEGIN TRANSACTION; + +DROP INDEX index_folder_path; +ALTER TABLE folder RENAME TO folder_old; + +CREATE TABLE folder ( + id CHAR(36) PRIMARY KEY, + root BOOLEAN NOT NULL, + name VARCHAR(256) NOT NULL COLLATE NOCASE, + path VARCHAR(4096) NOT NULL, + path_hash BLOB NOT NULL, + created DATETIME NOT NULL, + cover_art VARCHAR(256), + last_scan INTEGER NOT NULL, + parent_id CHAR(36) REFERENCES folder +); +CREATE UNIQUE INDEX index_folder_path ON folder(path_hash); + +INSERT INTO folder(id, root, name, path, path_hash, created, cover_art, last_scan, parent_id) +SELECT id, root, name, path, path_hash, created, CASE WHEN has_cover_art THEN 'cover.jpg' ELSE NULL END, last_scan, parent_id +FROM folder_old; + +DROP TABLE folder_old; + +COMMIT; +VACUUM; + diff --git a/schema/mysql.sql b/schema/mysql.sql index 518c9c0..edc54e2 100644 --- a/schema/mysql.sql +++ b/schema/mysql.sql @@ -5,7 +5,7 @@ CREATE TABLE folder ( path VARCHAR(4096) NOT NULL, path_hash BINARY(20) NOT NULL, created DATETIME NOT NULL, - has_cover_art BOOLEAN NOT NULL, + cover_art VARCHAR(256), last_scan INTEGER NOT NULL, parent_id BINARY(16) REFERENCES folder ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/schema/postgresql.sql b/schema/postgresql.sql index e34a3e2..0c4f7a4 100644 --- a/schema/postgresql.sql +++ b/schema/postgresql.sql @@ -5,7 +5,7 @@ CREATE TABLE folder ( path VARCHAR(4096) NOT NULL, path_hash BYTEA NOT NULL, created TIMESTAMP NOT NULL, - has_cover_art BOOLEAN NOT NULL, + cover_art VARCHAR(256), last_scan INTEGER NOT NULL, parent_id UUID REFERENCES folder ); diff --git a/schema/sqlite.sql b/schema/sqlite.sql index ac076d3..5ee370d 100644 --- a/schema/sqlite.sql +++ b/schema/sqlite.sql @@ -5,7 +5,7 @@ CREATE TABLE folder ( path VARCHAR(4096) NOT NULL, path_hash BLOB NOT NULL, created DATETIME NOT NULL, - has_cover_art BOOLEAN NOT NULL, + cover_art VARCHAR(256), last_scan INTEGER NOT NULL, parent_id CHAR(36) REFERENCES folder ); diff --git a/supysonic/api/media.py b/supysonic/api/media.py index 9fe30fe..8ccf426 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -132,29 +132,30 @@ def download_media(): @api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ]) def cover_art(): res = get_entity(Folder) - if not res.has_cover_art or not os.path.isfile(os.path.join(res.path, 'cover.jpg')): + if not res.cover_art or not os.path.isfile(os.path.join(res.path, res.cover_art)): raise NotFound('Cover art') + cover_path = os.path.join(res.path, res.cover_art) size = request.values.get('size') if size: size = int(size) else: - return send_file(os.path.join(res.path, 'cover.jpg')) + return send_file(cover_path) - im = Image.open(os.path.join(res.path, 'cover.jpg')) - if size > im.size[0] and size > im.size[1]: - return send_file(os.path.join(res.path, 'cover.jpg')) + im = Image.open(cover_path) + if size > im.width and size > im.height: + return send_file(cover_path) size_path = os.path.join(current_app.config['WEBAPP']['cache_dir'], str(size)) path = os.path.abspath(os.path.join(size_path, str(res.id))) if os.path.exists(path): - return send_file(path, mimetype = 'image/jpeg') + return send_file(path, mimetype = 'image/' + im.format.lower()) if not os.path.exists(size_path): os.makedirs(size_path) im.thumbnail([size, size], Image.ANTIALIAS) - im.save(path, 'JPEG') - return send_file(path, mimetype = 'image/jpeg') + im.save(path, im.format) + return send_file(path, mimetype = 'image/' + im.format.lower()) @api.route('/getLyrics.view', methods = [ 'GET', 'POST' ]) def lyrics(): diff --git a/supysonic/covers.py b/supysonic/covers.py new file mode 100644 index 0000000..c6fc1ab --- /dev/null +++ b/supysonic/covers.py @@ -0,0 +1,80 @@ +# 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. + +import os, os.path +import re + +from PIL import Image + +EXTENSIONS = ('.jpg', '.jpeg', '.png', '.bmp') +NAMING_SCORE_RULES = ( + ('cover', 5), + ('albumart', 5), + ('folder', 5), + ('front', 10), + ('back', -10), + ('large', 2), + ('small', -2) +) + +class CoverFile(object): + __clean_regex = re.compile(r'[^a-z]') + @staticmethod + def __clean_name(name): + return CoverFile.__clean_regex.sub('', name.lower()) + + def __init__(self, name, album_name = None): + self.name = name + self.score = 0 + + for part, score in NAMING_SCORE_RULES: + if part in name.lower(): + self.score += score + + if album_name: + basename, _ = os.path.splitext(name) + clean = CoverFile.__clean_name(basename) + album_name = CoverFile.__clean_name(album_name) + if clean in album_name or album_name in clean: + self.score += 20 + +def is_valid_cover(path): + if not os.path.isfile(path): + return False + + _, ext = os.path.splitext(path) + if ext.lower() not in EXTENSIONS: + return False + + try: # Ensure the image can be read + with Image.open(path): + return True + except IOError: + return False + +def find_cover_in_folder(path, album_name = None): + if not os.path.isdir(path): + raise ValueError('Invalid path') + + candidates = [] + for f in os.listdir(path): + file_path = os.path.join(path, f) + if not is_valid_cover(file_path): + continue + + cover = CoverFile(f, album_name) + candidates.append(cover) + + if not candidates: + return None + if len(candidates) == 1: + return candidates[0] + + return sorted(candidates, key = lambda c: c.score, reverse = True)[0] + diff --git a/supysonic/db.py b/supysonic/db.py index 6b3c56f..179b2ff 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -59,7 +59,7 @@ class Folder(PathMixin, db.Entity): path = Required(str, 4096) # unique _path_hash = Required(buffer, column = 'path_hash') created = Required(datetime, precision = 0, default = now) - has_cover_art = Required(bool, default = False) + cover_art = Optional(str, nullable = True) last_scan = Required(int, default = 0) parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id') @@ -82,7 +82,7 @@ class Folder(PathMixin, db.Entity): if not self.root: info['parent'] = str(self.parent.id) info['artist'] = self.parent.name - if self.has_cover_art: + if self.cover_art: info['coverArt'] = str(self.id) try: @@ -163,7 +163,7 @@ class Album(db.Entity): created = min(self.tracks.created).isoformat() ) - track_with_cover = self.tracks.select(lambda t: t.folder.has_cover_art).first() + track_with_cover = self.tracks.select(lambda t: t.folder.cover_art is not None).first() if track_with_cover is not None: info['coverArt'] = str(track_with_cover.folder.id) @@ -242,7 +242,7 @@ class Track(PathMixin, db.Entity): info['year'] = self.year if self.genre: info['genre'] = self.genre - if self.folder.has_cover_art: + if self.folder.cover_art: info['coverArt'] = str(self.folder.id) try: diff --git a/supysonic/scanner.py b/supysonic/scanner.py index ac6fcff..3b8ebb2 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -14,6 +14,7 @@ import time from pony.orm import db_session +from .covers import find_cover_in_folder from .db import Folder, Artist, Album, Track, User from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from .db import RatingFolder, RatingTrack @@ -84,7 +85,15 @@ class Scanner: folders = [ folder ] while folders: f = folders.pop() - f.has_cover_art = os.path.isfile(os.path.join(f.path, 'cover.jpg')) + + album_name = None + track = f.tracks.select().first() + if track is not None: + album_name = track.album.name + + cover = find_cover_in_folder(f.path, album_name) + f.cover_art = cover.name if cover is not None else None + folders += f.children folder.last_scan = int(time.time()) diff --git a/tests/api/test_media.py b/tests/api/test_media.py index ec6b5bd..26b43f7 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -28,7 +28,7 @@ class MediaTestCase(ApiTestBase): name = 'Root', path = os.path.abspath('tests/assets'), root = True, - has_cover_art = True # 420x420 PNG + cover_art = 'cover.jpg' ) self.folderid = folder.id diff --git a/tests/base/test_db.py b/tests/base/test_db.py index 90361d9..8421f7f 100644 --- a/tests/base/test_db.py +++ b/tests/base/test_db.py @@ -42,7 +42,7 @@ class DbTestCase(unittest.TestCase): root = False, name = 'Child folder', path = 'tests/assets', - has_cover_art = True, + cover_art = 'cover.jpg', parent = root_folder ) From b43a70a045ae9efbc06d407dd17fe97804a238a1 Mon Sep 17 00:00:00 2001 From: spl0k Date: Mon, 21 May 2018 16:37:21 +0200 Subject: [PATCH 05/17] Cover art discovery: handle badly encoded files --- supysonic/covers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/supysonic/covers.py b/supysonic/covers.py index c6fc1ab..c0a9ef3 100644 --- a/supysonic/covers.py +++ b/supysonic/covers.py @@ -64,7 +64,11 @@ def find_cover_in_folder(path, album_name = None): candidates = [] for f in os.listdir(path): - file_path = os.path.join(path, f) + try: + file_path = os.path.join(path, f) + except UnicodeError: + continue + if not is_valid_cover(file_path): continue From 78cf84e13684bc5f3faa7958ce96c5e414aa45fd Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 24 Jun 2018 13:19:04 +0200 Subject: [PATCH 06/17] Remove invalid folders before scanning for covers Fixes #101 --- supysonic/managers/folder.py | 2 +- supysonic/scanner.py | 6 +++- tests/__init__.py | 6 +++- tests/issue101.py | 56 ++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tests/issue101.py diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py index 13d6834..3670333 100644 --- a/supysonic/managers/folder.py +++ b/supysonic/managers/folder.py @@ -54,7 +54,7 @@ class FolderManager: Track.select(lambda t: t.root_folder == folder).delete(bulk = True) Album.prune() Artist.prune() - Folder.prune() + Folder.select(lambda f: not f.root and f.path.startswith(folder.path)).delete(bulk = True) folder.delete() diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 3b8ebb2..43e049e 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -81,11 +81,15 @@ class Scanner: if not self.__is_valid_path(track.path): self.remove_file(track.path) - # Update cover art info + # Remove deleted/moved folders and update cover art info folders = [ folder ] while folders: f = folders.pop() + if not f.root and not os.path.isdir(f.path): + Folder.select(lambda sub: sub.path.startswith(f.path)).delete(bulk = True) + continue + album_name = None track = f.tracks.select().first() if track is not None: diff --git a/tests/__init__.py b/tests/__init__.py index 87a687b..80bc299 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2017 Alban 'spl0k' Féron +# Copyright (C) 2017-2018 Alban 'spl0k' Féron # 2017 Óscar García Amor # # Distributed under terms of the GNU AGPLv3 license. @@ -15,6 +15,8 @@ from . import managers from . import api from . import frontend +from .issue101 import Issue101TestCase + def suite(): suite = unittest.TestSuite() @@ -22,5 +24,7 @@ def suite(): suite.addTest(managers.suite()) suite.addTest(api.suite()) suite.addTest(frontend.suite()) + suite.addTest(unittest.makeSuite(Issue101TestCase)) return suite + diff --git a/tests/issue101.py b/tests/issue101.py new file mode 100644 index 0000000..98b68a5 --- /dev/null +++ b/tests/issue101.py @@ -0,0 +1,56 @@ +# 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. + +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 Issue101TestCase(unittest.TestCase): + def setUp(self): + self.__dir = tempfile.mkdtemp() + init_database('sqlite:', True) + with db_session: + FolderManager.add('folder', self.__dir) + + def tearDown(self): + release_database() + shutil.rmtree(self.__dir) + + def test_issue(self): + firstsubdir = tempfile.mkdtemp(dir = self.__dir) + subdir = firstsubdir + for _ in range(4): + subdir = tempfile.mkdtemp(dir = 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() + + shutil.rmtree(firstsubdir) + + with db_session: + folder = Folder.select(lambda f: f.root).first() + scanner.scan(folder) + scanner.finish() + + +if __name__ == '__main__': + unittest.main() + From b25e943e4e4d384c9a6609ab43cd106404c64be7 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 29 Jul 2018 17:58:20 +0200 Subject: [PATCH 07/17] Removed bulk deletes from scanner Was causing either cache issues or constraint errors Fixes #103, #102 --- supysonic/db.py | 6 +++--- supysonic/scanner.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/supysonic/db.py b/supysonic/db.py index 179b2ff..e5e46ec 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -107,7 +107,7 @@ class Folder(PathMixin, db.Entity): not exists(f for f in Folder if f.parent == self) and not self.root) total = 0 while True: - count = query.delete() # Non-bulk, MariaDB<10.3.1 doesn't like it + count = query.delete() total += count if not count: return total @@ -140,7 +140,7 @@ class Artist(db.Entity): @classmethod def prune(cls): return cls.select(lambda self: not exists(a for a in Album if a.artist == self) and \ - not exists(t for t in Track if t.artist == self)).delete(bulk = True) + not exists(t for t in Track if t.artist == self)).delete() class Album(db.Entity): _table_ = 'album' @@ -180,7 +180,7 @@ class Album(db.Entity): @classmethod def prune(cls): - return cls.select(lambda self: not exists(t for t in Track if t.album == self)).delete(bulk = True) + return cls.select(lambda self: not exists(t for t in Track if t.album == self)).delete() class Track(PathMixin, db.Entity): _table_ = 'track' diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 43e049e..1c2f718 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -87,7 +87,7 @@ class Scanner: f = folders.pop() if not f.root and not os.path.isdir(f.path): - Folder.select(lambda sub: sub.path.startswith(f.path)).delete(bulk = True) + f.delete() # Pony will cascade continue album_name = None From 1b0cc54581feca43dfad983d9d275c3da5d41cbe Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 4 Aug 2018 12:24:29 +0200 Subject: [PATCH 08/17] Workaround new pony QueryResult behaviour Ref ponyorm/pony#369 --- supysonic/api/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supysonic/api/search.py b/supysonic/api/search.py index 3ade661..758e7d1 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -41,7 +41,7 @@ def old_search(): if offset + count > fcount: toff = max(0, offset - fcount) tend = offset + count - fcount - res += tracks[toff : tend] + res = res[:] + tracks[toff : tend][:] return request.formatter('searchResult', dict( totalHits = folders.count() + tracks.count(), From dd33e8da1424646a00dd22101f2cb07a2158753a Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 4 Aug 2018 13:12:20 +0200 Subject: [PATCH 09/17] Fixed LastFM account linking --- supysonic/frontend/user.py | 2 +- supysonic/templates/profile.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/supysonic/frontend/user.py b/supysonic/frontend/user.py index fbe9bd6..7d45aaf 100644 --- a/supysonic/frontend/user.py +++ b/supysonic/frontend/user.py @@ -58,7 +58,7 @@ def user_index(): @frontend.route('/user/') @me_or_uuid def user_profile(uid, user): - return render_template('profile.html', user = user, has_lastfm = current_app.config['LASTFM']['api_key'] != None, clients = user.clients) + return render_template('profile.html', user = user, api_key = current_app.config['LASTFM']['api_key'], clients = user.clients) @frontend.route('/user/', methods = [ 'POST' ]) @me_or_uuid diff --git a/supysonic/templates/profile.html b/supysonic/templates/profile.html index 342c6fc..4dc3cab 100644 --- a/supysonic/templates/profile.html +++ b/supysonic/templates/profile.html @@ -54,7 +54,7 @@
LastFM status
- {% if has_lastfm %} + {% if api_key != None %} {% if user.lastfm_session %}
From a08689e8c0f5bb8b98061836b6d30158e5bd5ee1 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 5 Aug 2018 15:35:29 +0200 Subject: [PATCH 10/17] Restore SQLite case insensitive LIKE See ponyorm/pony#367 --- supysonic/cli.py | 3 +-- supysonic/db.py | 21 +++++++++++++++++++++ supysonic/scanner.py | 3 +-- supysonic/watcher.py | 3 +-- supysonic/web.py | 3 +-- tests/api/test_album_songs.py | 4 +--- tests/api/test_annotation.py | 4 +--- tests/api/test_browse.py | 3 +-- tests/api/test_media.py | 3 +-- tests/api/test_playlist.py | 4 +--- tests/api/test_search.py | 4 ++-- tests/api/test_transcoding.py | 4 +--- tests/base/test_cli.py | 3 +-- tests/base/test_db.py | 2 +- tests/base/test_scanner.py | 4 +++- tests/base/test_watcher.py | 3 +-- tests/frontend/test_folder.py | 4 +--- tests/frontend/test_login.py | 4 +--- tests/frontend/test_playlist.py | 4 +--- tests/frontend/test_user.py | 3 +-- tests/issue101.py | 4 +--- tests/managers/test_manager_folder.py | 4 +++- tests/managers/test_manager_user.py | 4 +++- tests/testbase.py | 4 +--- 24 files changed, 51 insertions(+), 51 deletions(-) diff --git a/supysonic/cli.py b/supysonic/cli.py index f41f0a5..34f6f80 100755 --- a/supysonic/cli.py +++ b/supysonic/cli.py @@ -14,10 +14,9 @@ import getpass import sys import time -from pony.orm import db_session from pony.orm import ObjectNotFound -from .db import Folder, User +from .db import Folder, User, db_session from .managers.folder import FolderManager from .managers.user import UserManager from .scanner import Scanner diff --git a/supysonic/db.py b/supysonic/db.py index e5e46ec..db7b009 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -17,6 +17,7 @@ from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr from pony.orm import ObjectNotFound from pony.orm import buffer from pony.orm import min, max, avg, sum, exists +from pony.orm import db_session as pony_session from uuid import UUID, uuid4 from .py23 import dict, strtype @@ -469,6 +470,26 @@ class Playlist(db.Entity): self.tracks = ','.join(t for t in tracks if t) +class DBSessionContextWrapper(object): + def __ensure_sqlite_case_insensitive_like(self): + if db.provider.dialect == 'SQLite': + db.execute('PRAGMA case_sensitive_like = OFF') + + def __call__(self, func, *args, **kwargs): + def new_func(*args, **kwargs): + self.__ensure_sqlite_case_insensitive_like() + return func(*args, **kwargs) + return pony_session(new_func, *args, **kwargs) + + def __enter__(self): + pony_session.__enter__() + self.__ensure_sqlite_case_insensitive_like() + + def __exit__(self, *args, **kwargs): + pony_session.__exit__(*args, **kwargs) + +db_session = DBSessionContextWrapper() + def parse_uri(database_uri): if not isinstance(database_uri, strtype): raise TypeError('Expecting a string') diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 1c2f718..ccf3038 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -12,12 +12,11 @@ import mimetypes import mutagen import time -from pony.orm import db_session - from .covers import find_cover_in_folder from .db import Folder, Artist, Album, Track, User from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from .db import RatingFolder, RatingTrack +from .db import db_session from .py23 import strtype class StatsDetails(object): diff --git a/supysonic/watcher.py b/supysonic/watcher.py index 2f6d794..adafa9a 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -11,13 +11,12 @@ import logging import time from logging.handlers import TimedRotatingFileHandler -from pony.orm import db_session from signal import signal, SIGTERM, SIGINT from threading import Thread, Condition, Timer from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler -from .db import init_database, release_database, Folder +from .db import init_database, release_database, db_session, Folder from .py23 import dict from .scanner import Scanner diff --git a/supysonic/web.py b/supysonic/web.py index ac5259d..2814612 100644 --- a/supysonic/web.py +++ b/supysonic/web.py @@ -12,10 +12,9 @@ import mimetypes from flask import Flask from os import makedirs, path -from pony.orm import db_session from .config import IniConfig -from .db import init_database +from .db import init_database, db_session def create_application(config = None): global app diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index e77d5e7..0a8583e 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -10,9 +10,7 @@ import uuid -from pony.orm import db_session - -from supysonic.db import Folder, Artist, Album, Track +from supysonic.db import Folder, Artist, Album, Track, db_session from .apitestbase import ApiTestBase diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py index 52f81f7..b552912 100644 --- a/tests/api/test_annotation.py +++ b/tests/api/test_annotation.py @@ -10,9 +10,7 @@ import uuid -from pony.orm import db_session - -from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs +from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs, db_session from .apitestbase import ApiTestBase diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py index 05ca9cb..69e0761 100644 --- a/tests/api/test_browse.py +++ b/tests/api/test_browse.py @@ -12,9 +12,8 @@ import time import uuid from lxml import etree -from pony.orm import db_session -from supysonic.db import Folder, Artist, Album, Track +from supysonic.db import Folder, Artist, Album, Track, db_session from .apitestbase import ApiTestBase diff --git a/tests/api/test_media.py b/tests/api/test_media.py index 26b43f7..c5c4526 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -13,9 +13,8 @@ import uuid from io import BytesIO from PIL import Image -from pony.orm import db_session -from supysonic.db import Folder, Artist, Album, Track +from supysonic.db import Folder, Artist, Album, Track, db_session from .apitestbase import ApiTestBase diff --git a/tests/api/test_playlist.py b/tests/api/test_playlist.py index 0e9b063..aace508 100644 --- a/tests/api/test_playlist.py +++ b/tests/api/test_playlist.py @@ -10,9 +10,7 @@ import uuid -from pony.orm import db_session - -from supysonic.db import Folder, Artist, Album, Track, Playlist, User +from supysonic.db import Folder, Artist, Album, Track, Playlist, User, db_session from .apitestbase import ApiTestBase diff --git a/tests/api/test_search.py b/tests/api/test_search.py index 8216257..ec62db2 100644 --- a/tests/api/test_search.py +++ b/tests/api/test_search.py @@ -11,9 +11,9 @@ import time import unittest -from pony.orm import db_session, commit +from pony.orm import commit -from supysonic.db import Folder, Artist, Album, Track +from supysonic.db import Folder, Artist, Album, Track, db_session from .apitestbase import ApiTestBase diff --git a/tests/api/test_transcoding.py b/tests/api/test_transcoding.py index 0b3f959..4a85373 100644 --- a/tests/api/test_transcoding.py +++ b/tests/api/test_transcoding.py @@ -10,9 +10,7 @@ import unittest -from pony.orm import db_session - -from supysonic.db import Folder, Track +from supysonic.db import Folder, Track, db_session from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner diff --git a/tests/base/test_cli.py b/tests/base/test_cli.py index b0e11d0..d05c364 100644 --- a/tests/base/test_cli.py +++ b/tests/base/test_cli.py @@ -15,14 +15,13 @@ import tempfile import unittest from contextlib import contextmanager -from pony.orm import db_session try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs from StringIO import StringIO except ImportError: from io import StringIO -from supysonic.db import Folder, User, init_database, release_database +from supysonic.db import Folder, User, init_database, release_database, db_session from supysonic.cli import SupysonicCLI from ..testbase import TestConfig diff --git a/tests/base/test_db.py b/tests/base/test_db.py index 8421f7f..a3ebe51 100644 --- a/tests/base/test_db.py +++ b/tests/base/test_db.py @@ -13,10 +13,10 @@ import unittest import uuid from collections import namedtuple -from pony.orm import db_session from supysonic import db +db_session = db.db_session date_regex = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') class DbTestCase(unittest.TestCase): diff --git a/tests/base/test_scanner.py b/tests/base/test_scanner.py index 74be660..b570c73 100644 --- a/tests/base/test_scanner.py +++ b/tests/base/test_scanner.py @@ -15,12 +15,14 @@ import tempfile import unittest from contextlib import contextmanager -from pony.orm import db_session, commit +from pony.orm import commit from supysonic import db from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner +db_session = db.db_session + class ScannerTestCase(unittest.TestCase): def setUp(self): db.init_database('sqlite:', True) diff --git a/tests/base/test_watcher.py b/tests/base/test_watcher.py index 2c19253..8cf63a5 100644 --- a/tests/base/test_watcher.py +++ b/tests/base/test_watcher.py @@ -18,10 +18,9 @@ import unittest from contextlib import contextmanager from hashlib import sha1 -from pony.orm import db_session from threading import Thread -from supysonic.db import init_database, release_database, Track, Artist +from supysonic.db import init_database, release_database, db_session, Track, Artist from supysonic.managers.folder import FolderManager from supysonic.watcher import SupysonicWatcher diff --git a/tests/frontend/test_folder.py b/tests/frontend/test_folder.py index c42107c..0d88a1d 100644 --- a/tests/frontend/test_folder.py +++ b/tests/frontend/test_folder.py @@ -10,9 +10,7 @@ import uuid -from pony.orm import db_session - -from supysonic.db import Folder +from supysonic.db import Folder, db_session from .frontendtestbase import FrontendTestBase diff --git a/tests/frontend/test_login.py b/tests/frontend/test_login.py index 5dde4ff..f5eb3fc 100644 --- a/tests/frontend/test_login.py +++ b/tests/frontend/test_login.py @@ -11,9 +11,7 @@ import uuid -from pony.orm import db_session - -from supysonic.db import User +from supysonic.db import User, db_session from .frontendtestbase import FrontendTestBase diff --git a/tests/frontend/test_playlist.py b/tests/frontend/test_playlist.py index 67bdb22..a5df907 100644 --- a/tests/frontend/test_playlist.py +++ b/tests/frontend/test_playlist.py @@ -10,9 +10,7 @@ import uuid -from pony.orm import db_session - -from supysonic.db import Folder, Artist, Album, Track, Playlist, User +from supysonic.db import Folder, Artist, Album, Track, Playlist, User, db_session from .frontendtestbase import FrontendTestBase diff --git a/tests/frontend/test_user.py b/tests/frontend/test_user.py index 67f72c3..2bcdd7f 100644 --- a/tests/frontend/test_user.py +++ b/tests/frontend/test_user.py @@ -11,9 +11,8 @@ import uuid from flask import escape -from pony.orm import db_session -from supysonic.db import User, ClientPrefs +from supysonic.db import User, ClientPrefs, db_session from .frontendtestbase import FrontendTestBase diff --git a/tests/issue101.py b/tests/issue101.py index 98b68a5..c089315 100644 --- a/tests/issue101.py +++ b/tests/issue101.py @@ -12,9 +12,7 @@ import shutil import tempfile import unittest -from pony.orm import db_session - -from supysonic.db import init_database, release_database +from supysonic.db import init_database, release_database, db_session from supysonic.db import Folder from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py index 8430479..c6ac034 100644 --- a/tests/managers/test_manager_folder.py +++ b/tests/managers/test_manager_folder.py @@ -18,7 +18,9 @@ import tempfile import unittest import uuid -from pony.orm import db_session, ObjectNotFound +from pony.orm import ObjectNotFound + +db_session = db.db_session class FolderManagerTestCase(unittest.TestCase): def setUp(self): diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 082cdb2..d723b11 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -17,9 +17,11 @@ import io import unittest import uuid -from pony.orm import db_session, commit +from pony.orm import commit from pony.orm import ObjectNotFound +db_session = db.db_session + class UserManagerTestCase(unittest.TestCase): def setUp(self): # Create an empty sqlite database in memory diff --git a/tests/testbase.py b/tests/testbase.py index d93b477..6a294f2 100644 --- a/tests/testbase.py +++ b/tests/testbase.py @@ -14,9 +14,7 @@ import shutil import unittest import tempfile -from pony.orm import db_session - -from supysonic.db import init_database, release_database +from supysonic.db import init_database, release_database, db_session from supysonic.config import DefaultConfig from supysonic.managers.user import UserManager from supysonic.web import create_application From 4df8e975ec6df1fa290e6d1594203fbdce1ba653 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 11 Aug 2018 15:52:50 +0200 Subject: [PATCH 11/17] Added links to some docker images Closes #106 --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 9fa313e..83419ef 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ details, go check the [API implementation status][docs-api]. + [As a standalone debug server](#as-a-standalone-debug-server) + [As an Apache WSGI application](#as-an-apache-wsgi-application) + [Other options](#other-options) + + [Docker](#docker) * [Quickstart](#quickstart) * [Watching library changes](#watching-library-changes) * [Upgrading](#upgrading) @@ -184,6 +185,18 @@ Here are some quick docs on how to configure your server for [FastCGI][] or [fastcgi]: http://flask.pocoo.org/docs/deploying/fastcgi/ [cgi]: http://flask.pocoo.org/docs/deploying/cgi/ +### Docker + +If you want to run _Supysonic_ in a _Docker_ container, here are some images +provided by the community. + +- https://github.com/ultimate-pms/docker-supysonic +- https://github.com/ogarcia/docker-supysonic +- https://github.com/foosinn/supysonic +- https://github.com/mikafouenski/docker-supysonic +- https://github.com/oakman/supysonic-docker +- https://github.com/glogiotatidis/supysonic-docker + ## Quickstart To start using _Supysonic_, you'll first have to specify where your music From 38f718b4df885cf3473d11cdb1add3ad7a17c5af Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 11 Aug 2018 16:16:34 +0200 Subject: [PATCH 12/17] Using new pony `on_connect` decorator Revert "Restore SQLite case insensitive LIKE" This reverts commit a08689e8c0f5bb8b98061836b6d30158e5bd5ee1. --- setup.py | 2 +- supysonic/cli.py | 3 ++- supysonic/db.py | 26 +++++--------------------- supysonic/scanner.py | 3 ++- supysonic/watcher.py | 3 ++- supysonic/web.py | 3 ++- tests/api/test_album_songs.py | 4 +++- tests/api/test_annotation.py | 4 +++- tests/api/test_browse.py | 3 ++- tests/api/test_media.py | 3 ++- tests/api/test_playlist.py | 4 +++- tests/api/test_search.py | 4 ++-- tests/api/test_transcoding.py | 4 +++- tests/base/test_cli.py | 3 ++- tests/base/test_db.py | 2 +- tests/base/test_scanner.py | 4 +--- tests/base/test_watcher.py | 3 ++- tests/frontend/test_folder.py | 4 +++- tests/frontend/test_login.py | 4 +++- tests/frontend/test_playlist.py | 4 +++- tests/frontend/test_user.py | 3 ++- tests/issue101.py | 4 +++- tests/managers/test_manager_folder.py | 4 +--- tests/managers/test_manager_user.py | 4 +--- tests/testbase.py | 4 +++- 25 files changed, 57 insertions(+), 52 deletions(-) diff --git a/setup.py b/setup.py index 352b9e3..ca97a79 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ from setuptools import find_packages reqs = [ 'flask>=0.11', - 'pony>=0.7.2', + 'pony>=0.7.6', 'Pillow', 'requests>=1.0.0', 'mutagen>=1.33' diff --git a/supysonic/cli.py b/supysonic/cli.py index 34f6f80..f41f0a5 100755 --- a/supysonic/cli.py +++ b/supysonic/cli.py @@ -14,9 +14,10 @@ import getpass import sys import time +from pony.orm import db_session from pony.orm import ObjectNotFound -from .db import Folder, User, db_session +from .db import Folder, User from .managers.folder import FolderManager from .managers.user import UserManager from .scanner import Scanner diff --git a/supysonic/db.py b/supysonic/db.py index db7b009..260a4f3 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -17,7 +17,6 @@ from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr from pony.orm import ObjectNotFound from pony.orm import buffer from pony.orm import min, max, avg, sum, exists -from pony.orm import db_session as pony_session from uuid import UUID, uuid4 from .py23 import dict, strtype @@ -32,6 +31,11 @@ def now(): db = Database() +@db.on_connect(provider = 'sqlite') +def sqlite_case_insensitive_like(db, connection): + cursor = connection.cursor() + cursor.execute('PRAGMA case_sensitive_like = OFF') + class PathMixin(object): @classmethod def get(cls, *args, **kwargs): @@ -470,26 +474,6 @@ class Playlist(db.Entity): self.tracks = ','.join(t for t in tracks if t) -class DBSessionContextWrapper(object): - def __ensure_sqlite_case_insensitive_like(self): - if db.provider.dialect == 'SQLite': - db.execute('PRAGMA case_sensitive_like = OFF') - - def __call__(self, func, *args, **kwargs): - def new_func(*args, **kwargs): - self.__ensure_sqlite_case_insensitive_like() - return func(*args, **kwargs) - return pony_session(new_func, *args, **kwargs) - - def __enter__(self): - pony_session.__enter__() - self.__ensure_sqlite_case_insensitive_like() - - def __exit__(self, *args, **kwargs): - pony_session.__exit__(*args, **kwargs) - -db_session = DBSessionContextWrapper() - def parse_uri(database_uri): if not isinstance(database_uri, strtype): raise TypeError('Expecting a string') diff --git a/supysonic/scanner.py b/supysonic/scanner.py index ccf3038..1c2f718 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -12,11 +12,12 @@ import mimetypes import mutagen import time +from pony.orm import db_session + from .covers import find_cover_in_folder from .db import Folder, Artist, Album, Track, User from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from .db import RatingFolder, RatingTrack -from .db import db_session from .py23 import strtype class StatsDetails(object): diff --git a/supysonic/watcher.py b/supysonic/watcher.py index adafa9a..2f6d794 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -11,12 +11,13 @@ import logging import time from logging.handlers import TimedRotatingFileHandler +from pony.orm import db_session from signal import signal, SIGTERM, SIGINT from threading import Thread, Condition, Timer from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler -from .db import init_database, release_database, db_session, Folder +from .db import init_database, release_database, Folder from .py23 import dict from .scanner import Scanner diff --git a/supysonic/web.py b/supysonic/web.py index 2814612..ac5259d 100644 --- a/supysonic/web.py +++ b/supysonic/web.py @@ -12,9 +12,10 @@ import mimetypes from flask import Flask from os import makedirs, path +from pony.orm import db_session from .config import IniConfig -from .db import init_database, db_session +from .db import init_database def create_application(config = None): global app diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index 0a8583e..e77d5e7 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -10,7 +10,9 @@ import uuid -from supysonic.db import Folder, Artist, Album, Track, db_session +from pony.orm import db_session + +from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py index b552912..52f81f7 100644 --- a/tests/api/test_annotation.py +++ b/tests/api/test_annotation.py @@ -10,7 +10,9 @@ import uuid -from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs, db_session +from pony.orm import db_session + +from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs from .apitestbase import ApiTestBase diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py index 69e0761..05ca9cb 100644 --- a/tests/api/test_browse.py +++ b/tests/api/test_browse.py @@ -12,8 +12,9 @@ import time import uuid from lxml import etree +from pony.orm import db_session -from supysonic.db import Folder, Artist, Album, Track, db_session +from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase diff --git a/tests/api/test_media.py b/tests/api/test_media.py index c5c4526..26b43f7 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -13,8 +13,9 @@ import uuid from io import BytesIO from PIL import Image +from pony.orm import db_session -from supysonic.db import Folder, Artist, Album, Track, db_session +from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase diff --git a/tests/api/test_playlist.py b/tests/api/test_playlist.py index aace508..0e9b063 100644 --- a/tests/api/test_playlist.py +++ b/tests/api/test_playlist.py @@ -10,7 +10,9 @@ import uuid -from supysonic.db import Folder, Artist, Album, Track, Playlist, User, db_session +from pony.orm import db_session + +from supysonic.db import Folder, Artist, Album, Track, Playlist, User from .apitestbase import ApiTestBase diff --git a/tests/api/test_search.py b/tests/api/test_search.py index ec62db2..8216257 100644 --- a/tests/api/test_search.py +++ b/tests/api/test_search.py @@ -11,9 +11,9 @@ import time import unittest -from pony.orm import commit +from pony.orm import db_session, commit -from supysonic.db import Folder, Artist, Album, Track, db_session +from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase diff --git a/tests/api/test_transcoding.py b/tests/api/test_transcoding.py index 4a85373..0b3f959 100644 --- a/tests/api/test_transcoding.py +++ b/tests/api/test_transcoding.py @@ -10,7 +10,9 @@ import unittest -from supysonic.db import Folder, Track, db_session +from pony.orm import db_session + +from supysonic.db import Folder, Track from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner diff --git a/tests/base/test_cli.py b/tests/base/test_cli.py index d05c364..b0e11d0 100644 --- a/tests/base/test_cli.py +++ b/tests/base/test_cli.py @@ -15,13 +15,14 @@ import tempfile import unittest from contextlib import contextmanager +from pony.orm import db_session try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs from StringIO import StringIO except ImportError: from io import StringIO -from supysonic.db import Folder, User, init_database, release_database, db_session +from supysonic.db import Folder, User, init_database, release_database from supysonic.cli import SupysonicCLI from ..testbase import TestConfig diff --git a/tests/base/test_db.py b/tests/base/test_db.py index a3ebe51..8421f7f 100644 --- a/tests/base/test_db.py +++ b/tests/base/test_db.py @@ -13,10 +13,10 @@ import unittest import uuid from collections import namedtuple +from pony.orm import db_session from supysonic import db -db_session = db.db_session date_regex = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') class DbTestCase(unittest.TestCase): diff --git a/tests/base/test_scanner.py b/tests/base/test_scanner.py index b570c73..74be660 100644 --- a/tests/base/test_scanner.py +++ b/tests/base/test_scanner.py @@ -15,14 +15,12 @@ import tempfile import unittest from contextlib import contextmanager -from pony.orm import commit +from pony.orm import db_session, commit from supysonic import db from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner -db_session = db.db_session - class ScannerTestCase(unittest.TestCase): def setUp(self): db.init_database('sqlite:', True) diff --git a/tests/base/test_watcher.py b/tests/base/test_watcher.py index 8cf63a5..2c19253 100644 --- a/tests/base/test_watcher.py +++ b/tests/base/test_watcher.py @@ -18,9 +18,10 @@ import unittest from contextlib import contextmanager from hashlib import sha1 +from pony.orm import db_session from threading import Thread -from supysonic.db import init_database, release_database, db_session, Track, Artist +from supysonic.db import init_database, release_database, Track, Artist from supysonic.managers.folder import FolderManager from supysonic.watcher import SupysonicWatcher diff --git a/tests/frontend/test_folder.py b/tests/frontend/test_folder.py index 0d88a1d..c42107c 100644 --- a/tests/frontend/test_folder.py +++ b/tests/frontend/test_folder.py @@ -10,7 +10,9 @@ import uuid -from supysonic.db import Folder, db_session +from pony.orm import db_session + +from supysonic.db import Folder from .frontendtestbase import FrontendTestBase diff --git a/tests/frontend/test_login.py b/tests/frontend/test_login.py index f5eb3fc..5dde4ff 100644 --- a/tests/frontend/test_login.py +++ b/tests/frontend/test_login.py @@ -11,7 +11,9 @@ import uuid -from supysonic.db import User, db_session +from pony.orm import db_session + +from supysonic.db import User from .frontendtestbase import FrontendTestBase diff --git a/tests/frontend/test_playlist.py b/tests/frontend/test_playlist.py index a5df907..67bdb22 100644 --- a/tests/frontend/test_playlist.py +++ b/tests/frontend/test_playlist.py @@ -10,7 +10,9 @@ import uuid -from supysonic.db import Folder, Artist, Album, Track, Playlist, User, db_session +from pony.orm import db_session + +from supysonic.db import Folder, Artist, Album, Track, Playlist, User from .frontendtestbase import FrontendTestBase diff --git a/tests/frontend/test_user.py b/tests/frontend/test_user.py index 2bcdd7f..67f72c3 100644 --- a/tests/frontend/test_user.py +++ b/tests/frontend/test_user.py @@ -11,8 +11,9 @@ import uuid from flask import escape +from pony.orm import db_session -from supysonic.db import User, ClientPrefs, db_session +from supysonic.db import User, ClientPrefs from .frontendtestbase import FrontendTestBase diff --git a/tests/issue101.py b/tests/issue101.py index c089315..98b68a5 100644 --- a/tests/issue101.py +++ b/tests/issue101.py @@ -12,7 +12,9 @@ import shutil import tempfile import unittest -from supysonic.db import init_database, release_database, db_session +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 diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py index c6ac034..8430479 100644 --- a/tests/managers/test_manager_folder.py +++ b/tests/managers/test_manager_folder.py @@ -18,9 +18,7 @@ import tempfile import unittest import uuid -from pony.orm import ObjectNotFound - -db_session = db.db_session +from pony.orm import db_session, ObjectNotFound class FolderManagerTestCase(unittest.TestCase): def setUp(self): diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index d723b11..082cdb2 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -17,11 +17,9 @@ import io import unittest import uuid -from pony.orm import commit +from pony.orm import db_session, commit from pony.orm import ObjectNotFound -db_session = db.db_session - class UserManagerTestCase(unittest.TestCase): def setUp(self): # Create an empty sqlite database in memory diff --git a/tests/testbase.py b/tests/testbase.py index 6a294f2..d93b477 100644 --- a/tests/testbase.py +++ b/tests/testbase.py @@ -14,7 +14,9 @@ import shutil import unittest import tempfile -from supysonic.db import init_database, release_database, db_session +from pony.orm import db_session + +from supysonic.db import init_database, release_database from supysonic.config import DefaultConfig from supysonic.managers.user import UserManager from supysonic.web import create_application From 040169020e78cc8922bc8e9737ae617e1a6d787f Mon Sep 17 00:00:00 2001 From: spl0k Date: Mon, 27 Aug 2018 11:53:51 +0200 Subject: [PATCH 13/17] Updated README See #109 --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 83419ef..715f48c 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,18 @@ details, go check the [API implementation status][docs-api]. _Supysonic_ can run as a standalone application (not recommended for a "production" server) or as a WSGI application (on _Apache_ for instance). -To install it, run: + +To install it, either run: $ python setup.py install +or + + $ pip install . + +but not both. Please note that the `pip` method doesn't seem to work with +Python 2.7. + ### Prerequisites You'll need these to run _Supysonic_: @@ -59,11 +67,11 @@ You'll need these to run _Supysonic_: * [watchdog](https://github.com/gorakhargosh/watchdog) (if you want to use the [watcher](#watching-library-changes)) -You can install all of them using `pip`: +All the dependencies (except _watchdog_) will automatically be installed by the +installation command above. - $ pip install . - -You may also need a database specific package: +You may also need a database specific package if you don't want to use SQLite +(the default): * _MySQL_: `pip install pymysql` or `pip install mysqlclient` * _PostgreSQL_: `pip install psycopg2-binary` @@ -227,7 +235,7 @@ Instead of manually running a scan every time your library changes, you can run a watcher that will listen to any library change and update the database accordingly. -The watcher is `bin/supysonic-watcher`, it is a non-exiting process and doesn't +The watcher is `supysonic-watcher`, it is a non-exiting process and doesn't print anything to the console. If you want to keep it running in background, either use the old `nohup` or `screen` methods, or start it as a simple _systemd_ unit (unit file not included). From 9736622ce1985ef86079834ccb9202bcd370cf8e Mon Sep 17 00:00:00 2001 From: spl0k Date: Mon, 27 Aug 2018 15:51:01 +0200 Subject: [PATCH 14/17] Recently played API returns albums/folders that have been played at least once Fixes #107 --- supysonic/api/albums_songs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index b5abb9a..f0934fb 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -67,7 +67,7 @@ def album_list(): elif ltype == 'frequent': query = query.order_by(lambda f: desc(avg(f.tracks.play_count))) elif ltype == 'recent': - query = query.order_by(lambda f: desc(max(f.tracks.last_play))) + query = select(t.folder for t in Track if max(t.folder.tracks.last_play) is not None).order_by(lambda f: desc(max(f.tracks.last_play))) elif ltype == 'starred': query = select(s.starred for s in StarredFolder if s.user.id == request.user.id and count(s.starred.tracks) > 0) elif ltype == 'alphabeticalByName': @@ -99,7 +99,7 @@ def album_list_id3(): elif ltype == 'frequent': query = query.order_by(lambda a: desc(avg(a.tracks.play_count))) elif ltype == 'recent': - query = query.order_by(lambda a: desc(max(a.tracks.last_play))) + query = Album.select(lambda a: max(a.tracks.last_play) is not None).order_by(lambda a: desc(max(a.tracks.last_play))) elif ltype == 'starred': query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) elif ltype == 'alphabeticalByName': From 9c58b695ff98147df6913f33c6c1a7e28a62433e Mon Sep 17 00:00:00 2001 From: spl0k Date: Tue, 28 Aug 2018 17:44:32 +0200 Subject: [PATCH 15/17] Watcher now handles cover art Closes #92 --- supysonic/scanner.py | 51 +++++++++++--- supysonic/watcher.py | 94 ++++++++++++++++++++------ tests/base/test_watcher.py | 132 +++++++++++++++++++++++++++++++++---- 3 files changed, 235 insertions(+), 42 deletions(-) diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 1c2f718..99b5527 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -14,7 +14,7 @@ import time from pony.orm import db_session -from .covers import find_cover_in_folder +from .covers import find_cover_in_folder, CoverFile from .db import Folder, Artist, Album, Track, User from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from .db import RatingFolder, RatingTrack @@ -90,14 +90,7 @@ class Scanner: f.delete() # Pony will cascade continue - album_name = None - track = f.tracks.select().first() - if track is not None: - album_name = track.album.name - - cover = find_cover_in_folder(f.path, album_name) - f.cover_art = cover.name if cover is not None else None - + self.find_cover(f.path) folders += f.children folder.last_scan = int(time.time()) @@ -212,6 +205,46 @@ class Scanner: tr.folder = folder tr.path = dst_path + @db_session + def find_cover(self, dirpath): + if not isinstance(dirpath, strtype): # pragma: nocover + raise TypeError('Expecting string, got ' + str(type(dirpath))) + + folder = Folder.get(path = dirpath) + if folder is None: + return + + album_name = None + track = folder.tracks.select().first() + if track is not None: + album_name = track.album.name + + cover = find_cover_in_folder(folder.path, album_name) + folder.cover_art = cover.name if cover is not None else None + + @db_session + def add_cover(self, path): + if not isinstance(path, strtype): # pragma: nocover + raise TypeError('Expecting string, got ' + str(type(path))) + + folder = Folder.get(path = os.path.dirname(path)) + if folder is None: + return + + cover_name = os.path.basename(path) + if not folder.cover_art: + folder.cover_art = cover_name + else: + album_name = None + track = folder.tracks.select().first() + if track is not None: + album_name = track.album.name + + current_cover = CoverFile(folder.cover_art, album_name) + new_cover = CoverFile(cover_name, album_name) + if new_cover.score > current_cover.score: + folder.cover_art = cover_name + def __find_album(self, artist, album): ar = self.__find_artist(artist) al = ar.albums.select(lambda a: a.name == album).first() diff --git a/supysonic/watcher.py b/supysonic/watcher.py index 2f6d794..240f886 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -8,6 +8,7 @@ # Distributed under terms of the GNU AGPLv3 license. import logging +import os.path import time from logging.handlers import TimedRotatingFileHandler @@ -17,6 +18,7 @@ from threading import Thread, Condition, Timer from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler +from . import covers from .db import init_database, release_database, Folder from .py23 import dict from .scanner import Scanner @@ -25,10 +27,13 @@ OP_SCAN = 1 OP_REMOVE = 2 OP_MOVE = 4 FLAG_CREATE = 8 +FLAG_COVER = 16 class SupysonicWatcherEventHandler(PatternMatchingEventHandler): def __init__(self, extensions, queue, logger): - patterns = map(lambda e: "*." + e.lower(), extensions.split()) if extensions else None + patterns = None + if extensions: + patterns = list(map(lambda e: "*." + e.lower(), extensions.split())) + list(map(lambda e: "*" + e, covers.EXTENSIONS)) super(SupysonicWatcherEventHandler, self).__init__(patterns = patterns, ignore_directories = True) self.__queue = queue @@ -37,29 +42,51 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler): def dispatch(self, event): try: super(SupysonicWatcherEventHandler, self).dispatch(event) - except Exception as e: + except Exception as e: # pragma: nocover self.__logger.critical(e) def on_created(self, event): self.__logger.debug("File created: '%s'", event.src_path) - self.__queue.put(event.src_path, OP_SCAN | FLAG_CREATE) + + op = OP_SCAN | FLAG_CREATE + if not covers.is_valid_cover(event.src_path): + self.__queue.put(event.src_path, op) + + dirname = os.path.dirname(event.src_path) + with db_session: + folder = Folder.get(path = dirname) + if folder is None: + self.__queue.put(dirname, op | FLAG_COVER) + else: + self.__queue.put(event.src_path, op | FLAG_COVER) def on_deleted(self, event): self.__logger.debug("File deleted: '%s'", event.src_path) - self.__queue.put(event.src_path, OP_REMOVE) + + op = OP_REMOVE + _, ext = os.path.splitext(event.src_path) + if ext in covers.EXTENSIONS: + op |= FLAG_COVER + self.__queue.put(event.src_path, op) def on_modified(self, event): self.__logger.debug("File modified: '%s'", event.src_path) - self.__queue.put(event.src_path, OP_SCAN) + if not covers.is_valid_cover(event.src_path): + self.__queue.put(event.src_path, OP_SCAN) def on_moved(self, event): self.__logger.debug("File moved: '%s' -> '%s'", event.src_path, event.dest_path) - self.__queue.put(event.dest_path, OP_MOVE, src_path = event.src_path) + + op = OP_MOVE + _, ext = os.path.splitext(event.src_path) + if ext in covers.EXTENSIONS: + op |= FLAG_COVER + self.__queue.put(event.dest_path, op, src_path = event.src_path) class Event(object): def __init__(self, path, operation, **kwargs): if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE): - raise Exception("Flags SCAN and REMOVE both set") + raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover self.__path = path self.__time = time.time() @@ -68,7 +95,7 @@ class Event(object): def set(self, operation, **kwargs): if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE): - raise Exception("Flags SCAN and REMOVE both set") + raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover self.__time = time.time() if operation & OP_SCAN: @@ -113,7 +140,7 @@ class ScannerProcessingQueue(Thread): def run(self): try: self.__run() - except Exception as e: + except Exception as e: # pragma: nocover self.__logger.critical(e) raise e @@ -132,21 +159,48 @@ class ScannerProcessingQueue(Thread): item = self.__next_item() while item: - if item.operation & OP_MOVE: - self.__logger.info("Moving: '%s' -> '%s'", item.src_path, item.path) - scanner.move_file(item.src_path, item.path) - if item.operation & OP_SCAN: - self.__logger.info("Scanning: '%s'", item.path) - scanner.scan_file(item.path) - if item.operation & OP_REMOVE: - self.__logger.info("Removing: '%s'", item.path) - scanner.remove_file(item.path) + if item.operation & FLAG_COVER: + self.__process_cover_item(scanner, item) + else: + self.__process_regular_item(scanner, item) + item = self.__next_item() scanner.finish() self.__logger.debug("Freeing scanner") del scanner + def __process_regular_item(self, scanner, item): + if item.operation & OP_MOVE: + self.__logger.info("Moving: '%s' -> '%s'", item.src_path, item.path) + scanner.move_file(item.src_path, item.path) + + if item.operation & OP_SCAN: + self.__logger.info("Scanning: '%s'", item.path) + scanner.scan_file(item.path) + + if item.operation & OP_REMOVE: + self.__logger.info("Removing: '%s'", item.path) + scanner.remove_file(item.path) + + def __process_cover_item(self, scanner, item): + if item.operation & OP_SCAN: + if os.path.isdir(item.path): + self.__logger.info("Looking for covers: '%s'", item.path) + scanner.find_cover(item.path) + else: + self.__logger.info("Potentially adding cover: '%s'", item.path) + scanner.add_cover(item.path) + + if item.operation & OP_REMOVE: + self.__logger.info("Removing cover: '%s'", item.path) + scanner.find_cover(os.path.dirname(item.path)) + + if item.operation & OP_MOVE: + self.__logger.info("Moving cover: '%s' -> '%s'", item.src_path, item.path) + scanner.find_cover(os.path.dirname(item.src_path)) + scanner.add_cover(item.path) + def stop(self): self.__running = False with self.__cond: @@ -232,7 +286,7 @@ class SupysonicWatcher(object): logger.info("Starting watcher for %s", folder.path) observer.schedule(handler, folder.path, recursive = True) - try: + try: # pragma: nocover signal(SIGTERM, self.__terminate) signal(SIGINT, self.__terminate) except ValueError: @@ -254,5 +308,5 @@ class SupysonicWatcher(object): self.__running = False def __terminate(self, signum, frame): - self.stop() + self.stop() # pragma: nocover diff --git a/tests/base/test_watcher.py b/tests/base/test_watcher.py index 2c19253..b7d29d2 100644 --- a/tests/base/test_watcher.py +++ b/tests/base/test_watcher.py @@ -21,7 +21,7 @@ from hashlib import sha1 from pony.orm import db_session from threading import Thread -from supysonic.db import init_database, release_database, Track, Artist +from supysonic.db import init_database, release_database, Track, Artist, Folder from supysonic.managers.folder import FolderManager from supysonic.watcher import SupysonicWatcher @@ -99,14 +99,26 @@ class WatcherTestCase(WatcherTestBase): with tempfile.NamedTemporaryFile() as f: return os.path.basename(f.name) - def _temppath(self): - return os.path.join(self.__dir, self._tempname() + '.mp3') + def _temppath(self, suffix, depth = 0): + if depth > 0: + dirpath = os.path.join(self.__dir, *(self._tempname() for _ in range(depth))) + os.makedirs(dirpath) + else: + dirpath = self.__dir + return os.path.join(dirpath, self._tempname() + suffix) - def _addfile(self): - path = self._temppath() + def _addfile(self, depth = 0): + path = self._temppath('.mp3', depth) shutil.copyfile('tests/assets/folder/silence.mp3', path) return path + def _addcover(self, suffix = None, depth = 0): + suffix = '.jpg' if suffix is None else (suffix + '.jpg') + path = self._temppath(suffix, depth) + shutil.copyfile('tests/assets/cover.jpg', path) + return path + +class AudioWatcherTestCase(WatcherTestCase): @db_session def assertTrackCountEqual(self, expected): self.assertEqual(Track.select().count(), expected) @@ -163,7 +175,7 @@ class WatcherTestCase(WatcherTestBase): self.assertEqual(Track.select().count(), 1) trackid = Track.select().first().id - newpath = self._temppath() + newpath = self._temppath('.mp3') shutil.move(path, newpath) self._sleep() @@ -179,7 +191,7 @@ class WatcherTestCase(WatcherTestBase): filename = self._tempname() + '.mp3' initialpath = os.path.join(tempfile.gettempdir(), filename) shutil.copyfile('tests/assets/folder/silence.mp3', initialpath) - shutil.move(initialpath, os.path.join(self.__dir, filename)) + shutil.move(initialpath, self._temppath('.mp3')) self._sleep() self.assertTrackCountEqual(1) @@ -212,7 +224,7 @@ class WatcherTestCase(WatcherTestBase): def test_add_rename(self): path = self._addfile() - shutil.move(path, self._temppath()) + shutil.move(path, self._temppath('.mp3')) self._sleep() self.assertTrackCountEqual(1) @@ -221,7 +233,7 @@ class WatcherTestCase(WatcherTestBase): self._sleep() self.assertTrackCountEqual(1) - newpath = self._temppath() + newpath = self._temppath('.mp3') shutil.move(path, newpath) os.unlink(newpath) self._sleep() @@ -229,7 +241,7 @@ class WatcherTestCase(WatcherTestBase): def test_add_rename_delete(self): path = self._addfile() - newpath = self._temppath() + newpath = self._temppath('.mp3') shutil.move(path, newpath) os.unlink(newpath) self._sleep() @@ -240,18 +252,112 @@ class WatcherTestCase(WatcherTestBase): self._sleep() self.assertTrackCountEqual(1) - newpath = self._temppath() - finalpath = self._temppath() + newpath = self._temppath('.mp3') + finalpath = self._temppath('.mp3') shutil.move(path, newpath) shutil.move(newpath, finalpath) self._sleep() self.assertTrackCountEqual(1) +class CoverWatcherTestCase(WatcherTestCase): + def test_add_file_then_cover(self): + self._addfile() + path = self._addcover() + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, os.path.basename(path)) + + def test_add_cover_then_file(self): + path = self._addcover() + self._addfile() + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, os.path.basename(path)) + + def test_remove_cover(self): + self._addfile() + path = self._addcover() + self._sleep() + + os.unlink(path) + self._sleep() + + with db_session: + self.assertIsNone(Folder.select().first().cover_art) + + def test_naming_add_good(self): + bad = os.path.basename(self._addcover()) + self._sleep() + good = os.path.basename(self._addcover('cover')) + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, good) + + def test_naming_add_bad(self): + good = os.path.basename(self._addcover('cover')) + self._sleep() + bad = os.path.basename(self._addcover()) + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, good) + + def test_naming_remove_good(self): + bad = self._addcover() + good = self._addcover('cover') + self._sleep() + os.unlink(good) + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, os.path.basename(bad)) + + def test_naming_remove_bad(self): + bad = self._addcover() + good = self._addcover('cover') + self._sleep() + os.unlink(bad) + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, os.path.basename(good)) + + def test_rename(self): + path = self._addcover() + self._sleep() + newpath = self._temppath('.jpg') + shutil.move(path, newpath) + self._sleep() + + with db_session: + self.assertEqual(Folder.select().first().cover_art, os.path.basename(newpath)) + + def test_add_to_folder_without_track(self): + path = self._addcover(depth = 1) + self._sleep() + + with db_session: + self.assertFalse(Folder.exists(cover_art = os.path.basename(path))) + + def test_remove_from_folder_without_track(self): + path = self._addcover(depth = 1) + self._sleep() + os.unlink(path) + self._sleep() + + def test_add_track_to_empty_folder(self): + self._addfile(1) + self._sleep() + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(NothingToWatchTestCase)) - suite.addTest(unittest.makeSuite(WatcherTestCase)) + suite.addTest(unittest.makeSuite(AudioWatcherTestCase)) + suite.addTest(unittest.makeSuite(CoverWatcherTestCase)) return suite From 66bc65a46c9bd58fefc82243a8fbda9cccb8b286 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 1 Sep 2018 17:52:00 +0200 Subject: [PATCH 16/17] Fixed setup installing tests --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ca97a79..3b46027 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup( author_email=project.AUTHOR_EMAIL, url=project.URL, license=project.LICENSE, - packages=find_packages(), + packages=find_packages(exclude=['tests*']), install_requires = reqs, extras_require = extras, scripts=['bin/supysonic-cli', 'bin/supysonic-watcher'], From 6f6521786a90ce669752f7edf79990067f864730 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 8 Sep 2018 15:22:28 +0200 Subject: [PATCH 17/17] Updated README for Apache2 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 715f48c..eefbacc 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,7 @@ example of what it looks like: WSGIApplicationGroup %{GLOBAL} WSGIPassAuthorization On - Order deny,allow - Allow from all + Require all granted You might also need to run _Apache_ using the system default locale, as the one