refactor: move music db into a separated file.

IMPORTANT NOTE:
The default database path has changed. It was database.db, but
now they ARE settings.db and music.db.

Database migration will detect music table in settings.db, and
move it out of settings.db.

To update from old version, you need to
 1. if you use your own db path settings in the command
    line or in configuration.ini, you don't have to change
    anything;
 1. or,if you use default database path, rename database.db
    into settings.db.
This commit is contained in:
Terry Geng 2020-05-13 10:43:40 +08:00
parent 4c27bb28a1
commit 40ea744e7f
No known key found for this signature in database
GPG Key ID: F982F8EA1DF720E7
7 changed files with 161 additions and 111 deletions

View File

@ -1151,9 +1151,9 @@ def cmd_drop_database(bot, user, text, command, parameter):
if bot.is_admin(user):
var.db.drop_table()
var.db = SettingsDatabase(var.dbfile)
var.db = SettingsDatabase(var.settings_db_path)
var.music_db.drop_table()
var.music_db = MusicDatabase(var.dbfile)
var.music_db = MusicDatabase(var.settings_db_path)
log.info("command: database dropped.")
bot.send_msg(constants.strings('database_dropped'), text)
else:

View File

@ -45,7 +45,6 @@ admin = User1;User2;
music_folder = music_folder/
# Folder that stores the downloaded music.
tmp_folder = /tmp/
database_path = database.db
pip3_path = venv/bin/pip
auto_check_update = True
logfile =

View File

@ -24,6 +24,22 @@ port = 64738
#username = botamusique
#comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!
# 'music_folder': Folder that stores your local songs.
#music_folder = music_folder/
# 'database_path': The path of the database. The database will store things like your volume
# set by command !volume, your playback mode and your playlist, banned URLs, etc.
# This option will be overridden by command line arguments.
# 'music_database_path': The path of database that stores the music library. Can be disabled by
# setting 'save_music_library=False'
#database_path=settings.db
#music_database_path=music.db
# 'admin': Users allowed to kill the bot, or ban URLs. Separated by ';'
#admin = User1;User2;
# 'volume' is default volume from 0 to 1.
# This option will be overridden by value in the database.
#volume = 0.1
@ -42,17 +58,6 @@ port = 64738
# stable will use simple bash with curl command to get releases, testing will follow github master branch with git commands
#target_version = stable
# 'admin': Users allowed to kill the bot, or ban URLs. Separated by ';'
#admin = User1;User2;
# 'music_folder': Folder that stores your local songs.
#music_folder = music_folder/
# 'database_path': The path of the database. The database will store things like your volume
# set by command !volume, your playback mode and your playlist, etc.
# This option will be overridden by command line arguments.
#database_path = database.db
# 'tmp_folder': Folder that stores the downloaded music.
# 'tmp_folder_max_size': in MB, 0 for no cache, -1 for unlimited size
# 'ignored_folders', 'ignored_files': files and folders that would be ignored during scanning.

View File

@ -1,3 +1,4 @@
import os
import re
import sqlite3
import json
@ -188,62 +189,13 @@ class Condition:
return self
SETTING_DB_VERSION = 1
SETTING_DB_VERSION = 2
MUSIC_DB_VERSION = 2
class SettingsDatabase:
def __init__(self, db_path):
self.db_path = db_path
# connect
conn = sqlite3.connect(self.db_path)
self.db_version_check_and_create()
conn.commit()
conn.close()
def has_table(self, table):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
tables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", (table,)).fetchall()
conn.close()
if len(tables) == 0:
return False
return True
def db_version_check_and_create(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
if self.has_table('botamusique'):
# check version
ver = self.getint("bot", "db_version", fallback=None)
if ver is None or ver != SETTING_DB_VERSION:
# old_name = "botamusique_old_%s" % datetime.datetime.now().strftime("%Y%m%d")
# cursor.execute("ALTER TABLE botamusique RENAME TO %s" % old_name)
cursor.execute("DROP TABLE botamusique")
conn.commit()
self.create_table()
else:
self.create_table()
conn.close()
def create_table(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS botamusique ("
"section TEXT, "
"option TEXT, "
"value TEXT, "
"UNIQUE(section, option))")
cursor.execute("INSERT INTO botamusique (section, option, value) "
"VALUES (?, ?, ?)", ("bot", "db_version", SETTING_DB_VERSION))
conn.commit()
conn.close()
def get(self, section, option, **kwargs):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
@ -321,9 +273,6 @@ class MusicDatabase:
def __init__(self, db_path):
self.db_path = db_path
MusicDatabaseMigration(self).migrate()
self.manage_special_tags()
def insert_music(self, music_dict, _conn=None):
conn = sqlite3.connect(self.db_path) if _conn is None else _conn
cursor = conn.cursor()
@ -539,15 +488,53 @@ class MusicDatabase:
conn.close()
class MusicDatabaseMigration:
def __init__(self, db: MusicDatabase):
self.db = db
self.migrate_func = {}
self.migrate_func[0] = self.migrate_from_0_to_1
self.migrate_func[1] = self.migrate_from_1_to_2
class DatabaseMigration:
def __init__(self, settings_db: SettingsDatabase, music_db: MusicDatabase):
self.settings_db = settings_db
self.music_db = music_db
self.settings_table_migrate_func = {}
self.settings_table_migrate_func[0] = self.settings_table_migrate_from_0_to_2
self.settings_table_migrate_func[1] = self.settings_table_migrate_from_0_to_2
self.music_table_migrate_func = {}
self.music_table_migrate_func[0] = self.music_table_migrate_from_0_to_1
self.music_table_migrate_func[1] = self.music_table_migrate_from_1_to_2
def migrate(self):
conn = sqlite3.connect(self.db.db_path)
self.settings_database_migrate()
self.music_database_migrate()
def settings_database_migrate(self):
conn = sqlite3.connect(self.settings_db.db_path)
cursor = conn.cursor()
if self.has_table('botamusique', conn):
current_version = 0
ver = cursor.execute("SELECT value FROM botamusique WHERE section='bot' "
"AND option='db_version'").fetchone()
if ver:
current_version = int(ver[0])
if current_version == SETTING_DB_VERSION:
conn.close()
return
else:
log.info(f"database: migrating from settings table version {current_version} to {SETTING_DB_VERSION}...")
while current_version < SETTING_DB_VERSION:
log.debug(f"database: migrate step {current_version}/{SETTING_DB_VERSION - 1}")
current_version = self.settings_table_migrate_func[current_version](conn)
log.info(f"database: migration done.")
cursor.execute("UPDATE botamusique SET value=? "
"WHERE section='bot' AND option='db_version'", (SETTING_DB_VERSION,))
else:
log.info(f"database: no settings table found. Creating settings table version {SETTING_DB_VERSION}.")
self.create_settings_table_version_2(conn)
conn.commit()
conn.close()
def music_database_migrate(self):
conn = sqlite3.connect(self.music_db.db_path)
cursor = conn.cursor()
if self.has_table('music', conn):
current_version = 0
@ -562,7 +549,7 @@ class MusicDatabaseMigration:
log.info(f"database: migrating from music table version {current_version} to {MUSIC_DB_VERSION}...")
while current_version < MUSIC_DB_VERSION:
log.debug(f"database: migrate step {current_version}/{MUSIC_DB_VERSION - 1}")
current_version = self.migrate_func[current_version](conn)
current_version = self.music_table_migrate_func[current_version](conn)
log.info(f"database: migration done.")
cursor.execute("UPDATE music SET title=? "
@ -570,7 +557,7 @@ class MusicDatabaseMigration:
else:
log.info(f"database: no music table found. Creating music table version {MUSIC_DB_VERSION}.")
self.create_table_version_2(conn)
self.create_music_table_version_2(conn)
conn.commit()
conn.close()
@ -582,7 +569,20 @@ class MusicDatabaseMigration:
return False
return True
def create_table_version_1(self, conn):
def create_settings_table_version_2(self, conn):
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS botamusique ("
"section TEXT, "
"option TEXT, "
"value TEXT, "
"UNIQUE(section, option))")
cursor.execute("INSERT INTO botamusique (section, option, value) "
"VALUES (?, ?, ?)", ("bot", "db_version", 2))
conn.commit()
return 1
def create_music_table_version_1(self, conn):
cursor = conn.cursor()
cursor.execute("CREATE TABLE music ("
@ -600,15 +600,37 @@ class MusicDatabaseMigration:
conn.commit()
def create_table_version_2(self, conn):
self.create_table_version_1(conn)
def create_music_table_version_2(self, conn):
self.create_music_table_version_1(conn)
def migrate_from_0_to_1(self, conn):
def settings_table_migrate_from_0_to_2(self, conn):
cursor = conn.cursor()
cursor.execute("DROP TABLE botamusique")
conn.commit()
self.create_settings_table_version_2(conn)
# move music database into a separated file
if self.has_table('music', conn) and not os.path.exists(self.music_db.db_path):
log.info(f"database: move music db into separated file.")
cursor.execute(f"ATTACH DATABASE '{self.music_db.db_path}' AS music_db")
cursor.execute(f"SELECT sql FROM sqlite_master "
f"WHERE type='table' AND name='music'")
sql_create_table = cursor.fetchone()[0]
sql_create_table = sql_create_table.replace("music", "music_db.music")
cursor.execute(sql_create_table)
cursor.execute("INSERT INTO music_db.music SELECT * FROM music")
conn.commit()
cursor.execute("DETACH DATABASE music_db")
cursor.execute("DROP TABLE music")
return 2
def music_table_migrate_from_0_to_1(self, conn):
cursor = conn.cursor()
cursor.execute("ALTER TABLE music RENAME TO music_old")
conn.commit()
self.create_table_version_1(conn)
self.create_music_table_version_1(conn)
cursor.execute("INSERT INTO music (id, type, title, metadata, tags)"
"SELECT id, type, title, metadata, tags FROM music_old")
@ -617,7 +639,7 @@ class MusicDatabaseMigration:
return 1 # return new version number
def migrate_from_1_to_2(self, conn):
def music_table_migrate_from_1_to_2(self, conn):
items_to_update = self.db.query_music(Condition(), conn)
for item in items_to_update:
item['keywords'] = item['title']

View File

@ -115,6 +115,7 @@ class MusicCache(dict):
self.log.debug("library: music save into database: %s" % item.format_debug_string())
self.db.insert_music(item.to_dict())
self.db.manage_special_tags()
self.dir_lock.release()

View File

@ -23,7 +23,7 @@ from packaging import version
import util
import command
import constants
from database import SettingsDatabase, MusicDatabase
from database import SettingsDatabase, MusicDatabase, DatabaseMigration
import media.system
from media.item import ValidationFailedError, PreparationFailedError
from media.playlist import BasePlaylist
@ -666,7 +666,9 @@ if __name__ == '__main__':
parser.add_argument("--config", dest='config', type=str, default='configuration.ini',
help='Load configuration from this file. Default: configuration.ini')
parser.add_argument("--db", dest='db', type=str,
default=None, help='database file. Default: database.db')
default=None, help='settings database file. Default: settings.db')
parser.add_argument("--music-db", dest='music_db', type=str,
default=None, help='music library database file. Default: music.db')
parser.add_argument("-q", "--quiet", dest="quiet",
action="store_true", help="Only Error logs")
@ -691,20 +693,21 @@ if __name__ == '__main__':
args = parser.parse_args()
# ======================
# Load Config
# ======================
config = configparser.ConfigParser(interpolation=None, allow_no_value=True)
var.config = config
parsed_configs = config.read([util.solve_filepath('configuration.default.ini'), util.solve_filepath(args.config)],
encoding='utf-8')
var.dbfile = args.db if args.db is not None else util.solve_filepath(
config.get("bot", "database_path", fallback="database.db"))
if len(parsed_configs) == 0:
logging.error('Could not read configuration from file \"{}\"'.format(args.config))
sys.exit()
var.config = config
var.db = SettingsDatabase(var.dbfile)
# Setup logger
# ======================
# Setup Logger
# ======================
bot_logger = logging.getLogger("bot")
formatter = logging.Formatter('[%(asctime)s %(levelname)s %(threadName)s] %(message)s', "%b %d %H:%M:%S")
@ -732,14 +735,32 @@ if __name__ == '__main__':
logging.getLogger("root").addHandler(handler)
var.bot_logger = bot_logger
# ======================
# Load Database
# ======================
var.settings_db_path = args.db if args.db is not None else util.solve_filepath(
config.get("bot", "database_path", fallback="settings.db"))
var.music_db_path = args.music_db if args.music_db is not None else util.solve_filepath(
config.get("bot", "music_database_path", fallback="music.db"))
var.db = SettingsDatabase(var.settings_db_path)
if var.config.get("bot", "save_music_library", fallback=True):
var.music_db = MusicDatabase(var.dbfile)
var.music_db = MusicDatabase(var.music_db_path)
else:
var.music_db = MusicDatabase(":memory:")
DatabaseMigration(var.db, var.music_db).migrate()
var.cache = MusicCache(var.music_db)
# load playback mode
if var.config.get("bot", "refresh_cache_on_startup", fallback=True):
var.cache.build_dir_cache()
# ======================
# Load playback mode
# ======================
playback_mode = None
if var.db.has_option("playlist", "playback_mode"):
playback_mode = var.db.get('playlist', 'playback_mode')
@ -751,12 +772,13 @@ if __name__ == '__main__':
else:
raise KeyError("Unknown playback mode '%s'" % playback_mode)
# ======================
# Create bot instance
# ======================
var.bot = MumbleBot(args)
command.register_all_commands(var.bot)
if var.config.get("bot", "refresh_cache_on_startup", fallback=True):
var.cache.build_dir_cache()
# load playlist
if var.config.getboolean('bot', 'save_playlist', fallback=True):
var.bot_logger.info("bot: load playlist from previous session")

View File

@ -13,7 +13,8 @@ cache: 'media.cache.MusicCache' = None
user = ""
is_proxified = False
dbfile = None
settings_db_path = None
music_db_path = None
db = None
music_db: 'database.MusicDatabase' = None
config: 'database.SettingsDatabase' = None