1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-23 01:16:18 +00:00
supysonic/db.py

419 lines
12 KiB
Python
Raw Normal View History

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/>.
2012-10-13 09:29:48 +00:00
import config
2012-10-13 12:53:09 +00:00
2013-06-25 20:07:49 +00:00
from sqlalchemy import create_engine, Table, Column, ForeignKey, func
from sqlalchemy import Integer, String, Boolean, DateTime
2012-10-13 12:53:09 +00:00
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
2012-10-13 09:29:48 +00:00
from sqlalchemy.ext.declarative import declarative_base
2013-07-15 18:30:33 +00:00
from sqlalchemy.types import TypeDecorator, BINARY
2013-07-16 09:30:19 +00:00
from sqlalchemy.dialects.postgresql import UUID as pgUUID
2012-10-13 12:53:09 +00:00
import uuid, datetime, time
import os.path
2012-10-13 09:29:48 +00:00
2012-10-13 12:53:09 +00:00
class UUID(TypeDecorator):
2013-07-16 09:30:19 +00:00
"""Platform-somewhat-independent UUID type
Uses Postgresql's UUID type, otherwise uses BINARY(16),
should be more efficient than a CHAR(32).
Mix of http://stackoverflow.com/a/812363
and http://www.sqlalchemy.org/docs/core/types.html#backend-agnostic-guid-type
"""
2012-10-13 09:29:48 +00:00
impl = BINARY
2012-10-13 12:53:09 +00:00
2013-07-16 09:30:19 +00:00
def load_dialect_impl(self, dialect):
if dialect.name == 'postgresql':
return dialect.type_descriptor(pgUUID())
else:
return dialect.type_descriptor(BINARY(16))
2012-10-13 09:29:48 +00:00
2013-07-16 09:30:19 +00:00
def process_bind_param(self, value, dialect):
2012-10-13 09:29:48 +00:00
if value and isinstance(value, uuid.UUID):
2013-07-16 09:30:19 +00:00
if dialect.name == 'postgresql':
return str(value)
2012-10-13 09:29:48 +00:00
return value.bytes
if value and not isinstance(value, uuid.UUID):
raise ValueError, 'value %s is not a valid uuid.UUID' % value
return None
2013-07-16 09:30:19 +00:00
def process_result_value(self, value, dialect):
2012-10-13 09:29:48 +00:00
if value:
2013-07-16 09:30:19 +00:00
if dialect.name == 'postgresql':
return uuid.UUID(value)
2012-10-13 09:29:48 +00:00
return uuid.UUID(bytes = value)
return None
def is_mutable(self):
return False
@staticmethod
def gen_id_column():
return Column(UUID, primary_key = True, default = uuid.uuid4)
def now():
return datetime.datetime.now().replace(microsecond = 0)
engine = create_engine(config.get('base', 'database_uri'), convert_unicode = True)
session = scoped_session(sessionmaker(autocommit = False, autoflush = False, bind = engine))
2012-10-13 09:29:48 +00:00
Base = declarative_base()
Base.query = session.query_property()
2012-10-13 09:29:48 +00:00
class User(Base):
2012-10-13 12:53:09 +00:00
__tablename__ = 'user'
2012-10-13 09:29:48 +00:00
id = UUID.gen_id_column()
2013-07-15 18:30:33 +00:00
name = Column(String(64), unique = True)
mail = Column(String(255))
2012-10-13 09:29:48 +00:00
password = Column(String(40))
salt = Column(String(6))
admin = Column(Boolean, default = False)
lastfm_session = Column(String(32), nullable = True)
lastfm_status = Column(Boolean, default = True) # True: ok/unlinked, False: invalid session
2012-10-13 09:29:48 +00:00
last_play_id = Column(UUID, ForeignKey('track.id'), nullable = True)
last_play = relationship('Track')
last_play_date = Column(DateTime, nullable = True)
2013-06-18 14:12: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,
2013-06-25 20:18:54 +00:00
'playlistRole': True,
2013-06-18 14:12:35 +00:00
'coverArtRole': False,
'commentRole': False,
'podcastRole': False,
'streamRole': True,
'jukeboxRole': False,
'shareRole': False
}
class ClientPrefs(Base):
__tablename__ = 'client_prefs'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
client_name = Column(String(32), nullable = False, primary_key = True)
format = Column(String(8), nullable = True)
bitrate = Column(Integer, nullable = True)
2012-10-21 14:18:35 +00:00
class Folder(Base):
2012-10-13 12:53:09 +00:00
__tablename__ = 'folder'
2012-10-13 09:29:48 +00:00
id = UUID.gen_id_column()
2012-10-21 14:18:35 +00:00
root = Column(Boolean, default = False)
name = Column(String(255))
2013-07-15 18:30:33 +00:00
path = Column(String(4096)) # should be unique, but mysql don't like such large columns
created = Column(DateTime, default = now)
2012-11-11 20:39:26 +00:00
has_cover_art = Column(Boolean, default = False)
last_scan = Column(Integer, default = 0)
2012-10-13 12:53:09 +00:00
2012-10-21 14:18:35 +00:00
parent_id = Column(UUID, ForeignKey('folder.id'), nullable = True)
children = relationship('Folder', backref = backref('parent', remote_side = [ id ]))
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,
'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
2012-10-13 12:53:09 +00:00
class Artist(Base):
__tablename__ = 'artist'
id = UUID.gen_id_column()
name = Column(String(255), unique = True)
albums = relationship('Album', backref = 'artist')
2012-10-13 12:53:09 +00:00
2013-06-13 16:44:56 +00:00
def as_subsonic_artist(self, user):
info = {
'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
2012-10-13 12:53:09 +00:00
class Album(Base):
__tablename__ = 'album'
id = UUID.gen_id_column()
name = Column(String(255))
2012-10-13 12:53:09 +00:00
artist_id = Column(UUID, ForeignKey('artist.id'))
tracks = relationship('Track', backref = 'album')
2012-10-13 12:53:09 +00:00
2013-06-13 16:44:56 +00:00
def as_subsonic_album(self, user):
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()
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())
2012-10-13 12:53:09 +00:00
class Track(Base):
__tablename__ = 'track'
id = UUID.gen_id_column()
disc = Column(Integer)
number = Column(Integer)
title = Column(String(255))
year = Column(Integer, nullable = True)
genre = Column(String(255), nullable = True)
duration = Column(Integer)
2012-10-13 12:53:09 +00:00
album_id = Column(UUID, ForeignKey('album.id'))
bitrate = Column(Integer)
2012-10-13 09:29:48 +00:00
2013-07-15 18:30:33 +00:00
path = Column(String(4096)) # should be unique, but mysql don't like such large columns
content_type = Column(String(32))
created = Column(DateTime, default = now)
last_modification = Column(Integer)
play_count = Column(Integer, default = 0)
last_play = Column(DateTime, nullable = True)
2012-10-21 14:18:35 +00:00
root_folder_id = Column(UUID, ForeignKey('folder.id'))
root_folder = relationship('Folder', primaryjoin = Folder.id == root_folder_id)
2012-10-14 11:07:02 +00:00
folder_id = Column(UUID, ForeignKey('folder.id'))
2012-10-21 14:18:35 +00:00
folder = relationship('Folder', primaryjoin = Folder.id == folder_id, backref = 'tracks')
2012-10-14 11:07:02 +00:00
2013-06-13 16:44:56 +00:00
def as_subsonic_child(self, user):
info = {
'id': str(self.id),
2012-10-21 14:18:35 +00:00
'parent': str(self.folder.id),
'isDir': False,
'title': self.title,
'album': self.album.name,
'artist': self.album.artist.name,
'track': self.number,
'size': os.path.getsize(self.path),
'contentType': self.content_type,
'suffix': self.suffix(),
'duration': self.duration,
'bitRate': self.bitrate,
2012-10-21 14:18:35 +00:00
'path': self.path[len(self.root_folder.path) + 1:],
'isVideo': False,
'discNumber': self.disc,
'created': self.created.isoformat(),
'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)
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
# 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
def suffix(self):
return os.path.splitext(self.path)[1][1:].lower()
2012-11-10 23:01:52 +00:00
def sort_key(self):
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
2013-06-12 19:29:42 +00:00
class StarredFolder(Base):
__tablename__ = 'starred_folder'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
date = Column(DateTime, default = now)
user = relationship('User')
starred = relationship('Folder')
class StarredArtist(Base):
__tablename__ = 'starred_artist'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('artist.id'), primary_key = True)
date = Column(DateTime, default = now)
user = relationship('User')
starred = relationship('Artist')
class StarredAlbum(Base):
__tablename__ = 'starred_album'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('album.id'), primary_key = True)
date = Column(DateTime, default = now)
user = relationship('User')
starred = relationship('Album')
class StarredTrack(Base):
__tablename__ = 'starred_track'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('track.id'), primary_key = True)
date = Column(DateTime, default = now)
user = relationship('User')
starred = relationship('Track')
2013-06-14 10:25:15 +00:00
class RatingFolder(Base):
__tablename__ = 'rating_folder'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
rated_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
rating = Column(Integer)
user = relationship('User')
rated = relationship('Folder')
class RatingTrack(Base):
__tablename__ = 'rating_track'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
rated_id = Column(UUID, ForeignKey('track.id'), primary_key = True)
rating = Column(Integer)
user = relationship('User')
rated = relationship('Track')
class ChatMessage(Base):
__tablename__ = 'chat_message'
id = UUID.gen_id_column()
user_id = Column(UUID, ForeignKey('user.id'))
time = Column(Integer, default = lambda: int(time.time()))
2013-07-15 18:30:33 +00:00
message = Column(String(512))
user = relationship('User')
def responsize(self):
return {
'username': self.user.name,
'time': self.time * 1000,
'message': self.message
}
2013-06-25 20:07:49 +00:00
playlist_track_assoc = Table('playlist_track', Base.metadata,
Column('playlist_id', UUID, ForeignKey('playlist.id')),
Column('track_id', UUID, ForeignKey('track.id'))
)
class Playlist(Base):
__tablename__ = 'playlist'
id = UUID.gen_id_column()
user_id = Column(UUID, ForeignKey('user.id'))
name = Column(String(255))
comment = Column(String(255), nullable = True)
2013-06-25 20:07:49 +00:00
public = Column(Boolean, default = False)
created = Column(DateTime, default = now)
user = relationship('User')
tracks = relationship('Track', secondary = playlist_track_assoc)
def as_subsonic_playlist(self, user):
2013-06-25 20:07:49 +00:00
info = {
'id': str(self.id),
'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
2012-10-13 09:29:48 +00:00
def init_db():
Base.metadata.create_all(bind = engine)
def recreate_db():
2012-10-13 09:29:48 +00:00
Base.metadata.drop_all(bind = engine)
Base.metadata.create_all(bind = engine)