mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 17:06:17 +00:00
Redefined models using peewee
Obviously untested, and breaks everything 🙃
This commit is contained in:
parent
9db3549734
commit
0b6891a5c4
@ -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
|
||||||
|
317
supysonic/db.py
317
supysonic/db.py
@ -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 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user