192 lines
6.9 KiB
Python
192 lines
6.9 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():
|
|
(file, tags) = self.tagger.get_result(self.table.sql, self)
|
|
if file is None:
|
|
track = self.table.sql.tracks.lookup(self, path=path)
|
|
self.tagger.tag_file(path, track.mtime if track else None)
|
|
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."""
|