diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index 3427acf..e14d7b7 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -82,11 +82,29 @@ def get_client_prefs(): def get_entity(cls, param="id"): eid = request.values[param] - eid = uuid.UUID(eid) + if cls == Folder: + eid = int(eid) + else: + eid = uuid.UUID(eid) entity = cls[eid] return entity +def get_entity_id(cls, eid): + """Return the entity ID as its proper type.""" + if cls == Folder: + if isinstance(eid, uuid.UUID): + raise GenericError("Invalid ID") + try: + return int(eid) + except ValueError: + raise GenericError("Invalid ID") + try: + return uuid.UUID(eid) + except (AttributeError, ValueError): + raise GenericError("Invalid ID") + + from .errors import * from .system import * diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index 4d502ad..0e84a5d 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -7,8 +7,6 @@ # # Distributed under terms of the GNU AGPLv3 license. -import uuid - from datetime import timedelta from flask import request from pony.orm import select, desc, avg, max, min, count @@ -42,7 +40,12 @@ def rand_songs(): size = int(size) if size else 10 fromYear = int(fromYear) if fromYear else None toYear = int(toYear) if toYear else None - fid = uuid.UUID(musicFolderId) if musicFolderId else None + fid = None + if musicFolderId: + try: + fid = int(musicFolderId) + except ValueError: + raise ValueError("Invalid folder ID") query = Track.select() if fromYear: diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py index 08612fb..2b8b64c 100644 --- a/supysonic/api/annotation.py +++ b/supysonic/api/annotation.py @@ -21,7 +21,7 @@ from ..db import RatingTrack, RatingFolder from ..lastfm import LastFm from ..py23 import dict -from . import api, get_entity +from . import api, get_entity, get_entity_id from .exceptions import AggregateException, GenericError, MissingParameter, NotFound @@ -32,12 +32,14 @@ def star_single(cls, eid): :param eid: id of the entity to star """ - uid = uuid.UUID(eid) - e = cls[uid] + try: + e = cls[eid] + except ObjectNotFound: + raise NotFound("{} {}".format(cls.__name__, eid)) starred_cls = getattr(sys.modules[__name__], "Starred" + cls.__name__) try: - starred_cls[request.user, uid] + starred_cls[request.user, eid] raise GenericError("{} {} already starred".format(cls.__name__, eid)) except ObjectNotFound: pass @@ -52,10 +54,9 @@ def unstar_single(cls, eid): :param eid: id of the entity to unstar """ - uid = uuid.UUID(eid) starred_cls = getattr(sys.modules[__name__], "Starred" + cls.__name__) delete( - s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid + s for s in starred_cls if s.user.id == request.user.id and s.starred.id == eid ) return None @@ -68,30 +69,44 @@ def handle_star_request(func): errors = [] for eid in id: - terr = None - ferr = None - try: - func(Track, eid) - except Exception as e: - terr = e + tid = get_entity_id(Track, eid) + except GenericError: + tid = None try: - func(Folder, eid) - except Exception as e: - ferr = e + fid = get_entity_id(Folder, eid) + except GenericError: + fid = None + err = None - if terr and ferr: - errors += [terr, ferr] + if tid is None and fid is None: + raise GenericError("Invalid ID") + + if tid is not None: + try: + func(Track, tid) + except Exception as e: + err = e + else: + try: + func(Folder, fid) + except Exception as e: + err = e + + if err: + errors.append(err) for alId in albumId: + alb_id = get_entity_id(Album, alId) try: - func(Album, alId) + func(Album, alb_id) except Exception as e: errors.append(e) for arId in artistId: + art_id = get_entity_id(Artist, arId) try: - func(Artist, arId) + func(Artist, art_id) except Exception as e: errors.append(e) @@ -115,31 +130,43 @@ def rate(): id = request.values["id"] rating = request.values["rating"] - uid = uuid.UUID(id) + try: + tid = get_entity_id(Track, id) + except GenericError: + tid = None + try: + fid = get_entity_id(Folder, id) + except GenericError: + fid = None + uid = None rating = int(rating) + if tid is None and fid is None: + raise GenericError("Invalid ID") + if not 0 <= rating <= 5: raise GenericError("rating must be between 0 and 5 (inclusive)") if rating == 0: - delete( - r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == uid - ) - delete( - r - for r in RatingFolder - if r.user.id == request.user.id and r.rated.id == uid - ) + if tid is not None: + delete( + r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == tid + ) + else: + delete( + r + for r in RatingFolder + if r.user.id == request.user.id and r.rated.id == fid + ) else: - try: - rated = Track[uid] + if tid is not None: + rated = Track[tid] rating_cls = RatingTrack - except ObjectNotFound: - try: - rated = Folder[uid] - rating_cls = RatingFolder - except ObjectNotFound: - raise NotFound("Track or Folder") + uid = tid + else: + rated = Folder[fid] + rating_cls = RatingFolder + uid = fid try: rating_info = rating_cls[request.user, uid] diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index 72949f5..884b066 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -16,7 +16,7 @@ from pony.orm import ObjectNotFound, select from ..db import Folder, Artist, Album, Track from ..py23 import dict -from . import api, get_entity +from . import api, get_entity, get_entity_id @api.route("/getMusicFolders.view", methods=["GET", "POST"]) @@ -42,7 +42,7 @@ def list_indexes(): if musicFolderId is None: folders = Folder.select(lambda f: f.root)[:] else: - mfid = uuid.UUID(musicFolderId) + mfid = get_entity_id(Folder, musicFolderId) folder = Folder[mfid] if not folder.root: raise ObjectNotFound(Folder, mfid) diff --git a/supysonic/api/media.py b/supysonic/api/media.py index fc7d18d..22716c7 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -35,7 +35,7 @@ from ..covers import get_embedded_cover from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now from ..py23 import dict -from . import api, get_entity +from . import api, get_entity, get_entity_id from .exceptions import ( GenericError, MissingParameter, @@ -190,21 +190,33 @@ def stream_media(): @api.route("/download.view", methods=["GET", "POST"]) def download_media(): id = request.values["id"] - uid = uuid.UUID(id) - try: # Track -> direct download - rv = Track[uid] - return send_file(rv.path, mimetype=rv.mimetype, conditional=True) - except ObjectNotFound: - pass + try: + uid = get_entity_id(Track, id) + except GenericError: + uid = None + try: + fid = get_entity_id(Folder, id) + except GenericError: + fid = None - try: # Folder -> stream zipped tracks, non recursive - rv = Folder[uid] - except ObjectNotFound: - try: # Album -> stream zipped tracks - rv = Album[uid] + if uid is None and fid is None: + raise GenericError("Invalid ID") + + if uid is not None: + try: + rv = Track[uid] + return send_file(rv.path, mimetype=rv.mimetype, conditional=True) except ObjectNotFound: - raise NotFound("Track, Folder or Album") + try: # Album -> stream zipped tracks + rv = Album[uid] + except ObjectNotFound: + raise NotFound("Track or Album") + else: + try: # Folder -> stream zipped tracks, non recursive + rv = Folder[fid] + except ObjectNotFound: + raise NotFound("Folder") z = ZipFile(compression=ZIP_DEFLATED) for track in rv.tracks: @@ -217,15 +229,28 @@ def download_media(): @api.route("/getCoverArt.view", methods=["GET", "POST"]) def cover_art(): cache = current_app.cache + eid = request.values["id"] - if Folder.exists(id=eid): + try: + fid = get_entity_id(Folder, eid) + except GenericError: + fid = None + try: + tid = get_entity_id(Track, eid) + except GenericError: + tid = None + + if not fid and not tid: + raise GenericError("Invalid ID") + + if fid and Folder.exists(id=eid): res = get_entity(Folder) 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) - elif Track.exists(id=eid): + elif tid and Track.exists(id=eid): cache_key = "{}-cover".format(eid) try: cover_path = cache.get(cache_key) diff --git a/supysonic/config.py b/supysonic/config.py index a83616d..de9d5ea 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -90,8 +90,13 @@ class IniConfig(DefaultConfig): lv = value.lower() if lv in ("yes", "true", "on"): return True - elif lv in ("no", "false", "off"): + if lv in ("no", "false", "off"): return False + try: + if isinstance(value, unicode): + return str(value) + except NameError: + pass return value @classmethod diff --git a/supysonic/db.py b/supysonic/db.py index d23ac53..2da1152 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -29,7 +29,7 @@ try: except ImportError: from urlparse import urlparse, parse_qsl -SCHEMA_VERSION = "20190518" +SCHEMA_VERSION = "20190915" def now(): @@ -79,7 +79,7 @@ class PathMixin(object): class Folder(PathMixin, db.Entity): _table_ = "folder" - id = PrimaryKey(UUID, default=uuid4) + id = PrimaryKey(int, auto=True) root = Required(bool, default=False) name = Required(str, autostrip=False) path = Required(str, 4096, autostrip=False) # unique diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py index db8cb46..eede1da 100644 --- a/supysonic/managers/folder.py +++ b/supysonic/managers/folder.py @@ -21,15 +21,13 @@ from ..py23 import strtype class FolderManager: @staticmethod - def get(uid): - if isinstance(uid, strtype): - uid = uuid.UUID(uid) - elif isinstance(uid, uuid.UUID): - pass - else: + def get(id): + try: + id = int(id) + except ValueError: raise ValueError("Invalid folder id") - return Folder[uid] + return Folder[id] @staticmethod def add(name, path): @@ -55,8 +53,8 @@ class FolderManager: return folder @staticmethod - def delete(uid): - folder = FolderManager.get(uid) + def delete(id): + folder = FolderManager.get(id) if not folder.root: raise ObjectNotFound(Folder) diff --git a/supysonic/schema/migration/mysql/20190915.sql b/supysonic/schema/migration/mysql/20190915.sql new file mode 100644 index 0000000..05e915b --- /dev/null +++ b/supysonic/schema/migration/mysql/20190915.sql @@ -0,0 +1,69 @@ +START TRANSACTION; + +CREATE TEMPORARY TABLE IF NOT EXISTS folder_id_to_int ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + uuid BINARY(16) NOT NULL +); + +INSERT INTO folder_id_to_int(uuid) SELECT id FROM folder; + +DROP INDEX index_folder_parent_id_fk ON folder; +DROP INDEX index_track_folder_id_fk ON track; +DROP INDEX index_track_root_folder_id_fk ON track; +DROP INDEX index_starred_folder_starred_id_fk ON starred_folder; +DROP INDEX index_rating_folder_rated_id_fk ON rating_folder; + + +ALTER TABLE folder + ADD int_id INTEGER AFTER id, + ADD int_parent_id INTEGER REFERENCES folder AFTER parent_id; +UPDATE folder SET int_id = (SELECT id FROM folder_id_to_int WHERE uuid = folder.id); +UPDATE folder SET int_parent_id = (SELECT id FROM folder_id_to_int WHERE uuid = folder.parent_id); +ALTER TABLE folder + DROP PRIMARY KEY, + DROP COLUMN id, + DROP COLUMN parent_id, + CHANGE COLUMN int_id id INTEGER AUTO_INCREMENT, + CHANGE COLUMN int_parent_id parent_id INTEGER, + ADD PRIMARY KEY (id); + + +ALTER TABLE track + ADD int_root_folder_id INTEGER NOT NULL REFERENCES folder AFTER root_folder_id, + ADD int_folder_id INTEGER NOT NULL REFERENCES folder AFTER folder_id; +UPDATE track SET int_root_folder_id = (SELECT id FROM folder_id_to_int WHERE uuid = track.root_folder_id); +UPDATE track SET int_folder_id = (SELECT id FROM folder_id_to_int WHERE uuid = track.folder_id); +ALTER TABLE track + DROP COLUMN root_folder_id, + DROP COLUMN folder_id, + CHANGE COLUMN int_root_folder_id root_folder_id INTEGER NOT NULL, + CHANGE COLUMN int_folder_id folder_id INTEGER NOT NULL; + + +ALTER TABLE starred_folder ADD int_starred_id INTEGER NOT NULL REFERENCES folder AFTER starred_id; +UPDATE starred_folder SET int_starred_id = (SELECT id FROM folder_id_to_int WHERE uuid = starred_folder.starred_id); +ALTER TABLE starred_folder + DROP PRIMARY KEY, + DROP COLUMN starred_id, + CHANGE COLUMN int_starred_id starred_id INTEGER NOT NULL, + ADD PRIMARY KEY (user_id, starred_id); + + +ALTER TABLE rating_folder ADD int_rated_id INTEGER NOT NULL REFERENCES folder AFTER rated_id; +UPDATE rating_folder SET int_rated_id = (SELECT id FROM folder_id_to_int WHERE uuid = rating_folder.rated_id); +ALTER TABLE rating_folder + DROP PRIMARY KEY, + DROP COLUMN rated_id, + CHANGE COLUMN int_rated_id rated_id INTEGER NOT NULL, + ADD PRIMARY KEY (user_id, rated_id); + + +CREATE INDEX IF NOT EXISTS index_folder_parent_id_fk ON folder(parent_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 INDEX IF NOT EXISTS index_starred_folder_starred_id_fk ON starred_folder(starred_id); +CREATE INDEX IF NOT EXISTS index_rating_folder_rated_id_fk ON rating_folder(rated_id); + +DROP TABLE folder_id_to_int; + +COMMIT; diff --git a/supysonic/schema/migration/postgres/20190915.sql b/supysonic/schema/migration/postgres/20190915.sql new file mode 100644 index 0000000..39d0678 --- /dev/null +++ b/supysonic/schema/migration/postgres/20190915.sql @@ -0,0 +1,83 @@ +START TRANSACTION; + +CREATE TEMPORARY TABLE IF NOT EXISTS folder_id_to_int ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + uid UUID NOT NULL +); + +INSERT INTO folder_id_to_int(uid) SELECT id FROM folder; + +ALTER TABLE folder DROP CONSTRAINT folder_parent_id_fkey; +ALTER TABLE rating_folder DROP CONSTRAINT rating_folder_rated_id_fkey; +ALTER TABLE starred_folder DROP CONSTRAINT starred_folder_starred_id_fkey; +ALTER TABLE track DROP CONSTRAINT track_folder_id_fkey; +ALTER TABLE track DROP CONSTRAINT track_root_folder_id_fkey; + + +ALTER TABLE folder + ADD int_id INTEGER, + ADD int_parent_id INTEGER; +UPDATE folder SET int_id = (SELECT id FROM folder_id_to_int WHERE uid = folder.id); +UPDATE folder SET int_parent_id = (SELECT id FROM folder_id_to_int WHERE uid = folder.parent_id); +CREATE SEQUENCE folder_id_seq AS INTEGER; +SELECT setval('folder_id_seq', coalesce(max(int_id), 0) + 1, false) FROM folder; +ALTER TABLE folder + DROP CONSTRAINT folder_pkey, + DROP COLUMN id, + DROP COLUMN parent_id, + ALTER COLUMN int_id SET DEFAULT nextval('folder_id_seq'), + ADD PRIMARY KEY (int_id); +ALTER TABLE folder RENAME COLUMN int_id TO id; +ALTER TABLE folder RENAME COLUMN int_parent_id TO parent_id; +ALTER TABLE folder ADD CONSTRAINT folder_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES folder(id); + + +ALTER TABLE track + ADD int_root_folder_id INTEGER, + ADD int_folder_id INTEGER; +UPDATE track SET int_root_folder_id = (SELECT id FROM folder_id_to_int WHERE uid = track.root_folder_id); +UPDATE track SET int_folder_id = (SELECT id FROM folder_id_to_int WHERE uid = track.folder_id); +ALTER TABLE track + DROP COLUMN root_folder_id, + DROP COLUMN folder_id, + ALTER COLUMN int_root_folder_id SET NOT NULL, + ALTER COLUMN int_folder_id SET NOT NULL; +ALTER TABLE track RENAME COLUMN int_root_folder_id TO root_folder_id; +ALTER TABLE track RENAME COLUMN int_folder_id TO folder_id; +ALTER TABLE track ADD CONSTRAINT track_folder_id_fkey FOREIGN KEY (folder_id) REFERENCES folder(id); +ALTER TABLE track ADD CONSTRAINT track_root_folder_id_fkey FOREIGN KEY (root_folder_id) REFERENCES folder(id); + + +ALTER TABLE starred_folder ADD int_starred_id INTEGER; +UPDATE starred_folder SET int_starred_id = (SELECT id FROM folder_id_to_int WHERE uid = starred_folder.starred_id); +ALTER TABLE starred_folder + DROP CONSTRAINT starred_folder_pkey, + DROP COLUMN starred_id, + ALTER COLUMN int_starred_id SET NOT NULL, + ADD PRIMARY KEY (user_id, int_starred_id); +ALTER TABLE starred_folder RENAME COLUMN int_starred_id TO starred_id; +ALTER TABLE starred_folder ADD CONSTRAINT starred_folder_starred_id_fkey FOREIGN KEY (starred_id) REFERENCES folder(id); + + +ALTER TABLE rating_folder ADD int_rated_id INTEGER; +UPDATE rating_folder SET int_rated_id = (SELECT id FROM folder_id_to_int WHERE uid = rating_folder.rated_id); +ALTER TABLE rating_folder + DROP CONSTRAINT rating_folder_pkey, + DROP COLUMN rated_id, + ALTER COLUMN int_rated_id SET NOT NULL, + ADD PRIMARY KEY (user_id, int_rated_id); +ALTER TABLE rating_folder RENAME COLUMN int_rated_id TO rated_id; +ALTER TABLE rating_folder ADD CONSTRAINT rating_folder_rated_id_fkey FOREIGN KEY (rated_id) REFERENCES folder(id); + + +CREATE INDEX IF NOT EXISTS index_folder_parent_id_fk ON folder(parent_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 INDEX IF NOT EXISTS index_starred_folder_starred_id_fk ON starred_folder(starred_id); +CREATE INDEX IF NOT EXISTS index_rating_folder_rated_id_fk ON rating_folder(rated_id); + + + +DROP TABLE folder_id_to_int; + +COMMIT; diff --git a/supysonic/schema/migration/sqlite/20190915.sql b/supysonic/schema/migration/sqlite/20190915.sql new file mode 100644 index 0000000..2e0062e --- /dev/null +++ b/supysonic/schema/migration/sqlite/20190915.sql @@ -0,0 +1,136 @@ +COMMIT; +PRAGMA foreign_keys = OFF; +BEGIN TRANSACTION; + +CREATE TEMPORARY TABLE IF NOT EXISTS folder_id_to_int ( + id INTEGER NOT NULL PRIMARY KEY, + uuid CHAR(36) NOT NULL +); + +INSERT INTO folder_id_to_int(uuid) SELECT id FROM folder; + +DROP INDEX index_folder_parent_id_fk; +DROP INDEX index_folder_path; + +CREATE TABLE IF NOT EXISTS folder_new ( + id INTEGER NOT NULL 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 INTEGER REFERENCES folder +); +CREATE INDEX IF NOT EXISTS index_folder_parent_id_fk ON folder_new(parent_id); +CREATE UNIQUE INDEX IF NOT EXISTS index_folder_path ON folder_new(path_hash); + +INSERT INTO folder_new(id, root, name, path, path_hash, created, cover_art, last_scan, parent_id) +SELECT id_int.id, root, name, path, path_hash, created, cover_art, last_scan, NULL +FROM folder +JOIN folder_id_to_int id_int ON folder.id == id_int.uuid +WHERE folder.parent_id IS NULL; + +INSERT INTO folder_new(id, root, name, path, path_hash, created, cover_art, last_scan, parent_id) +SELECT id_int.id, root, name, path, path_hash, created, cover_art, last_scan, parent_id_int.id +FROM folder +JOIN folder_id_to_int id_int ON folder.id == id_int.uuid +JOIN folder_id_to_int parent_id_int ON folder.parent_id == parent_id_int.uuid +WHERE folder.parent_id IS NOT NULL; + +DROP TABLE folder; +ALTER TABLE folder_new RENAME TO folder; + + +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; + +CREATE TABLE IF NOT EXISTS track_new ( + 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 INTEGER NOT NULL REFERENCES folder, + folder_id INTEGER NOT NULL REFERENCES folder +); +CREATE INDEX IF NOT EXISTS index_track_album_id_fk ON track_new(album_id); +CREATE INDEX IF NOT EXISTS index_track_artist_id_fk ON track_new(artist_id); +CREATE INDEX IF NOT EXISTS index_track_folder_id_fk ON track_new(folder_id); +CREATE INDEX IF NOT EXISTS index_track_root_folder_id_fk ON track_new(root_folder_id); +CREATE UNIQUE INDEX IF NOT EXISTS index_track_path ON track_new(path_hash); + +INSERT INTO track_new(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 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_id_int.id, folder_id_int.id +FROM track +JOIN folder_id_to_int root_id_int ON track.root_folder_id == root_id_int.uuid +JOIN folder_id_to_int folder_id_int ON track.folder_id == folder_id_int.uuid; + +DROP TABLE track; +ALTER TABLE track_new RENAME TO track; + + +DROP INDEX index_starred_folder_user_id_fk; +DROP INDEX index_starred_folder_starred_id_fk; + +CREATE TABLE IF NOT EXISTS starred_folder_new ( + user_id CHAR(36) NOT NULL REFERENCES user, + starred_id INTEGER NOT NULL REFERENCES folder, + date DATETIME NOT NULL, + PRIMARY KEY (user_id, starred_id) +); +CREATE INDEX IF NOT EXISTS index_starred_folder_user_id_fk ON starred_folder_new(user_id); +CREATE INDEX IF NOT EXISTS index_starred_folder_starred_id_fk ON starred_folder_new(starred_id); + +INSERT INTO starred_folder_new(user_id, starred_id, date) +SELECT user_id, id_int.id, date +FROM starred_folder +JOIN folder_id_to_int id_int ON starred_folder.starred_id == id_int.uuid; + +DROP TABLE starred_folder; +ALTER TABLE starred_folder_new RENAME TO starred_folder; + + +DROP INDEX index_rating_folder_user_id_fk; +DROP INDEX index_rating_folder_rated_id_fk; + +CREATE TABLE IF NOT EXISTS rating_folder_new ( + user_id CHAR(36) NOT NULL REFERENCES user, + rated_id INTEGER NOT NULL REFERENCES folder, + rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), + PRIMARY KEY (user_id, rated_id) +); +CREATE INDEX IF NOT EXISTS index_rating_folder_user_id_fk ON rating_folder_new(user_id); +CREATE INDEX IF NOT EXISTS index_rating_folder_rated_id_fk ON rating_folder_new(rated_id); + +INSERT INTO rating_folder_new(user_id, rated_id, rating) +SELECT user_id, id_int.id, rating +FROM rating_folder +JOIN folder_id_to_int id_int ON rating_folder.rated_id == id_int.uuid; + +DROP TABLE rating_folder; +ALTER TABLE rating_folder_new RENAME TO rating_folder; + + +DROP TABLE folder_id_to_int; + +COMMIT; +VACUUM; +PRAGMA foreign_keys = ON; +BEGIN TRANSACTION; diff --git a/supysonic/schema/mysql.sql b/supysonic/schema/mysql.sql index 864b2a8..ebe0820 100644 --- a/supysonic/schema/mysql.sql +++ b/supysonic/schema/mysql.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS folder ( - id BINARY(16) PRIMARY KEY, + id INTEGER PRIMARY KEY AUTO_INCREMENT, root BOOLEAN NOT NULL, name VARCHAR(256) NOT NULL, path VARCHAR(4096) NOT NULL, @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS folder ( created DATETIME NOT NULL, cover_art VARCHAR(256), last_scan INTEGER NOT NULL, - parent_id BINARY(16) REFERENCES folder + parent_id INTEGER REFERENCES folder ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS index_folder_parent_id_fk ON folder(parent_id); @@ -41,8 +41,8 @@ CREATE TABLE IF NOT EXISTS track ( last_modification INTEGER NOT NULL, play_count INTEGER NOT NULL, last_play DATETIME, - root_folder_id BINARY(16) NOT NULL REFERENCES folder, - folder_id BINARY(16) NOT NULL REFERENCES folder + root_folder_id INTEGER NOT NULL REFERENCES folder, + folder_id INTEGER NOT NULL REFERENCES folder ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 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); @@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS client_prefs ( CREATE TABLE IF NOT EXISTS starred_folder ( user_id BINARY(16) NOT NULL REFERENCES user, - starred_id BINARY(16) NOT NULL REFERENCES folder, + starred_id INTEGER NOT NULL REFERENCES folder, date DATETIME NOT NULL, PRIMARY KEY (user_id, starred_id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -109,7 +109,7 @@ CREATE INDEX IF NOT EXISTS index_starred_track_starred_id_fk ON starred_track(st CREATE TABLE IF NOT EXISTS rating_folder ( user_id BINARY(16) NOT NULL REFERENCES user, - rated_id BINARY(16) NOT NULL REFERENCES folder, + rated_id INTEGER NOT NULL REFERENCES folder, rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), PRIMARY KEY (user_id, rated_id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/supysonic/schema/postgres.sql b/supysonic/schema/postgres.sql index 95c9e94..f763148 100644 --- a/supysonic/schema/postgres.sql +++ b/supysonic/schema/postgres.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS folder ( - id UUID PRIMARY KEY, + id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, root BOOLEAN NOT NULL, name CITEXT NOT NULL, path VARCHAR(4096) NOT NULL, @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS folder ( created TIMESTAMP NOT NULL, cover_art VARCHAR(256), last_scan INTEGER NOT NULL, - parent_id UUID REFERENCES folder + parent_id INTEGER REFERENCES folder ); CREATE INDEX IF NOT EXISTS index_folder_parent_id_fk ON folder(parent_id); @@ -41,8 +41,8 @@ CREATE TABLE IF NOT EXISTS track ( last_modification INTEGER NOT NULL, play_count INTEGER NOT NULL, last_play TIMESTAMP, - root_folder_id UUID NOT NULL REFERENCES folder, - folder_id UUID NOT NULL REFERENCES folder + root_folder_id INTEGER NOT NULL REFERENCES folder, + folder_id INTEGER 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); @@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS client_prefs ( CREATE TABLE IF NOT EXISTS starred_folder ( user_id UUID NOT NULL REFERENCES "user", - starred_id UUID NOT NULL REFERENCES folder, + starred_id INTEGER NOT NULL REFERENCES folder, date TIMESTAMP NOT NULL, PRIMARY KEY (user_id, starred_id) ); @@ -109,7 +109,7 @@ CREATE INDEX IF NOT EXISTS index_starred_track_starred_id_fk ON starred_track(st CREATE TABLE IF NOT EXISTS rating_folder ( user_id UUID NOT NULL REFERENCES "user", - rated_id UUID NOT NULL REFERENCES folder, + rated_id INTEGER NOT NULL REFERENCES folder, rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), PRIMARY KEY (user_id, rated_id) ); diff --git a/supysonic/schema/sqlite.sql b/supysonic/schema/sqlite.sql index 23a7ff8..40e8441 100644 --- a/supysonic/schema/sqlite.sql +++ b/supysonic/schema/sqlite.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS folder ( - id CHAR(36) PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY, root BOOLEAN NOT NULL, name VARCHAR(256) NOT NULL COLLATE NOCASE, path VARCHAR(4096) NOT NULL, @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS folder ( created DATETIME NOT NULL, cover_art VARCHAR(256), last_scan INTEGER NOT NULL, - parent_id CHAR(36) REFERENCES folder + parent_id INTEGER REFERENCES folder ); CREATE INDEX IF NOT EXISTS index_folder_parent_id_fk ON folder(parent_id); CREATE UNIQUE INDEX IF NOT EXISTS index_folder_path ON folder(path_hash); @@ -42,8 +42,8 @@ CREATE TABLE IF NOT EXISTS track ( 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 + root_folder_id INTEGER NOT NULL REFERENCES folder, + folder_id INTEGER 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); @@ -75,7 +75,7 @@ CREATE TABLE IF NOT EXISTS client_prefs ( CREATE TABLE IF NOT EXISTS starred_folder ( user_id CHAR(36) NOT NULL REFERENCES user, - starred_id CHAR(36) NOT NULL REFERENCES folder, + starred_id INTEGER NOT NULL REFERENCES folder, date DATETIME NOT NULL, PRIMARY KEY (user_id, starred_id) ); @@ -111,7 +111,7 @@ CREATE INDEX IF NOT EXISTS index_starred_track_starred_id_fk ON starred_track(st CREATE TABLE IF NOT EXISTS rating_folder ( user_id CHAR(36) NOT NULL REFERENCES user, - rated_id CHAR(36) NOT NULL REFERENCES folder, + rated_id INTEGER NOT NULL REFERENCES folder, rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), PRIMARY KEY (user_id, rated_id) ); diff --git a/supysonic/utils.py b/supysonic/utils.py index eb0fbbe..f621ca0 100644 --- a/supysonic/utils.py +++ b/supysonic/utils.py @@ -6,12 +6,13 @@ # Copyright (C) 2019 Alban 'spl0k' FĂ©ron # # Distributed under terms of the GNU AGPLv3 license. +import uuid from base64 import b64encode, b64decode from os import urandom from pony.orm import db_session, commit, ObjectNotFound -from supysonic.db import Meta +from supysonic.db import Folder, Meta @db_session diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index 10fefbc..3d86c81 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -118,7 +118,7 @@ class AlbumSongsTestCase(ApiTestBase): self._make_request("getRandomSongs", {"fromYear": "year"}, error=0) self._make_request("getRandomSongs", {"toYear": "year"}, error=0) self._make_request("getRandomSongs", {"musicFolderId": "idid"}, error=0) - self._make_request("getRandomSongs", {"musicFolderId": uuid.uuid4()}, error=70) + self._make_request("getRandomSongs", {"musicFolderId": 1234567890}, error=70) rv, child = self._make_request( "getRandomSongs", tag="randomSongs", skip_post=True diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py index ef2b732..da4bc74 100644 --- a/tests/api/test_annotation.py +++ b/tests/api/test_annotation.py @@ -27,6 +27,10 @@ class AnnotationTestCase(ApiTestBase): artist = Artist(name="Artist") album = Album(name="Album", artist=artist) + # Populate folder ids + root = Folder.get(name="Root") + folder = Folder.get(name="Folder") + track = Track( title="Track", album=album, @@ -73,7 +77,7 @@ class AnnotationTestCase(ApiTestBase): self.assertIn("starred", Folder[self.folderid].as_subsonic_child(self.user)) self._make_request("star", {"id": str(self.folderid)}, error=0, skip_xsd=True) - self._make_request("star", {"albumId": str(self.folderid)}, error=70) + self._make_request("star", {"albumId": str(self.folderid)}, error=0) self._make_request("star", {"albumId": str(self.artistid)}, error=70) self._make_request("star", {"albumId": str(self.trackid)}, error=70) self._make_request("star", {"albumId": str(self.albumid)}, skip_post=True) @@ -81,7 +85,7 @@ class AnnotationTestCase(ApiTestBase): self.assertIn("starred", Album[self.albumid].as_subsonic_album(self.user)) self._make_request("star", {"albumId": str(self.albumid)}, error=0) - self._make_request("star", {"artistId": str(self.folderid)}, error=70) + self._make_request("star", {"artistId": str(self.folderid)}, error=0) self._make_request("star", {"artistId": str(self.albumid)}, error=70) self._make_request("star", {"artistId": str(self.trackid)}, error=70) self._make_request("star", {"artistId": str(self.artistid)}, skip_post=True) @@ -213,7 +217,7 @@ class AnnotationTestCase(ApiTestBase): self._make_request("scrobble", error=10) self._make_request("scrobble", {"id": "song"}, error=0) self._make_request("scrobble", {"id": str(uuid.uuid4())}, error=70) - self._make_request("scrobble", {"id": str(self.folderid)}, error=70) + self._make_request("scrobble", {"id": str(self.folderid)}, error=0) self._make_request("scrobble", {"id": str(self.trackid)}) self._make_request("scrobble", {"id": str(self.trackid), "submission": True}) diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py index 6d40bd8..395ead1 100644 --- a/tests/api/test_browse.py +++ b/tests/api/test_browse.py @@ -82,7 +82,7 @@ class BrowseTestCase(ApiTestBase): def test_get_indexes(self): self._make_request("getIndexes", {"musicFolderId": "abcdef"}, error=0) - self._make_request("getIndexes", {"musicFolderId": str(uuid.uuid4())}, error=70) + self._make_request("getIndexes", {"musicFolderId": 1234567890}, error=70) self._make_request("getIndexes", {"ifModifiedSince": "quoi"}, error=0) rv, child = self._make_request( @@ -109,7 +109,7 @@ class BrowseTestCase(ApiTestBase): def test_get_music_directory(self): self._make_request("getMusicDirectory", error=10) self._make_request("getMusicDirectory", {"id": "id"}, error=0) - self._make_request("getMusicDirectory", {"id": str(uuid.uuid4())}, error=70) + self._make_request("getMusicDirectory", {"id": 1234567890}, error=70) # should test with folders with both children folders and tracks. this code would break in that case with db_session: diff --git a/tests/api/test_media.py b/tests/api/test_media.py index 42c14e3..b24f6ad 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -31,6 +31,7 @@ class MediaTestCase(ApiTestBase): root=True, cover_art="cover.jpg", ) + folder = Folder.get(name="Root") self.folderid = folder.id artist = Artist(name="Artist") @@ -74,7 +75,7 @@ class MediaTestCase(ApiTestBase): self._make_request("stream", error=10) self._make_request("stream", {"id": "string"}, error=0) self._make_request("stream", {"id": str(uuid.uuid4())}, error=70) - self._make_request("stream", {"id": str(self.folderid)}, error=70) + self._make_request("stream", {"id": str(self.folderid)}, error=0) self._make_request( "stream", {"id": str(self.trackid), "maxBitRate": "string"}, error=0 ) diff --git a/tests/base/test_db.py b/tests/base/test_db.py index 5251081..54768da 100644 --- a/tests/base/test_db.py +++ b/tests/base/test_db.py @@ -50,7 +50,12 @@ class DbTestCase(unittest.TestCase): parent=root_folder, ) - return root_folder, child_folder, child_2 + # Folder IDs don't get populated until we query the db. + return ( + db.Folder.get(name="Root folder"), + db.Folder.get(name="Child folder"), + db.Folder.get(name="Child Folder (No Art)") + ) def create_some_tracks(self, artist=None, album=None): root, child, child_2 = self.create_some_folders() @@ -209,7 +214,7 @@ class DbTestCase(unittest.TestCase): root_folder, folder_art, folder_noart = self.create_some_folders() track1 = self.create_track_in( - root_folder, folder_noart, artist=artist, album=album + folder_noart, root_folder, artist=artist, album=album ) album_dict = album.as_subsonic_album(user) diff --git a/tests/frontend/test_folder.py b/tests/frontend/test_folder.py index c9f3221..c0ca1ea 100644 --- a/tests/frontend/test_folder.py +++ b/tests/frontend/test_folder.py @@ -72,8 +72,8 @@ class FolderTestCase(FrontendTestBase): self._login("alice", "Alic3") rv = self.client.get("/folder/del/string", follow_redirects=True) - self.assertIn("badly formed", rv.data) - rv = self.client.get("/folder/del/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("Invalid folder id", rv.data) + rv = self.client.get("/folder/del/1234567890", follow_redirects=True) self.assertIn("No such folder", rv.data) rv = self.client.get("/folder/del/" + str(folder.id), follow_redirects=True) self.assertIn("Music folders", rv.data) @@ -87,8 +87,8 @@ class FolderTestCase(FrontendTestBase): self._login("alice", "Alic3") rv = self.client.get("/folder/scan/string", follow_redirects=True) - self.assertIn("badly formed", rv.data) - rv = self.client.get("/folder/scan/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("Invalid folder id", rv.data) + rv = self.client.get("/folder/scan/1234567890", follow_redirects=True) self.assertIn("No such folder", rv.data) rv = self.client.get("/folder/scan/" + str(folder.id), follow_redirects=True) self.assertIn("start", rv.data) diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py index 84e9dd1..27a3b2e 100644 --- a/tests/managers/test_manager_folder.py +++ b/tests/managers/test_manager_folder.py @@ -76,7 +76,7 @@ class FolderManagerTestCase(unittest.TestCase): self.assertRaises(ValueError, FolderManager.get, 0xDEADBEEF) # Non-existent folder - self.assertRaises(ObjectNotFound, FolderManager.get, uuid.uuid4()) + self.assertRaises(ObjectNotFound, FolderManager.get, 1234567890) @db_session def test_add_folder(self): @@ -114,12 +114,12 @@ class FolderManagerTestCase(unittest.TestCase): self.create_folders() with db_session: - # Delete invalid UUID + # Delete invalid Folder ID self.assertRaises(ValueError, FolderManager.delete, "invalid-uuid") self.assertEqual(db.Folder.select().count(), 3) # Delete non-existent folder - self.assertRaises(ObjectNotFound, FolderManager.delete, uuid.uuid4()) + self.assertRaises(ObjectNotFound, FolderManager.delete, 1234567890) self.assertEqual(db.Folder.select().count(), 3) # Delete non-root folder