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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic.
#
# Supysonic is a Python implementation of the Subsonic server API.
# Copyright (C) 2013 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({})

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic.
#
# Supysonic is a Python implementation of the Subsonic server API.
# Copyright (C) 2013, 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)

View File

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