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

Redefined models using peewee

Obviously untested, and breaks everything 🙃
This commit is contained in:
Alban Féron 2022-11-27 16:37:56 +01:00
parent 9db3549734
commit 0b6891a5c4
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
2 changed files with 126 additions and 193 deletions

View File

@ -53,7 +53,7 @@ python_requires = >=3.6,<3.11
install_requires = install_requires =
click click
flask >=0.11 flask >=0.11
pony >=0.7.6 peewee
Pillow Pillow
requests >=1.0.0 requests >=1.0.0
mediafile mediafile

View File

@ -1,7 +1,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-2020 Alban 'spl0k' Féron # Copyright (C) 2013-2022 Alban 'spl0k' Féron
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
@ -13,11 +13,20 @@ import time
from datetime import datetime from datetime import datetime
from hashlib import sha1 from hashlib import sha1
from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr from peewee import (
from pony.orm import ObjectNotFound, DatabaseError AutoField,
from pony.orm import buffer BinaryUUIDField,
from pony.orm import min, avg, sum, count, exists BlobField,
from pony.orm import db_session BooleanField,
CharField,
DateTimeField,
FixedCharField,
ForeignKeyField,
IntegerField,
TextField,
)
from peewee import CompositeKey
from playhouse.flask_utils import FlaskDB
from urllib.parse import urlparse, parse_qsl from urllib.parse import urlparse, parse_qsl
from uuid import UUID, uuid4 from uuid import UUID, uuid4
@ -28,22 +37,16 @@ def now():
return datetime.now().replace(microsecond=0) return datetime.now().replace(microsecond=0)
metadb = Database() def PrimaryKeyField(**kwargs):
return BinaryUUIDField(primary_key=True, default=uuid4, **kwargs)
class Meta(metadb.Entity): db = FlaskDB()
_table_ = "meta"
key = PrimaryKey(str, 32)
value = Required(str, 256)
db = Database() class Meta(db.Model):
key = CharField(32, primary_key=True)
value = CharField(256)
@db.on_connect(provider="sqlite")
def sqlite_case_insensitive_like(db, connection):
cursor = connection.cursor()
cursor.execute("PRAGMA case_sensitive_like = OFF")
class PathMixin: class PathMixin:
@ -53,43 +56,32 @@ class PathMixin:
path = kwargs.pop("path", None) path = kwargs.pop("path", None)
if path: if path:
kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest() kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest()
return db.Entity.get.__func__(cls, *args, **kwargs) return db.Model.get.__func__(cls, *args, **kwargs)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
path = kwargs["path"] path = kwargs["path"]
kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest() kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest()
db.Entity.__init__(self, *args, **kwargs) db.Model.__init__(self, *args, **kwargs)
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
db.Entity.__setattr__(self, attr, value) db.Model.__setattr__(self, attr, value)
if attr == "path": if attr == "path":
db.Entity.__setattr__( db.Model.__setattr__(
self, "_path_hash", sha1(value.encode("utf-8")).digest() self, "_path_hash", sha1(value.encode("utf-8")).digest()
) )
class Folder(PathMixin, db.Entity): class Folder(PathMixin, db.Model):
_table_ = "folder" id = AutoField()
root = BooleanField()
name = CharField()
path = CharField(4096) # unique
_path_hash = BlobField(column_name="path_hash", unique=True)
created = DateTimeField(default=now)
cover_art = CharField(null=True)
last_scan = IntegerField(default=0)
id = PrimaryKey(int, auto=True) parent = ForeignKeyField("self", null=True, backref="children")
root = Required(bool, default=False)
name = Required(str, autostrip=False)
path = Required(str, 4096, autostrip=False) # unique
_path_hash = Required(buffer, column="path_hash")
created = Required(datetime, precision=0, default=now)
cover_art = Optional(str, nullable=True, autostrip=False)
last_scan = Required(int, default=0)
parent = Optional(lambda: Folder, reverse="children", column="parent_id")
children = Set(lambda: Folder, reverse="parent")
__alltracks = Set(
lambda: Track, lazy=True, reverse="root_folder"
) # Never used, hide it. Could be huge, lazy load
tracks = Set(lambda: Track, reverse="folder")
stars = Set(lambda: StarredFolder)
ratings = Set(lambda: RatingFolder)
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = { info = {
@ -172,15 +164,9 @@ class Folder(PathMixin, db.Entity):
return total return total
class Artist(db.Entity): class Artist(db.Model):
_table_ = "artist" id = PrimaryKeyField()
name = CharField()
id = PrimaryKey(UUID, default=uuid4)
name = Required(str) # unique
albums = Set(lambda: Album)
tracks = Set(lambda: Track)
stars = Set(lambda: StarredArtist)
def as_subsonic_artist(self, user): def as_subsonic_artist(self, user):
info = { info = {
@ -206,15 +192,10 @@ class Artist(db.Entity):
).delete() ).delete()
class Album(db.Entity): class Album(db.Model):
_table_ = "album" id = PrimaryKeyField()
name = CharField()
id = PrimaryKey(UUID, default=uuid4) artist = ForeignKeyField(Artist, backref="albums")
name = Required(str)
artist = Required(Artist, column="artist_id")
tracks = Set(lambda: Track)
stars = Set(lambda: StarredAlbum)
def as_subsonic_album(self, user): # "AlbumID3" type in XSD def as_subsonic_album(self, user): # "AlbumID3" type in XSD
info = { info = {
@ -263,38 +244,31 @@ class Album(db.Entity):
).delete() ).delete()
class Track(PathMixin, db.Entity): class Track(PathMixin, db.Model):
_table_ = "track" id = PrimaryKeyField()
disc = IntegerField()
number = IntegerField()
title = CharField()
year = IntegerField(null=True)
genre = CharField(null=True)
duration = IntegerField()
has_art = BooleanField(default=False)
id = PrimaryKey(UUID, default=uuid4) album = ForeignKeyField(Album, backref="tracks")
disc = Required(int) artist = ForeignKeyField(Artist, backref="tracks")
number = Required(int)
title = Required(str)
year = Optional(int)
genre = Optional(str, nullable=True)
duration = Required(int)
has_art = Required(bool, default=False)
album = Required(Album, column="album_id") bitrate = IntegerField()
artist = Required(Artist, column="artist_id")
bitrate = Required(int) path = CharField(4096) # unique
_path_hash = BlobField(column_name="path_hash", unique=True)
created = DateTimeField(default=now)
last_modification = IntegerField()
path = Required(str, 4096, autostrip=False) # unique play_count = IntegerField(default=0)
_path_hash = Required(buffer, column="path_hash") last_play = DateTimeField(null=True)
created = Required(datetime, precision=0, default=now)
last_modification = Required(int)
play_count = Required(int, default=0) root_folder = ForeignKeyField(Folder, backref="+")
last_play = Optional(datetime, precision=0) folder = ForeignKeyField(Folder, backref="tracks")
root_folder = Required(Folder, column="root_folder_id")
folder = Required(Folder, column="folder_id")
__lastly_played_by = Set(lambda: User) # Never used, hide it
stars = Set(lambda: StarredTrack)
ratings = Set(lambda: RatingTrack)
def as_subsonic_child(self, user, prefs): def as_subsonic_child(self, user, prefs):
info = { info = {
@ -374,36 +348,23 @@ class Track(PathMixin, db.Entity):
return f"{self.album.artist.name}{self.album.name}{self.disc:02}{self.number:02}{self.title}".lower() return f"{self.album.artist.name}{self.album.name}{self.disc:02}{self.number:02}{self.title}".lower()
class User(db.Entity): class User(db.Model):
_table_ = "user" id = PrimaryKeyField()
name = CharField(64, unique=True)
mail = CharField(null=True)
password = FixedCharField(40)
salt = FixedCharField(6)
id = PrimaryKey(UUID, default=uuid4) admin = BooleanField(default=False)
name = Required(str, 64) # unique jukebox = BooleanField(default=False)
mail = Optional(str)
password = Required(str, 40)
salt = Required(str, 6)
admin = Required(bool, default=False) lastfm_session = FixedCharField(32, null=True)
jukebox = Required(bool, default=False) lastfm_status = BooleanField(
default=True
lastfm_session = Optional(str, 32, nullable=True)
lastfm_status = Required(
bool, default=True
) # True: ok/unlinked, False: invalid session ) # True: ok/unlinked, False: invalid session
last_play = Optional(Track, column="last_play_id") last_play = ForeignKeyField(Track, null=True, backref="+")
last_play_date = Optional(datetime, precision=0) last_play_date = DateTimeField(null=True)
clients = Set(lambda: ClientPrefs)
playlists = Set(lambda: Playlist)
__messages = Set(lambda: ChatMessage, lazy=True) # Never used, hide it
starred_folders = Set(lambda: StarredFolder, lazy=True)
starred_artists = Set(lambda: StarredArtist, lazy=True)
starred_albums = Set(lambda: StarredAlbum, lazy=True)
starred_tracks = Set(lambda: StarredTrack, lazy=True)
folder_ratings = Set(lambda: RatingFolder, lazy=True)
track_ratings = Set(lambda: RatingTrack, lazy=True)
def as_subsonic_user(self): def as_subsonic_user(self):
return { return {
@ -424,81 +385,57 @@ class User(db.Entity):
} }
class ClientPrefs(db.Entity): class ClientPrefs(db.Model):
_table_ = "client_prefs" user = ForeignKeyField(User, backref="clients")
client_name = CharField(32)
format = CharField(8, null=True)
bitrate = IntegerField(null=True)
user = Required(User, column="user_id") class Meta:
client_name = Required(str, 32) primary_key = CompositeKey("user", "client_name")
PrimaryKey(user, client_name)
format = Optional(str, 8, nullable=True)
bitrate = Optional(int)
class StarredFolder(db.Entity): def _make_starred_model(target_model):
_table_ = "starred_folder" class Starred(db.Model):
user = ForeignKeyField(User, backref="+")
starred = ForeignKeyField(target_model, backref="+")
date = DateTimeField(default=now)
user = Required(User, column="user_id") class Meta:
starred = Required(Folder, column="starred_id") primary_key = CompositeKey("user", "starred")
date = Required(datetime, precision=0, default=now) table_name = "starred_" + target_model._meta.table_name
PrimaryKey(user, starred) return Starred
class StarredArtist(db.Entity): StarredFolder = _make_starred_model(Folder)
_table_ = "starred_artist" StarredArtist = _make_starred_model(Artist)
StarredAlbum = _make_starred_model(Album)
user = Required(User, column="user_id") StarredTrack = _make_starred_model(Track)
starred = Required(Artist, column="starred_id")
date = Required(datetime, precision=0, default=now)
PrimaryKey(user, starred)
class StarredAlbum(db.Entity): def _make_rating_model(target_model):
_table_ = "starred_album" class Rating(db.Model):
user = ForeignKeyField(User, backref="+")
rated = ForeignKeyField(target_model, backref="+")
rating = IntegerField() # min=1, max=5
user = Required(User, column="user_id") class Meta:
starred = Required(Album, column="starred_id") primary_key = CompositeKey("user", "rated")
date = Required(datetime, precision=0, default=now) table_name = "rating_" + target_model._meta.table_name
PrimaryKey(user, starred) return Rating
class StarredTrack(db.Entity): RatingFolder = _make_rating_model(Folder)
_table_ = "starred_track" RatingTrack = _make_rating_model(Track)
user = Required(User, column="user_id")
starred = Required(Track, column="starred_id")
date = Required(datetime, precision=0, default=now)
PrimaryKey(user, starred)
class RatingFolder(db.Entity): class ChatMessage(db.Model):
_table_ = "rating_folder" id = PrimaryKeyField()
user = Required(User, column="user_id") user = ForeignKeyField(User, backref="+")
rated = Required(Folder, column="rated_id") time = IntegerField(default=lambda: int(time.time()))
rating = Required(int, min=1, max=5) message = CharField(512)
PrimaryKey(user, rated)
class RatingTrack(db.Entity):
_table_ = "rating_track"
user = Required(User, column="user_id")
rated = Required(Track, column="rated_id")
rating = Required(int, min=1, max=5)
PrimaryKey(user, rated)
class ChatMessage(db.Entity):
_table_ = "chat_message"
id = PrimaryKey(UUID, default=uuid4)
user = Required(User, column="user_id")
time = Required(int, default=lambda: int(time.time()))
message = Required(str, 512)
def responsize(self): def responsize(self):
return { return {
@ -508,16 +445,14 @@ class ChatMessage(db.Entity):
} }
class Playlist(db.Entity): class Playlist(db.Model):
_table_ = "playlist" id = PrimaryKeyField()
user = ForeignKeyField(User, backref="playlists")
id = PrimaryKey(UUID, default=uuid4) name = CharField()
user = Required(User, column="user_id") comment = CharField(null=True)
name = Required(str) public = BooleanField(default=False)
comment = Optional(str) created = DateTimeField(default=now)
public = Required(bool, default=False) tracks = TextField(null=True)
created = Required(datetime, precision=0, default=now)
tracks = Optional(LongStr)
def as_subsonic_playlist(self, user): def as_subsonic_playlist(self, user):
tracks = self.get_tracks() tracks = self.get_tracks()
@ -583,14 +518,12 @@ class Playlist(db.Entity):
self.tracks = ",".join(t for t in tracks if t) self.tracks = ",".join(t for t in tracks if t)
class RadioStation(db.Entity): class RadioStation(db.Model):
_table_ = "radio_station" id = PrimaryKeyField()
stream_url = CharField()
id = PrimaryKey(UUID, default=uuid4) name = CharField()
stream_url = Required(str) homepage_url = CharField(null=True)
name = Required(str) created = DateTimeField(default=now)
homepage_url = Optional(str, nullable=True)
created = Required(datetime, precision=0, default=now)
def as_subsonic_station(self): def as_subsonic_station(self):
info = { info = {