emmental/emmental/db/libraries.py

193 lines
7.0 KiB
Python

# 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."""