From 4eb7386c9928398260dfcba36c219c1a4dba5871 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 22 Oct 2017 22:05:17 +0200 Subject: [PATCH] Playlists improvements They don't mess up the the track order anymore A same track can now be added more than once to a playlist Closes #61 --- schema/migration/20161030.sqlite.sql | 0 schema/migration/20171022.mysql.sql | 11 +++++ schema/migration/20171022.postgresql.sql | 11 +++++ schema/migration/20171022.sqlite.sql | 37 ++++++++++++++ schema/mysql.sql | 9 +--- schema/postgresql.sql | 9 +--- schema/sqlite.sql | 9 +--- supysonic/api/playlists.py | 25 ++++------ supysonic/db.py | 61 ++++++++++++++++++++---- supysonic/scanner.py | 4 +- 10 files changed, 128 insertions(+), 48 deletions(-) mode change 100755 => 100644 schema/migration/20161030.sqlite.sql create mode 100644 schema/migration/20171022.mysql.sql create mode 100644 schema/migration/20171022.postgresql.sql create mode 100644 schema/migration/20171022.sqlite.sql diff --git a/schema/migration/20161030.sqlite.sql b/schema/migration/20161030.sqlite.sql old mode 100755 new mode 100644 diff --git a/schema/migration/20171022.mysql.sql b/schema/migration/20171022.mysql.sql new file mode 100644 index 0000000..1f04e2e --- /dev/null +++ b/schema/migration/20171022.mysql.sql @@ -0,0 +1,11 @@ +START TRANSACTION; + +ALTER TABLE playlist ADD tracks TEXT; +UPDATE playlist SET tracks = ( + SELECT GROUP_CONCAT(track_id SEPARATOR ',') + FROM playlist_track + WHERE playlist_id = playlist.id); +DROP TABLE playlist_track; + +COMMIT; + diff --git a/schema/migration/20171022.postgresql.sql b/schema/migration/20171022.postgresql.sql new file mode 100644 index 0000000..0778b21 --- /dev/null +++ b/schema/migration/20171022.postgresql.sql @@ -0,0 +1,11 @@ +START TRANSACTION; + +ALTER TABLE playlist ADD tracks TEXT; +UPDATE playlist SET tracks = ( + SELECT array_to_string(array_agg(track_id), ',') + FROM playlist_track + WHERE playlist_id = playlist.id); +DROP TABLE playlist_track; + +COMMIT; + diff --git a/schema/migration/20171022.sqlite.sql b/schema/migration/20171022.sqlite.sql new file mode 100644 index 0000000..ef6f3bd --- /dev/null +++ b/schema/migration/20171022.sqlite.sql @@ -0,0 +1,37 @@ +-- PRAGMA foreign_keys = OFF; +BEGIN TRANSACTION; + +ALTER TABLE playlist RENAME TO playlist_old; +CREATE TABLE playlist ( + id CHAR(36) PRIMARY KEY, + user_id CHAR(36) NOT NULL REFERENCES user, + name VARCHAR(256) NOT NULL COLLATE NOCASE, + comment VARCHAR(256), + public BOOLEAN NOT NULL, + created DATETIME NOT NULL, + tracks TEXT +); + +CREATE TABLE TMP_playlist_tracks ( + id CHAR(36) PRIMARY KEY, + tracks TEXT +); + +INSERT INTO TMP_playlist_tracks(id, tracks) +SELECT id, GROUP_CONCAT(track_id, ',') +FROM playlist_old, playlist_track +WHERE id = playlist_id +GROUP BY id; + +INSERT INTO playlist(id, user_id, name, comment, public, created, tracks) +SELECT p.id, user_id, name, comment, public, created, tracks +FROM playlist_old p, TMP_playlist_tracks pt +WHERE p.id = pt.id; + +DROP TABLE TMP_playlist_tracks; +DROP TABLE playlist_track; +DROP TABLE playlist_old; + +COMMIT; +-- PRAGMA foreign_keys = ON; + diff --git a/schema/mysql.sql b/schema/mysql.sql index 7d33311..0021c42 100644 --- a/schema/mysql.sql +++ b/schema/mysql.sql @@ -117,12 +117,7 @@ CREATE TABLE playlist ( name VARCHAR(256) NOT NULL, comment VARCHAR(256), public BOOLEAN NOT NULL, - created DATETIME NOT NULL -) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; - -CREATE TABLE playlist_track ( - playlist_id CHAR(36) NOT NULL REFERENCES playlist, - track_id CHAR(36) NOT NULL REFERENCES track, - PRIMARY KEY(playlist_id, track_id) + created DATETIME NOT NULL, + tracks TEXT ) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; diff --git a/schema/postgresql.sql b/schema/postgresql.sql index 08e8f19..a325d55 100644 --- a/schema/postgresql.sql +++ b/schema/postgresql.sql @@ -117,12 +117,7 @@ CREATE TABLE playlist ( name VARCHAR(256) NOT NULL, comment VARCHAR(256), public BOOLEAN NOT NULL, - created TIMESTAMP NOT NULL -); - -CREATE TABLE playlist_track ( - playlist_id UUID NOT NULL REFERENCES playlist, - track_id UUID NOT NULL REFERENCES track, - PRIMARY KEY(playlist_id, track_id) + created TIMESTAMP NOT NULL, + tracks TEXT ); diff --git a/schema/sqlite.sql b/schema/sqlite.sql index f6ded12..9dbc93f 100644 --- a/schema/sqlite.sql +++ b/schema/sqlite.sql @@ -117,12 +117,7 @@ CREATE TABLE playlist ( name VARCHAR(256) NOT NULL COLLATE NOCASE, comment VARCHAR(256), public BOOLEAN NOT NULL, - created DATETIME NOT NULL -); - -CREATE TABLE playlist_track ( - playlist_id CHAR(36) NOT NULL REFERENCES playlist, - track_id CHAR(36) NOT NULL REFERENCES track, - PRIMARY KEY(playlist_id, track_id) + created DATETIME NOT NULL, + tracks TEXT ); diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py index 133a0e6..07d7099 100644 --- a/supysonic/api/playlists.py +++ b/supysonic/api/playlists.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013 Alban 'spl0k' Féron +# Copyright (C) 2013-2017 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -45,7 +45,7 @@ def show_playlist(): return res info = res.as_subsonic_playlist(request.user) - info['entry'] = [ t.as_subsonic_child(request.user, request.prefs) for t in res.tracks ] + info['entry'] = [ t.as_subsonic_child(request.user, request.prefs) for t in res.get_tracks() ] return request.formatter({ 'playlist': info }) @app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ]) @@ -56,7 +56,7 @@ def create_playlist(): songs = request.values.getlist('songId') try: playlist_id = uuid.UUID(playlist_id) if playlist_id else None - songs = set(map(uuid.UUID, songs)) + songs = map(uuid.UUID, songs) except: return request.error_formatter(0, 'Invalid parameter') @@ -68,7 +68,7 @@ def create_playlist(): if playlist.user_id != request.user.id and not request.user.admin: return request.error_formatter(50, "You're not allowed to modify a playlist that isn't yours") - playlist.tracks.clear() + playlist.clear() if name: playlist.name = name elif name: @@ -84,7 +84,7 @@ def create_playlist(): if not track: return request.error_formatter(70, 'Unknown song') - playlist.tracks.add(track) + playlist.add(track) store.commit() return request.formatter({}) @@ -98,7 +98,6 @@ def delete_playlist(): if res.user_id != request.user.id and not request.user.admin: return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours") - res.tracks.clear() store.remove(res) store.commit() return request.formatter({}) @@ -116,8 +115,8 @@ def update_playlist(): name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ]) to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ]) try: - to_add = set(map(uuid.UUID, to_add)) - to_remove = sorted(set(map(int, to_remove))) + to_add = map(uuid.UUID, to_add) + to_remove = map(int, to_remove) except: return request.error_formatter(0, 'Invalid parameter') @@ -128,19 +127,13 @@ def update_playlist(): if public: playlist.public = public in (True, 'True', 'true', 1, '1') - tracks = list(playlist.tracks) - for sid in to_add: track = store.get(Track, sid) if not track: return request.error_formatter(70, 'Unknown song') - if track not in playlist.tracks: - playlist.tracks.add(track) + playlist.add(track) - for idx in to_remove: - if idx < 0 or idx >= len(tracks): - return request.error_formatter(0, 'Index out of range') - playlist.tracks.remove(tracks[idx]) + playlist.remove_at_indexes(to_remove) store.commit() return request.formatter({}) diff --git a/supysonic/db.py b/supysonic/db.py index 4daad71..2dd3492 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013, 2014 Alban 'spl0k' Féron +# Copyright (C) 2013-2017 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -350,31 +350,74 @@ class Playlist(object): comment = Unicode() # nullable public = Bool(default = False) created = DateTime(default_factory = now) + tracks = Unicode() user = Reference(user_id, User.id) def as_subsonic_playlist(self, user): + tracks = self.get_tracks() info = { 'id': str(self.id), 'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name), 'owner': self.user.name, 'public': self.public, - 'songCount': self.tracks.count(), - 'duration': self.tracks.find().sum(Track.duration), + 'songCount': len(tracks), + 'duration': sum(map(lambda t: t.duration, tracks)), 'created': self.created.isoformat() } if self.comment: info['comment'] = self.comment return info -class PlaylistTrack(object): - __storm_table__ = 'playlist_track' - __storm_primary__ = 'playlist_id', 'track_id' + def get_tracks(self): + if not self.tracks: + return [] - playlist_id = UUID() - track_id = UUID() + tracks = [] + should_fix = False + store = Store.of(self) -Playlist.tracks = ReferenceSet(Playlist.id, PlaylistTrack.playlist_id, PlaylistTrack.track_id, Track.id) + for t in self.tracks.split(','): + try: + tid = uuid.UUID(t) + track = store.get(Track, tid) + if track: + tracks.append(track) + else: + should_fix = True + except: + should_fix = True + + if should_fix: + self.tracks = ','.join(map(lambda t: str(t.id), tracks)) + store.commit() + + return tracks + + def clear(self): + self.tracks = "" + + def add(self, track): + if isinstance(track, uuid.UUID): + tid = track + elif isinstance(track, Track): + tid = track.id + elif isinstance(track, basestring): + tid = uuid.UUID(track) + + if self.tracks and len(self.tracks) > 0: + self.tracks = "{},{}".format(self.tracks, tid) + else: + self.tracks = str(tid) + + def remove_at_indexes(self, indexes): + tracks = self.tracks.split(',') + for i in indexes: + if i < 0 or i >= len(tracks): + continue + tracks[i] = None + + self.tracks = ','.join(t for t in tracks if t) def get_store(database_uri): database = create_database(database_uri) diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 30d85f3..7af7a33 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -26,7 +26,7 @@ from storm.expr import ComparableExpr, compile, Like from storm.exceptions import NotSupportedError from supysonic import config -from supysonic.db import Folder, Artist, Album, Track, User, PlaylistTrack +from supysonic.db import Folder, Artist, Album, Track, User from supysonic.db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from supysonic.db import RatingFolder, RatingTrack @@ -201,7 +201,7 @@ class Scanner: self.__store.find(StarredTrack, StarredTrack.starred_id == tr.id).remove() self.__store.find(RatingTrack, RatingTrack.rated_id == tr.id).remove() - self.__store.find(PlaylistTrack, PlaylistTrack.track_id == tr.id).remove() + # Playlist autofix themselves self.__store.find(User, User.last_play_id == tr.id).set(last_play_id = None) self.__folders_to_check.add(tr.folder)