2012-10-13 09:29:48 +00:00
|
|
|
# coding: utf-8
|
|
|
|
|
2014-03-02 17:31:32 +00:00
|
|
|
# This file is part of Supysonic.
|
|
|
|
#
|
|
|
|
# Supysonic is a Python implementation of the Subsonic server API.
|
|
|
|
# Copyright (C) 2013, 2014 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
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
from storm.properties import *
|
2014-03-09 18:12:11 +00:00
|
|
|
from storm.references import Reference, ReferenceSet
|
|
|
|
from storm.database import create_database
|
|
|
|
from storm.store import Store
|
|
|
|
from storm.variables import Variable
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2013-06-14 16:46:01 +00:00
|
|
|
import uuid, datetime, time
|
2012-10-20 16:05:37 +00:00
|
|
|
import os.path
|
2012-10-13 09:29:48 +00:00
|
|
|
|
2012-12-01 18:52:41 +00:00
|
|
|
def now():
|
|
|
|
return datetime.datetime.now().replace(microsecond = 0)
|
|
|
|
|
2014-03-09 18:12:11 +00:00
|
|
|
class UnicodeOrStrVariable(Variable):
|
|
|
|
__slots__ = ()
|
|
|
|
|
|
|
|
def parse_set(self, value, from_db):
|
|
|
|
if isinstance(value, unicode):
|
|
|
|
return value
|
|
|
|
elif isinstance(value, str):
|
|
|
|
return unicode(value)
|
|
|
|
raise TypeError("Expected unicode, found %r: %r" % (type(value), value))
|
|
|
|
|
|
|
|
Unicode.variable_class = UnicodeOrStrVariable
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class Folder(object):
|
|
|
|
__storm_table__ = 'folder'
|
2012-10-13 09:29:48 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
root = Bool(default = False)
|
|
|
|
name = Unicode()
|
|
|
|
path = Unicode() # unique
|
|
|
|
created = DateTime(default_factory = now)
|
|
|
|
has_cover_art = Bool(default = False)
|
|
|
|
last_scan = Int(default = 0)
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
parent_id = UUID() # nullable
|
|
|
|
parent = Reference(parent_id, id)
|
|
|
|
children = ReferenceSet(id, parent_id)
|
2012-10-21 14:18:35 +00:00
|
|
|
|
2013-06-13 16:44:56 +00:00
|
|
|
def as_subsonic_child(self, user):
|
2012-11-10 23:01:52 +00:00
|
|
|
info = {
|
|
|
|
'id': str(self.id),
|
|
|
|
'isDir': True,
|
|
|
|
'title': self.name,
|
2013-06-12 19:29:42 +00:00
|
|
|
'album': self.name,
|
2012-12-01 18:52:41 +00:00
|
|
|
'created': self.created.isoformat()
|
2012-11-10 23:01:52 +00:00
|
|
|
}
|
|
|
|
if not self.root:
|
|
|
|
info['parent'] = str(self.parent_id)
|
|
|
|
info['artist'] = self.parent.name
|
2012-11-11 20:39:26 +00:00
|
|
|
if self.has_cover_art:
|
|
|
|
info['coverArt'] = str(self.id)
|
2012-11-10 23:01:52 +00:00
|
|
|
|
2013-06-14 10:25:15 +00:00
|
|
|
starred = StarredFolder.query.get((user.id, self.id))
|
2013-06-13 16:44:56 +00:00
|
|
|
if starred:
|
|
|
|
info['starred'] = starred.date.isoformat()
|
|
|
|
|
2013-06-14 10:25:15 +00:00
|
|
|
rating = RatingFolder.query.get((user.id, self.id))
|
|
|
|
if rating:
|
|
|
|
info['userRating'] = rating.rating
|
|
|
|
avgRating = RatingFolder.query.filter(RatingFolder.rated_id == self.id).value(func.avg(RatingFolder.rating))
|
|
|
|
if avgRating:
|
|
|
|
info['averageRating'] = avgRating
|
|
|
|
|
2012-11-10 23:01:52 +00:00
|
|
|
return info
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class Artist(object):
|
|
|
|
__storm_table__ = 'artist'
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
name = Unicode() # unique
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2013-06-13 16:44:56 +00:00
|
|
|
def as_subsonic_artist(self, user):
|
|
|
|
info = {
|
2012-12-01 21:58:17 +00:00
|
|
|
'id': str(self.id),
|
|
|
|
'name': self.name,
|
|
|
|
# coverArt
|
|
|
|
'albumCount': len(self.albums)
|
|
|
|
}
|
|
|
|
|
2013-06-14 10:25:15 +00:00
|
|
|
starred = StarredArtist.query.get((user.id, self.id))
|
2013-06-13 16:44:56 +00:00
|
|
|
if starred:
|
|
|
|
info['starred'] = starred.date.isoformat()
|
|
|
|
|
|
|
|
return info
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class Album(object):
|
|
|
|
__storm_table__ = 'album'
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
name = Unicode()
|
|
|
|
artist_id = UUID()
|
|
|
|
artist = Reference(artist_id, Artist.id)
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2013-06-13 16:44:56 +00:00
|
|
|
def as_subsonic_album(self, user):
|
2012-12-01 21:58:17 +00:00
|
|
|
info = {
|
|
|
|
'id': str(self.id),
|
|
|
|
'name': self.name,
|
|
|
|
'artist': self.artist.name,
|
|
|
|
'artistId': str(self.artist_id),
|
|
|
|
'songCount': len(self.tracks),
|
|
|
|
'duration': sum(map(lambda t: t.duration, self.tracks)),
|
|
|
|
'created': min(map(lambda t: t.created, self.tracks)).isoformat()
|
|
|
|
}
|
|
|
|
if self.tracks[0].folder.has_cover_art:
|
|
|
|
info['coverArt'] = str(self.tracks[0].folder_id)
|
|
|
|
|
2013-06-14 10:25:15 +00:00
|
|
|
starred = StarredAlbum.query.get((user.id, self.id))
|
2013-06-13 16:44:56 +00:00
|
|
|
if starred:
|
|
|
|
info['starred'] = starred.date.isoformat()
|
|
|
|
|
2012-12-01 21:58:17 +00:00
|
|
|
return info
|
|
|
|
|
|
|
|
def sort_key(self):
|
|
|
|
year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
|
|
|
|
return '%i%s' % (year, self.name.lower())
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
Artist.albums = ReferenceSet(Artist.id, Album.artist_id)
|
2012-10-13 12:53:09 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class Track(object):
|
|
|
|
__storm_table__ = 'track'
|
2012-10-13 09:29:48 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
disc = Int()
|
|
|
|
number = Int()
|
|
|
|
title = Unicode()
|
|
|
|
year = Int() # nullable
|
|
|
|
genre = Unicode() # nullable
|
|
|
|
duration = Int()
|
|
|
|
album_id = UUID()
|
|
|
|
album = Reference(album_id, Album.id)
|
|
|
|
bitrate = Int()
|
2012-12-01 18:52:41 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
path = Unicode() # unique
|
|
|
|
content_type = Unicode()
|
|
|
|
created = DateTime(default_factory = now)
|
|
|
|
last_modification = Int()
|
2013-06-07 18:35:21 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
play_count = Int(default = 0)
|
|
|
|
last_play = DateTime() # nullable
|
|
|
|
|
|
|
|
root_folder_id = UUID()
|
|
|
|
root_folder = Reference(root_folder_id, Folder.id)
|
|
|
|
folder_id = UUID()
|
|
|
|
folder = Reference(folder_id, Folder.id)
|
2012-10-14 11:07:02 +00:00
|
|
|
|
2013-06-13 16:44:56 +00:00
|
|
|
def as_subsonic_child(self, user):
|
2012-10-20 16:05:37 +00:00
|
|
|
info = {
|
|
|
|
'id': str(self.id),
|
2012-10-21 14:18:35 +00:00
|
|
|
'parent': str(self.folder.id),
|
2012-10-20 16:05:37 +00:00
|
|
|
'isDir': False,
|
|
|
|
'title': self.title,
|
|
|
|
'album': self.album.name,
|
|
|
|
'artist': self.album.artist.name,
|
|
|
|
'track': self.number,
|
|
|
|
'size': os.path.getsize(self.path),
|
2013-10-14 16:36:45 +00:00
|
|
|
'contentType': self.content_type,
|
|
|
|
'suffix': self.suffix(),
|
2012-10-20 16:05:37 +00:00
|
|
|
'duration': self.duration,
|
|
|
|
'bitRate': self.bitrate,
|
2012-10-21 14:18:35 +00:00
|
|
|
'path': self.path[len(self.root_folder.path) + 1:],
|
2012-10-20 16:05:37 +00:00
|
|
|
'isVideo': False,
|
|
|
|
'discNumber': self.disc,
|
2012-12-01 18:52:41 +00:00
|
|
|
'created': self.created.isoformat(),
|
2012-10-20 16:05:37 +00:00
|
|
|
'albumId': str(self.album.id),
|
|
|
|
'artistId': str(self.album.artist.id),
|
|
|
|
'type': 'music'
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.year:
|
|
|
|
info['year'] = self.year
|
|
|
|
if self.genre:
|
|
|
|
info['genre'] = self.genre
|
2012-11-11 20:39:26 +00:00
|
|
|
if self.folder.has_cover_art:
|
|
|
|
info['coverArt'] = str(self.folder_id)
|
2012-10-20 16:05:37 +00:00
|
|
|
|
2013-06-14 10:25:15 +00:00
|
|
|
starred = StarredTrack.query.get((user.id, self.id))
|
2013-06-13 16:44:56 +00:00
|
|
|
if starred:
|
|
|
|
info['starred'] = starred.date.isoformat()
|
|
|
|
|
2013-06-14 10:25:15 +00:00
|
|
|
rating = RatingTrack.query.get((user.id, self.id))
|
|
|
|
if rating:
|
|
|
|
info['userRating'] = rating.rating
|
|
|
|
avgRating = RatingTrack.query.filter(RatingTrack.rated_id == self.id).value(func.avg(RatingTrack.rating))
|
|
|
|
if avgRating:
|
|
|
|
info['averageRating'] = avgRating
|
|
|
|
|
2012-10-20 16:05:37 +00:00
|
|
|
# transcodedContentType
|
|
|
|
# transcodedSuffix
|
|
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
def duration_str(self):
|
|
|
|
ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60)
|
|
|
|
if self.duration >= 3600:
|
|
|
|
ret = '%02i:%s' % (self.duration / 3600, ret)
|
|
|
|
return ret
|
|
|
|
|
2013-10-14 16:36:45 +00:00
|
|
|
def suffix(self):
|
|
|
|
return os.path.splitext(self.path)[1][1:].lower()
|
|
|
|
|
2012-11-10 23:01:52 +00:00
|
|
|
def sort_key(self):
|
2012-12-01 21:58:17 +00:00
|
|
|
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
|
2012-11-10 23:01:52 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
Folder.tracks = ReferenceSet(Folder.id, Track.folder_id)
|
|
|
|
Album.tracks = ReferenceSet(Album.id, Track.album_id)
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class User(object):
|
|
|
|
__storm_table__ = 'user'
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
name = Unicode() # unique
|
|
|
|
mail = Unicode()
|
|
|
|
password = Unicode()
|
|
|
|
salt = Unicode()
|
|
|
|
admin = Bool(default = False)
|
|
|
|
lastfm_session = Unicode() # nullable
|
|
|
|
lastfm_status = Bool(default = True) # True: ok/unlinked, False: invalid session
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
last_play_id = UUID() # nullable
|
|
|
|
last_play = Reference(last_play_id, Track.id)
|
|
|
|
last_play_date = DateTime() # nullable
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
def as_subsonic_user(self):
|
|
|
|
return {
|
|
|
|
'username': self.name,
|
|
|
|
'email': self.mail,
|
|
|
|
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status,
|
|
|
|
'adminRole': self.admin,
|
|
|
|
'settingsRole': True,
|
|
|
|
'downloadRole': True,
|
|
|
|
'uploadRole': False,
|
|
|
|
'playlistRole': True,
|
|
|
|
'coverArtRole': False,
|
|
|
|
'commentRole': False,
|
|
|
|
'podcastRole': False,
|
|
|
|
'streamRole': True,
|
|
|
|
'jukeboxRole': False,
|
|
|
|
'shareRole': False
|
|
|
|
}
|
|
|
|
|
|
|
|
class ClientPrefs(object):
|
|
|
|
__storm_table__ = 'client_prefs'
|
2014-03-16 17:51:19 +00:00
|
|
|
__storm_primary__ = 'user_id', 'client_name'
|
2014-03-06 19:02:35 +00:00
|
|
|
|
|
|
|
user_id = UUID()
|
|
|
|
client_name = Unicode()
|
|
|
|
format = Unicode() # nullable
|
|
|
|
bitrate = Int() # nullable
|
|
|
|
|
|
|
|
class BaseStarred(object):
|
2014-03-16 17:51:19 +00:00
|
|
|
__storm_primary__ = 'user_id', 'starred_id'
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
user_id = UUID()
|
|
|
|
starred_id = UUID()
|
|
|
|
date = DateTime(default_factory = now)
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
user = Reference(user_id, User.id)
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class StarredFolder(BaseStarred):
|
|
|
|
__storm_table__ = 'starred_folder'
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
starred = Reference(BaseStarred.starred_id, Folder.id)
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class StarredArtist(BaseStarred):
|
|
|
|
__storm_table__ = 'starred_artist'
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
starred = Reference(BaseStarred.starred_id, Artist.id)
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class StarredAlbum(BaseStarred):
|
|
|
|
__storm_table__ = 'starred_album'
|
2013-06-12 19:29:42 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
starred = Reference(BaseStarred.starred_id, Album.id)
|
2013-06-14 10:25:15 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class StarredTrack(BaseStarred):
|
|
|
|
__storm_table__ = 'starred_track'
|
2013-06-14 10:25:15 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
starred = Reference(BaseStarred.starred_id, Track.id)
|
2013-06-14 10:25:15 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class BaseRating(object):
|
2014-03-16 17:51:19 +00:00
|
|
|
__storm_primary__ = 'user_id', 'rated_id'
|
2013-06-14 10:25:15 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
user_id = UUID()
|
|
|
|
rated_id = UUID()
|
|
|
|
rating = Int()
|
2013-06-14 10:25:15 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
user = Reference(user_id, User.id)
|
2013-06-14 10:25:15 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class RatingFolder(BaseRating):
|
|
|
|
__storm_table__ = 'rating_folder'
|
2013-06-14 16:46:01 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
rated = Reference(BaseRating.rated_id, Folder.id)
|
2013-06-14 16:46:01 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class RatingTrack(BaseRating):
|
|
|
|
__storm_table__ = 'rating_track'
|
|
|
|
|
|
|
|
rated = Reference(BaseRating.rated_id, Track.id)
|
|
|
|
|
|
|
|
class ChatMessage(object):
|
|
|
|
__storm_table__ = 'chat_message'
|
|
|
|
|
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
user_id = UUID()
|
|
|
|
time = Int(default_factory = lambda: int(time.time()))
|
|
|
|
message = Unicode()
|
|
|
|
|
|
|
|
user = Reference(user_id, User.id)
|
2013-06-14 16:46:01 +00:00
|
|
|
|
|
|
|
def responsize(self):
|
|
|
|
return {
|
|
|
|
'username': self.user.name,
|
|
|
|
'time': self.time * 1000,
|
|
|
|
'message': self.message
|
|
|
|
}
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class Playlist(object):
|
|
|
|
__storm_table__ = 'playlist'
|
2013-06-25 20:07:49 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
id = UUID(primary = True, default_factory = uuid.uuid4)
|
|
|
|
user_id = UUID()
|
|
|
|
name = Unicode()
|
|
|
|
comment = Unicode() # nullable
|
|
|
|
public = Bool(default = False)
|
|
|
|
created = DateTime(default_factory = now)
|
2013-06-25 20:07:49 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
user = Reference(user_id, User.id)
|
2013-06-25 20:07:49 +00:00
|
|
|
|
2013-06-28 10:39:46 +00:00
|
|
|
def as_subsonic_playlist(self, user):
|
2013-06-25 20:07:49 +00:00
|
|
|
info = {
|
|
|
|
'id': str(self.id),
|
2013-06-28 10:39:46 +00:00
|
|
|
'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
|
2013-06-25 20:07:49 +00:00
|
|
|
'owner': self.user.name,
|
|
|
|
'public': self.public,
|
|
|
|
'songCount': len(self.tracks),
|
|
|
|
'duration': sum(map(lambda t: t.duration, self.tracks)),
|
|
|
|
'created': self.created.isoformat()
|
|
|
|
}
|
|
|
|
if self.comment:
|
|
|
|
info['comment'] = self.comment
|
|
|
|
return info
|
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
class PlaylistTrack(object):
|
|
|
|
__storm_table__ = 'playlist_track'
|
2014-03-16 17:51:19 +00:00
|
|
|
__storm_primary__ = 'playlist_id', 'track_id'
|
2014-03-06 19:02:35 +00:00
|
|
|
|
|
|
|
playlist_id = UUID()
|
|
|
|
track_id = UUID()
|
2012-10-13 10:29:37 +00:00
|
|
|
|
2014-03-06 19:02:35 +00:00
|
|
|
Playlist.tracks = ReferenceSet(Playlist.id, PlaylistTrack.playlist_id, PlaylistTrack.track_id, Track.id)
|
2012-10-13 09:29:48 +00:00
|
|
|
|
2014-03-09 18:12:11 +00:00
|
|
|
def get_store(database_uri):
|
|
|
|
database = create_database(database_uri)
|
|
|
|
store = Store(database)
|
|
|
|
return store
|
|
|
|
|