# Copyright 2022 (c) Anna Schumaker """A custom Gio.ListModel for working with libraries.""" import pathlib import sqlite3 from gi.repository import GObject from .. import path from . import idle from . import playlist from . import tagger from . import tracks class Library(playlist.Playlist): """Our custom Library with path and enabled properties.""" libraryid = GObject.Property(type=int) path = GObject.Property(type=GObject.TYPE_PYOBJECT) enabled = GObject.Property(type=bool, default=True) deleting = GObject.Property(type=bool, default=False) queue = GObject.Property(type=idle.Queue) readdir = GObject.Property(type=GObject.TYPE_PYOBJECT) tagger = GObject.Property(type=GObject.TYPE_PYOBJECT) online = GObject.Property(type=bool, default=False) def __init__(self, **kwargs): """Initialize our Library object.""" super().__init__(queue=idle.Queue(), **kwargs) self.scan() def __check_trackid(self, trackid: int) -> bool: track = self.table.sql.tracks.rows.get(trackid) if track is not None and not track.path.exists(): tagger.untag_track(self.table.sql, track) track.delete() return True def __queue_delete(self) -> bool: self.table.delete(self) self.table.sql.tracks.load() return True def __queue_tracks(self) -> bool: if (files := self.readdir.poll_result()) is None: self.__stop_thread("readdir") self.queue.push(self.__stop_thread, "tagger") return True self.queue.push_many(self.__tag_track, [(f,) for f in files]) return False def __reload_playlist_tracks(self, playlist: playlist.Playlist) -> bool: playlist.reload_tracks(idle=False) return True def __tag_track(self, path: pathlib.Path) -> bool: if self.tagger.ready.is_set(): result = self.tagger.get_result(db=self.table.sql, library=self) if result is None: track = self.table.sql.tracks.lookup(self, path=path) mtime = track.mtime if track else None self.tagger.tag_file(path, mtime=mtime) else: return True return False def __scan_library(self) -> bool: self.readdir = path.readdir_async(self.path) if self.readdir is not None: self.online = True self.load_tracks() self.queue.push_many(self.__check_trackid, [(tid,) for tid in self.tracks.trackids]) self.queue.push(self.__queue_tracks) self.tagger = tagger.Thread() return True def __stop_thread(self, thread_name: str) -> bool: if (thread := self.get_property(thread_name)) is not None: thread.stop() self.set_property(thread_name, None) return True def do_update(self, column: str) -> bool: """Update a Library playlist.""" match column: case "readdir" | "tagger": pass case "online": self.table.notify_online(self) case _: return super().do_update(column) return True def delete(self) -> bool: """Delete this Library.""" if self.deleting is False: self.stop() self.deleting = True self.table.sql.tracks.clear() for tbl in self.table.sql.playlist_tables(): if tbl is not self: self.queue.push_many(self.__reload_playlist_tracks, [(plist,) for plist in tbl.store]) self.queue.push(self.__queue_delete) return True return False def scan(self) -> None: """Scan the Library.""" if not self.queue.running: self.queue.push(self.__scan_library) def stop(self) -> None: """Stop this Library's background work.""" self.__stop_thread("readdir") self.__stop_thread("tagger") self.queue.cancel() @property def primary_key(self) -> int: """Get this library's primary key.""" return self.libraryid class Table(playlist.Table): """Our Library ListModel.""" def __init__(self, sql: GObject.TYPE_PYOBJECT, **kwargs): """Initialize the Libraries Table.""" super().__init__(sql=sql, system_tracks=False, **kwargs) def do_add_track(self, library: Library, track: tracks.Track) -> bool: """Verify adding a Track to a Library playlist.""" return track.get_library() == library def do_construct(self, **kwargs) -> Library: """Construct a new library.""" return Library(**kwargs) def do_remove_track(self, library: Library, track: tracks.Track) -> bool: """Verify removing a Track from a Library playlist.""" return True def do_sql_delete(self, library: Library) -> sqlite3.Cursor: """Delete a library.""" return self.sql("DELETE FROM libraries WHERE libraryid=?", library.libraryid) def do_sql_insert(self, path: pathlib.Path) -> sqlite3.Cursor: """Create a new library.""" if cur := self.sql("INSERT INTO libraries (path) VALUES (?)", path): return self.sql("SELECT * FROM libraries_view WHERE libraryid=?", cur.lastrowid) def do_sql_glob(self, glob: str) -> sqlite3.Cursor: """Search for libraries matching the search text.""" return self.sql("""SELECT libraryid FROM libraries_view WHERE name GLOB ?""", glob) def do_sql_select_all(self) -> sqlite3.Cursor: """Load libraries from the database.""" return self.sql("SELECT * FROM libraries_view") def do_sql_select_one(self, path: pathlib.Path) -> sqlite3.Cursor: """Look up a library by path.""" return self.sql("SELECT libraryid FROM libraries WHERE path=?", path) def do_sql_select_trackids(self, library: Library) -> sqlite3.Cursor: """Load a Library's Tracks from the database.""" return self.sql("""SELECT trackid FROM library_tracks_view WHERE libraryid=?""", library.libraryid) def do_sql_update(self, library: Library, column: str, newval) -> bool: """Update a Library playlist.""" if column == "enabled" and self.sql.playlists.collection: self.sql.playlists.collection.reload_tracks(idle=True) return self.sql(f"UPDATE libraries SET {column}=? WHERE rowid=?", newval, library.libraryid) def notify_online(self, library: Library) -> None: """Notify that a library's online status has changed.""" if not library.online or self.loaded: self.emit("library-online", library) def stop(self) -> None: """Stop any background work.""" for library in self.store: library.stop() super().stop() @GObject.Signal(arg_types=(Library,)) def library_online(self, library: Library) -> None: """Signal that a library online status has changed."""