diff --git a/supysonic/db.py b/supysonic/db.py index 29f7179..7086834 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -15,7 +15,6 @@ from datetime import datetime from hashlib import sha1 from peewee import ( AutoField, - BinaryUUIDField, BlobField, BooleanField, CharField, @@ -24,6 +23,7 @@ from peewee import ( ForeignKeyField, IntegerField, TextField, + UUIDField, ) from peewee import CompositeKey, DatabaseProxy, Model, MySQLDatabase from peewee import fn @@ -31,7 +31,7 @@ from playhouse.db_url import parseresult_to_dict, schemes from urllib.parse import urlparse from uuid import UUID, uuid4 -SCHEMA_VERSION = "20200607" +SCHEMA_VERSION = "20230111" def now(): @@ -45,7 +45,7 @@ def random(): def PrimaryKeyField(**kwargs): - return BinaryUUIDField(primary_key=True, default=uuid4, **kwargs) + return UUIDField(primary_key=True, default=uuid4, **kwargs) db = DatabaseProxy() diff --git a/supysonic/schema/migration/mysql/20230111.py b/supysonic/schema/migration/mysql/20230111.py new file mode 100644 index 0000000..d659cee --- /dev/null +++ b/supysonic/schema/migration/mysql/20230111.py @@ -0,0 +1,75 @@ +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2023 Alban 'spl0k' Féron +# +# Distributed under terms of the GNU AGPLv3 license. + +# Converts ids from binary data to hex-encoded strings + +try: + import MySQLdb as provider +except ImportError: + import pymysql as provider + +from uuid import UUID +from warnings import filterwarnings + + +def process_table(connection, table, fields, nullable_fields=()): + to_update = {field: set() for field in fields + nullable_fields} + + c = connection.cursor() + c.execute("SELECT {1} FROM {0}".format(table, ",".join(fields + nullable_fields))) + for row in c: + for field, value in zip(fields + nullable_fields, row): + if value is None or not isinstance(value, bytes): + continue + to_update[field].add(value) + + for field in fields: + sql = "ALTER TABLE {} MODIFY {} BINARY(32) NOT NULL".format(table, field) + c.execute(sql) + for field in nullable_fields: + sql = "ALTER TABLE {} MODIFY {} BINARY(32)".format(table, field) + c.execute(sql) + for field, values in to_update.items(): + if not values: + continue + sql = "UPDATE {0} SET {1}=%s WHERE {1}=%s".format(table, field) + c.executemany( + sql, map(lambda v: (UUID(bytes=v).hex, v + (b"\x00" * 16)), values) + ) + for field in fields: + sql = "ALTER TABLE {} MODIFY {} CHAR(32) NOT NULL".format(table, field) + c.execute(sql) + for field in nullable_fields: + sql = "ALTER TABLE {} MODIFY {} CHAR(32)".format(table, field) + c.execute(sql) + + connection.commit() + + +def apply(args): + filterwarnings("ignore", category=provider.Warning) + + conn = provider.connect(**args) + conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 0") + + process_table(conn, "artist", ("id",)) + process_table(conn, "album", ("id", "artist_id")) + process_table(conn, "track", ("id", "album_id", "artist_id")) + process_table(conn, "user", ("id",), ("last_play_id",)) + process_table(conn, "client_prefs", ("user_id",)) + process_table(conn, "starred_folder", ("user_id",)) + process_table(conn, "starred_artist", ("user_id", "starred_id")) + process_table(conn, "starred_album", ("user_id", "starred_id")) + process_table(conn, "starred_track", ("user_id", "starred_id")) + process_table(conn, "rating_folder", ("user_id",)) + process_table(conn, "rating_track", ("user_id", "rated_id")) + process_table(conn, "chat_message", ("id", "user_id")) + process_table(conn, "playlist", ("id", "user_id")) + process_table(conn, "radio_station", ("id",)) + + conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 1") + conn.close() diff --git a/supysonic/schema/migration/sqlite/20230111.py b/supysonic/schema/migration/sqlite/20230111.py new file mode 100644 index 0000000..9c81edb --- /dev/null +++ b/supysonic/schema/migration/sqlite/20230111.py @@ -0,0 +1,50 @@ +# This file is part of Supysonic. +# Supysonic is a Python implementation of the Subsonic server API. +# +# Copyright (C) 2023 Alban 'spl0k' Féron +# +# Distributed under terms of the GNU AGPLv3 license. + +# Converts ids from binary data to hex-encoded strings + +import sqlite3 + +from uuid import UUID + + +def process_table(connection, table, fields): + to_update = {field: set() for field in fields} + + c = connection.cursor() + for row in c.execute("SELECT {1} FROM {0}".format(table, ",".join(fields))): + for field, value in zip(fields, row): + if value is None or not isinstance(value, bytes): + continue + to_update[field].add(value) + + for field, values in to_update.items(): + sql = "UPDATE {0} SET {1}=? WHERE {1}=?".format(table, field) + c.executemany(sql, map(lambda v: (UUID(bytes=v).hex, v), values)) + + connection.commit() + + +def apply(args): + file = args.pop("database") + with sqlite3.connect(file, **args) as conn: + conn.cursor().execute("PRAGMA foreign_keys = OFF") + + process_table(conn, "artist", ("id",)) + process_table(conn, "album", ("id", "artist_id")) + process_table(conn, "track", ("id", "album_id", "artist_id")) + process_table(conn, "user", ("id", "last_play_id")) + process_table(conn, "client_prefs", ("user_id",)) + process_table(conn, "starred_folder", ("user_id",)) + process_table(conn, "starred_artist", ("user_id", "starred_id")) + process_table(conn, "starred_album", ("user_id", "starred_id")) + process_table(conn, "starred_track", ("user_id", "starred_id")) + process_table(conn, "rating_folder", ("user_id",)) + process_table(conn, "rating_track", ("user_id", "rated_id")) + process_table(conn, "chat_message", ("id", "user_id")) + process_table(conn, "playlist", ("id", "user_id")) + process_table(conn, "radio_station", ("id",)) diff --git a/supysonic/schema/mysql.sql b/supysonic/schema/mysql.sql index a4c1c96..dd14798 100644 --- a/supysonic/schema/mysql.sql +++ b/supysonic/schema/mysql.sql @@ -12,19 +12,19 @@ CREATE TABLE IF NOT EXISTS folder ( CREATE INDEX index_folder_parent_id_fk ON folder(parent_id); CREATE TABLE IF NOT EXISTS artist ( - id BINARY(16) PRIMARY KEY, + id CHAR(32) PRIMARY KEY, name VARCHAR(256) NOT NULL ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS album ( - id BINARY(16) PRIMARY KEY, + id CHAR(32) PRIMARY KEY, name VARCHAR(256) NOT NULL, - artist_id BINARY(16) NOT NULL REFERENCES artist(id) + artist_id CHAR(32) NOT NULL REFERENCES artist(id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX index_album_artist_id_fk ON album(artist_id); CREATE TABLE IF NOT EXISTS track ( - id BINARY(16) PRIMARY KEY, + id CHAR(32) PRIMARY KEY, disc INTEGER NOT NULL, number INTEGER NOT NULL, title VARCHAR(256) NOT NULL, @@ -32,8 +32,8 @@ CREATE TABLE IF NOT EXISTS track ( genre VARCHAR(256), duration INTEGER NOT NULL, has_art BOOLEAN NOT NULL DEFAULT false, - album_id BINARY(16) NOT NULL REFERENCES album(id), - artist_id BINARY(16) NOT NULL REFERENCES artist(id), + album_id CHAR(32) NOT NULL REFERENCES album(id), + artist_id CHAR(32) NOT NULL REFERENCES artist(id), bitrate INTEGER NOT NULL, path VARCHAR(4096) NOT NULL, path_hash BINARY(20) UNIQUE NOT NULL, @@ -50,7 +50,7 @@ CREATE INDEX index_track_folder_id_fk ON track(folder_id); CREATE INDEX index_track_root_folder_id_fk ON track(root_folder_id); CREATE TABLE IF NOT EXISTS user ( - id BINARY(16) PRIMARY KEY, + id CHAR(32) PRIMARY KEY, name VARCHAR(64) NOT NULL, mail VARCHAR(256), password CHAR(40) NOT NULL, @@ -59,13 +59,13 @@ CREATE TABLE IF NOT EXISTS user ( jukebox BOOLEAN NOT NULL, lastfm_session CHAR(32), lastfm_status BOOLEAN NOT NULL, - last_play_id BINARY(16) REFERENCES track(id), + last_play_id CHAR(32) REFERENCES track(id), last_play_date DATETIME ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX index_user_last_play_id_fk ON user(last_play_id); CREATE TABLE IF NOT EXISTS client_prefs ( - user_id BINARY(16) NOT NULL, + user_id CHAR(32) NOT NULL, client_name VARCHAR(32) NOT NULL, format VARCHAR(8), bitrate INTEGER, @@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS client_prefs ( ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS starred_folder ( - user_id BINARY(16) NOT NULL REFERENCES user(id), + user_id CHAR(32) NOT NULL REFERENCES user(id), starred_id INTEGER NOT NULL REFERENCES folder(id), date DATETIME NOT NULL, PRIMARY KEY (user_id, starred_id) @@ -82,8 +82,8 @@ CREATE INDEX index_starred_folder_user_id_fk ON starred_folder(user_id); CREATE INDEX index_starred_folder_starred_id_fk ON starred_folder(starred_id); CREATE TABLE IF NOT EXISTS starred_artist ( - user_id BINARY(16) NOT NULL REFERENCES user(id), - starred_id BINARY(16) NOT NULL REFERENCES artist(id), + user_id CHAR(32) NOT NULL REFERENCES user(id), + starred_id CHAR(32) NOT NULL REFERENCES artist(id), date DATETIME NOT NULL, PRIMARY KEY (user_id, starred_id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -91,8 +91,8 @@ CREATE INDEX index_starred_artist_user_id_fk ON starred_artist(user_id); CREATE INDEX index_starred_artist_starred_id_fk ON starred_artist(starred_id); CREATE TABLE IF NOT EXISTS starred_album ( - user_id BINARY(16) NOT NULL REFERENCES user(id), - starred_id BINARY(16) NOT NULL REFERENCES album(id), + user_id CHAR(32) NOT NULL REFERENCES user(id), + starred_id CHAR(32) NOT NULL REFERENCES album(id), date DATETIME NOT NULL, PRIMARY KEY (user_id, starred_id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -100,8 +100,8 @@ CREATE INDEX index_starred_album_user_id_fk ON starred_album(user_id); CREATE INDEX index_starred_album_starred_id_fk ON starred_album(starred_id); CREATE TABLE IF NOT EXISTS starred_track ( - user_id BINARY(16) NOT NULL REFERENCES user(id), - starred_id BINARY(16) NOT NULL REFERENCES track(id), + user_id CHAR(32) NOT NULL REFERENCES user(id), + starred_id CHAR(32) NOT NULL REFERENCES track(id), date DATETIME NOT NULL, PRIMARY KEY (user_id, starred_id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -109,7 +109,7 @@ CREATE INDEX index_starred_track_user_id_fk ON starred_track(user_id); CREATE INDEX index_starred_track_starred_id_fk ON starred_track(starred_id); CREATE TABLE IF NOT EXISTS rating_folder ( - user_id BINARY(16) NOT NULL REFERENCES user(id), + user_id CHAR(32) NOT NULL REFERENCES user(id), rated_id INTEGER NOT NULL REFERENCES folder(id), rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), PRIMARY KEY (user_id, rated_id) @@ -118,8 +118,8 @@ CREATE INDEX index_rating_folder_user_id_fk ON rating_folder(user_id); CREATE INDEX index_rating_folder_rated_id_fk ON rating_folder(rated_id); CREATE TABLE IF NOT EXISTS rating_track ( - user_id BINARY(16) NOT NULL REFERENCES user(id), - rated_id BINARY(16) NOT NULL REFERENCES track(id), + user_id CHAR(32) NOT NULL REFERENCES user(id), + rated_id CHAR(32) NOT NULL REFERENCES track(id), rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5), PRIMARY KEY (user_id, rated_id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -127,16 +127,16 @@ CREATE INDEX index_rating_track_user_id_fk ON rating_track(user_id); CREATE INDEX index_rating_track_rated_id_fk ON rating_track(rated_id); CREATE TABLE IF NOT EXISTS chat_message ( - id BINARY(16) PRIMARY KEY, - user_id BINARY(16) NOT NULL REFERENCES user(id), + id CHAR(32) PRIMARY KEY, + user_id CHAR(32) NOT NULL REFERENCES user(id), time INTEGER NOT NULL, message VARCHAR(512) NOT NULL ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX index_chat_message_user_id_fk ON chat_message(user_id); CREATE TABLE IF NOT EXISTS playlist ( - id BINARY(16) PRIMARY KEY, - user_id BINARY(16) NOT NULL REFERENCES user(id), + id CHAR(32) PRIMARY KEY, + user_id CHAR(32) NOT NULL REFERENCES user(id), name VARCHAR(256) NOT NULL, comment VARCHAR(256), public BOOLEAN NOT NULL, @@ -151,7 +151,7 @@ CREATE TABLE meta ( ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS radio_station ( - id BINARY(16) PRIMARY KEY, + id CHAR(32) PRIMARY KEY, stream_url VARCHAR(256) NOT NULL, name VARCHAR(256) NOT NULL, homepage_url VARCHAR(256),