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:
parent
4d3809a835
commit
4eb7386c99
0
schema/migration/20161030.sqlite.sql
Executable file → Normal file
0
schema/migration/20161030.sqlite.sql
Executable file → Normal file
11
schema/migration/20171022.mysql.sql
Normal file
11
schema/migration/20171022.mysql.sql
Normal 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;
|
||||||
|
|
11
schema/migration/20171022.postgresql.sql
Normal file
11
schema/migration/20171022.postgresql.sql
Normal 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;
|
||||||
|
|
37
schema/migration/20171022.sqlite.sql
Normal file
37
schema/migration/20171022.sqlite.sql
Normal 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;
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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({})
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user