From a6c43fe4684815cd3c26be5a9abd26087696d43b Mon Sep 17 00:00:00 2001 From: iou1name Date: Thu, 19 Dec 2024 15:00:38 -0500 Subject: [PATCH] add build library functions --- README.md | 2 +- build_db.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++ pyrite.sql | 13 +++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 build_db.py create mode 100644 pyrite.sql diff --git a/README.md b/README.md index 95281f1..1a53de6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A naturally occurring iron disulfide mineral. The name comes from the Greek word ## Requirements Python 3.12+ -Python packages: `fastapi uvicorn[standard] httpx jinja2 asyncpg mutagen` +Python packages: `fastapi uvicorn[standard] httpx jinja2 asyncpg tinytag` ## Install ``` diff --git a/build_db.py b/build_db.py new file mode 100644 index 0000000..eafce89 --- /dev/null +++ b/build_db.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Builds the music library database. +""" +import os +import asyncio +import multiprocessing + +import asyncpg +import tinytag + +import config + + +MUSIC_EXT = ['.flac', '.mp3', '.opus'] + +def read_track(filepath): + """ + Reads the specified file and extracts relevant information from it. + """ + t = tinytag.TinyTag.get(filepath) + + d = { + 'filepath': filepath, + 'artist': t.artist, + 'albumartist': t.albumartist, + 'album': t.album, + 'title': t.title, + 'date': t.year, + 'discnumber': str(t.disc), + 'tracknumber': str(t.track), + 'genre': t.genre, + 'duration': t.duration, + 'last_modified': os.path.getmtime(filepath) + } + + return d + + +async def build_library(root_dir): + """Walks the directory and builds a library from tracks discovered.""" + print("Building library") + + db_pool = await asyncpg.create_pool(**config.db) + async with db_pool.acquire() as conn: + with open('pyrite.sql', 'r') as file: + await conn.execute(file.read()) + + filepaths = [] + for dir_name, sub_dirs, files in os.walk(root_dir): + for file in files: + if not os.path.splitext(file)[1] in MUSIC_EXT: + continue + filepath = os.path.join(root_dir, dir_name, file) + last_modified = os.path.getmtime(filepath) + filepaths.append((filepath, last_modified)) + + async with db_pool.acquire() as conn: + tracks_prev = await conn.fetch("SELECT filepath, last_modified FROM track") + tracks_prev = {track['filepath']: track for track in tracks_prev} + + global worker + def worker(args): + """Worker for multi-processing tracks.""" + filepath, last_modified = args + track_prev = tracks_prev.get(filepath) + if track_prev: + if track_prev['last_modified'] >= last_modified: + return + data = read_track(filepath) + return data + + with multiprocessing.Pool() as pool: + mapping = pool.imap(worker, filepaths) + tracks = [] + prev_percent = 0 + while True: + try: + track = mapping.next() + if track: + tracks.append(track) + except StopIteration: + break + percent = round(len(tracks) / len(filepaths) * 100, 2) + if percent >= prev_percent + 2.5: + print(f"{percent}%") + prev_percent = percent + if not tracks: + print("No new tracks found!") + return + + cols = ', '.join(tracks[0].keys()) + vals = ', '.join(['$'+str(i) for i in range(1, len(tracks[0])+1)]) + tracks_data = [list(track.values()) for track in tracks] + + async with db_pool.acquire() as conn: + p = f"INSERT INTO track ({cols}) VALUES ({vals}) " + p += "ON CONFLICT(filepath) DO UPDATE SET " + for col in tracks[0].keys(): + p += col + " = EXCLUDED." + col + ", " + p = p[:-2] + + cur = await conn.prepare(p) + await cur.executemany(tracks_data) + print("Done") + +if __name__ == "__main__": + asyncio.run(build_library(config.music_dir)) diff --git a/pyrite.sql b/pyrite.sql new file mode 100644 index 0000000..f4e7607 --- /dev/null +++ b/pyrite.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS track ( + filepath TEXT PRIMARY KEY, + artist TEXT, + albumartist TEXT, + album TEXT, + title TEXT, + date TEXT, + discnumber TEXT, + tracknumber TEXT, + genre TEXT, + duration FLOAT, + last_modified FLOAT +)