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

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
This commit is contained in:
spl0k 2017-10-22 22:05:17 +02:00
parent 4d3809a835
commit 4eb7386c99
10 changed files with 128 additions and 48 deletions

0
schema/migration/20161030.sqlite.sql Executable file → Normal file
View File

View File

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

View File

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

View File

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

View File

@ -117,12 +117,7 @@ CREATE TABLE playlist (
name VARCHAR(256) NOT NULL, name VARCHAR(256) NOT NULL,
comment VARCHAR(256), comment VARCHAR(256),
public BOOLEAN NOT NULL, public BOOLEAN NOT NULL,
created DATETIME NOT NULL created DATETIME NOT NULL,
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; tracks TEXT
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)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; ) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

View File

@ -117,12 +117,7 @@ CREATE TABLE playlist (
name VARCHAR(256) NOT NULL, name VARCHAR(256) NOT NULL,
comment VARCHAR(256), comment VARCHAR(256),
public BOOLEAN NOT NULL, public BOOLEAN NOT NULL,
created TIMESTAMP NOT NULL created TIMESTAMP NOT NULL,
); tracks TEXT
CREATE TABLE playlist_track (
playlist_id UUID NOT NULL REFERENCES playlist,
track_id UUID NOT NULL REFERENCES track,
PRIMARY KEY(playlist_id, track_id)
); );

View File

@ -117,12 +117,7 @@ CREATE TABLE playlist (
name VARCHAR(256) NOT NULL COLLATE NOCASE, name VARCHAR(256) NOT NULL COLLATE NOCASE,
comment VARCHAR(256), comment VARCHAR(256),
public BOOLEAN NOT NULL, public BOOLEAN NOT NULL,
created DATETIME NOT NULL created DATETIME NOT NULL,
); tracks TEXT
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)
); );

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # Supysonic is a Python implementation of the Subsonic server API.
# Copyright (C) 2013 Alban 'spl0k' Féron # Copyright (C) 2013-2017 Alban 'spl0k' Féron
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -45,7 +45,7 @@ def show_playlist():
return res return res
info = res.as_subsonic_playlist(request.user) 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 }) return request.formatter({ 'playlist': info })
@app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ])
@ -56,7 +56,7 @@ def create_playlist():
songs = request.values.getlist('songId') songs = request.values.getlist('songId')
try: try:
playlist_id = uuid.UUID(playlist_id) if playlist_id else None playlist_id = uuid.UUID(playlist_id) if playlist_id else None
songs = set(map(uuid.UUID, songs)) songs = map(uuid.UUID, songs)
except: except:
return request.error_formatter(0, 'Invalid parameter') 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: 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") return request.error_formatter(50, "You're not allowed to modify a playlist that isn't yours")
playlist.tracks.clear() playlist.clear()
if name: if name:
playlist.name = name playlist.name = name
elif name: elif name:
@ -84,7 +84,7 @@ def create_playlist():
if not track: if not track:
return request.error_formatter(70, 'Unknown song') return request.error_formatter(70, 'Unknown song')
playlist.tracks.add(track) playlist.add(track)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@ -98,7 +98,6 @@ def delete_playlist():
if res.user_id != request.user.id and not request.user.admin: 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") return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours")
res.tracks.clear()
store.remove(res) store.remove(res)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@ -116,8 +115,8 @@ def update_playlist():
name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ]) name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ])
to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ]) to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ])
try: try:
to_add = set(map(uuid.UUID, to_add)) to_add = map(uuid.UUID, to_add)
to_remove = sorted(set(map(int, to_remove))) to_remove = map(int, to_remove)
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
@ -128,19 +127,13 @@ def update_playlist():
if public: if public:
playlist.public = public in (True, 'True', 'true', 1, '1') playlist.public = public in (True, 'True', 'true', 1, '1')
tracks = list(playlist.tracks)
for sid in to_add: for sid in to_add:
track = store.get(Track, sid) track = store.get(Track, sid)
if not track: if not track:
return request.error_formatter(70, 'Unknown song') return request.error_formatter(70, 'Unknown song')
if track not in playlist.tracks: playlist.add(track)
playlist.tracks.add(track)
for idx in to_remove: playlist.remove_at_indexes(to_remove)
if idx < 0 or idx >= len(tracks):
return request.error_formatter(0, 'Index out of range')
playlist.tracks.remove(tracks[idx])
store.commit() store.commit()
return request.formatter({}) return request.formatter({})

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # Supysonic is a Python implementation of the Subsonic server API.
# Copyright (C) 2013, 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 # 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 # it under the terms of the GNU Affero General Public License as published by
@ -350,31 +350,74 @@ class Playlist(object):
comment = Unicode() # nullable comment = Unicode() # nullable
public = Bool(default = False) public = Bool(default = False)
created = DateTime(default_factory = now) created = DateTime(default_factory = now)
tracks = Unicode()
user = Reference(user_id, User.id) user = Reference(user_id, User.id)
def as_subsonic_playlist(self, user): def as_subsonic_playlist(self, user):
tracks = self.get_tracks()
info = { info = {
'id': str(self.id), 'id': str(self.id),
'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name), 'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
'owner': self.user.name, 'owner': self.user.name,
'public': self.public, 'public': self.public,
'songCount': self.tracks.count(), 'songCount': len(tracks),
'duration': self.tracks.find().sum(Track.duration), 'duration': sum(map(lambda t: t.duration, tracks)),
'created': self.created.isoformat() 'created': self.created.isoformat()
} }
if self.comment: if self.comment:
info['comment'] = self.comment info['comment'] = self.comment
return info return info
class PlaylistTrack(object): def get_tracks(self):
__storm_table__ = 'playlist_track' if not self.tracks:
__storm_primary__ = 'playlist_id', 'track_id' return []
playlist_id = UUID() tracks = []
track_id = UUID() 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): def get_store(database_uri):
database = create_database(database_uri) database = create_database(database_uri)

View File

@ -26,7 +26,7 @@ from storm.expr import ComparableExpr, compile, Like
from storm.exceptions import NotSupportedError from storm.exceptions import NotSupportedError
from supysonic import config 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 StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from supysonic.db import RatingFolder, RatingTrack 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(StarredTrack, StarredTrack.starred_id == tr.id).remove()
self.__store.find(RatingTrack, RatingTrack.rated_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.__store.find(User, User.last_play_id == tr.id).set(last_play_id = None)
self.__folders_to_check.add(tr.folder) self.__folders_to_check.add(tr.folder)