emmental/emmental/db/playlist.py

310 lines
12 KiB
Python

# Copyright 2022 (c) Anna Schumaker
"""A customized Gio.ListStore for tracking Playlist GObjects."""
import sqlite3
import typing
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .tracks import Track, TrackidSet
from .. import format
from . import table
class Playlist(table.Row):
"""Our shared Playlist Row object."""
propertyid = GObject.Property(type=int)
name = GObject.Property(type=str)
active = GObject.Property(type=bool, default=False)
loop = GObject.Property(type=str, default="None")
shuffle = GObject.Property(type=bool, default=False)
sort_order = GObject.Property(type=str)
tracks = GObject.Property(type=TrackidSet)
n_tracks = GObject.Property(type=int)
user_tracks = GObject.Property(type=bool, default=False)
tracks_loaded = GObject.Property(type=bool, default=False)
tracks_movable = GObject.Property(type=bool, default=False)
current_trackid = GObject.Property(type=int)
child_set = GObject.Property(type=table.TableSubset)
children = GObject.Property(type=Gtk.FilterListModel)
def __init__(self, table: Gio.ListModel, propertyid: int,
name: str, current_trackid: int | None = 0, **kwargs):
"""Initialize a Playlist object."""
current_trackid = 0 if current_trackid is None else current_trackid
super().__init__(table=table, propertyid=propertyid, name=name,
current_trackid=current_trackid,
tracks=TrackidSet(), **kwargs)
self.tracks.bind_property("n-trackids", self, "n-tracks")
def __add_track(self, track: Track) -> bool:
self.tracks.add_track(track)
return True
def __remove_track(self, track: Track) -> bool:
self.tracks.remove_track(track)
self.table.remove_track(self, track)
return True
def add_children(self, child_table: table.Table, child_keys: set) -> None:
"""Create a FilterListModel for this playlist's children."""
self.child_set = table.TableSubset(child_table, keys=child_keys)
self.children = Gtk.FilterListModel.new(self.child_set,
child_table.get_filter())
def do_update(self, column: str) -> bool:
"""Update a Playlist object."""
match column:
case "propertyid" | "name" | "n-tracks" | "child-set" | \
"children" | "user-tracks" | "tracks-loaded" | \
"tracks-movable": pass
case _: return super().do_update(column)
return True
def add_child(self, child: typing.Self) -> None:
"""Add a child Playlist to this Playlist."""
self.child_set.add_row(child)
if self.child_set.keyset.n_keys == 1:
self.table.refilter(Gtk.FilterChange.LESS_STRICT)
def add_track(self, track: Track, *, idle: bool = False) -> None:
"""Add a Track to this Playlist."""
if self.table.add_track(self, track):
self.table.queue.push(self.__add_track, track, now=not idle)
def get_track_order(self) -> dict[int, int]:
"""Get a dictionary mapping for trackid -> sorted position."""
return self.table.get_track_order(self)
def has_child(self, child: typing.Self) -> bool:
"""Check if this Playlist has a specific child Playlist."""
return child in self.child_set
def has_track(self, track: Track) -> bool:
"""Check if a Track is on this Playlist."""
return track in self.tracks
def load_tracks(self) -> bool:
"""Load this Playlist's Tracks (if they haven't been loaded yet)."""
if not self.tracks_loaded:
self.tracks.trackids = self.table.get_trackids(self)
self.tracks_loaded = True
return True
def move_track_down(self, track: Track) -> bool:
"""Move a track down in the sort order."""
return self.table.move_track_down(self, track)
def move_track_up(self, track: Track) -> bool:
"""Move a track up in the sort order."""
return self.table.move_track_up(self, track)
def reload_tracks(self, *, idle: bool = False) -> None:
"""Load this Playlist's Tracks."""
self.tracks_loaded = False
self.table.queue.push(self.load_tracks, now=not idle)
def remove_child(self, child: typing.Self) -> None:
"""Remove a child Playlist from this Playlist."""
self.child_set.remove_row(child)
if self.child_set.keyset.n_keys == 0:
self.table.refilter(Gtk.FilterChange.MORE_STRICT)
def remove_track(self, track: table.Row, *, idle: bool = False) -> None:
"""Remove a Track from this Playlist."""
self.table.queue.push(self.__remove_track, track, now=not idle)
def rename(self, new_name: str) -> bool:
"""Rename this playlist."""
return self.table.rename(self, new_name)
@GObject.Property(type=table.Row)
def parent(self) -> table.Row | None:
"""Get this playlist's parent playlist."""
return None
class Table(table.Table):
"""A table.Table with extra functionality for Playlists."""
active_playlist = GObject.Property(type=Playlist)
treemodel = GObject.Property(type=Gtk.TreeListModel)
autodelete = GObject.Property(type=bool, default=False)
system_tracks = GObject.Property(type=bool, default=True)
def __init__(self, sql: GObject.TYPE_PYOBJECT, **kwargs):
"""Initialize a Playlist Table."""
super().__init__(sql=sql, **kwargs)
self.treemodel = Gtk.TreeListModel.new(root=self,
passthrough=False,
autoexpand=False,
create_func=self.__create_tree)
def __do_autodelete(self, plist: Playlist) -> bool:
if plist.n_tracks == 0:
self.delete(plist)
return True
def __autodelete(self, plist: Playlist):
if self.autodelete:
self.queue.push(self.__do_autodelete, plist)
def __create_tree(self, plist: Playlist) -> Gtk.FilterListModel | None:
return plist.children
def __refilter(self, change_how: Gtk.FilterChange) -> bool:
self.get_filter().changed(change_how)
return True
def do_add_track(self, playlist: Playlist, track: Track) -> bool:
"""Add a Track to the Playlist."""
raise NotImplementedError
def do_get_sort_key(self, playlist: Playlist) -> tuple[str]:
"""Get a sort key for the requested Playlist."""
return format.sort_key(playlist.name)
def do_get_user_track_order(self, playlist: Playlist) -> dict[int, int]:
"""Get a mapping of sort keys for the tracks in this Playlist."""
raise NotImplementedError
def do_move_track_down(self, playlist: Playlist, track: Track) -> bool:
"""Move a track down in the sort order."""
raise NotImplementedError
def do_move_track_up(self, playlist: Playlist, track: Track) -> bool:
"""Move a track up in the sort order."""
raise NotImplementedError
def do_remove_track(self, playlist: Playlist, track: Track) -> bool:
"""Remove a Track from the Playlist."""
raise NotImplementedError
def do_sql_select_trackids(self, playlist: Playlist) -> sqlite3.Cursor:
"""Select the trackids that are in this Playlist."""
raise NotImplementedError
def add_system_track(self, playlist: Playlist, track: Track) -> bool:
"""Add a Track to a system Playlist."""
cur = self.sql("""INSERT INTO system_tracks (propertyid, trackid)
VALUES (?, ?)""", playlist.propertyid, track.trackid)
return cur and cur.rowcount == 1
def add_track(self, playlist: Playlist, track: Track) -> bool:
"""Add a Track to a Playlist."""
if track is None or track.get_library().deleting:
return False
if self.system_tracks:
return self.add_system_track(playlist, track)
return self.do_add_track(playlist, track)
def clear(self) -> None:
"""Clear the Table."""
self.active_playlist = None
super().clear()
def construct(self, propertyid: int, name: str, **kwargs) -> Playlist:
"""Construct a new Playlist object."""
res = super().construct(propertyid=propertyid, name=name, **kwargs)
if res.active:
self.sql.set_active_playlist(res)
res.reload_tracks(idle=True)
return res
def delete(self, playlist: Playlist) -> bool:
"""Delete a playlist from the database."""
if playlist.active:
self.sql.set_active_playlist(None)
return super().delete(playlist)
def get_sql_system_trackids(self, playlist: Playlist) -> sqlite3.Cursor:
"""Load a System Playlist's Tracks from the database."""
return self.sql("""SELECT trackid FROM system_tracks_view
WHERE propertyid=?""", playlist.propertyid)
def get_trackids(self, playlist: Playlist) -> set[int]:
"""Load a Playlist's Tracks from the database."""
if self.system_tracks:
cur = self.get_sql_system_trackids(playlist)
else:
cur = self.do_sql_select_trackids(playlist)
res = {row["trackid"] for row in cur.fetchall()}
self.__autodelete(playlist)
return res
def get_track_order(self, playlist: Playlist) -> dict[int, int]:
"""Get the track sort order for a playlist."""
if playlist.tracks_movable and playlist.sort_order == "user":
return self.do_get_user_track_order(playlist)
return self.sql.tracks.map_sort_order(playlist.sort_order)
def move_track_down(self, playlist: Playlist, track: Track) -> bool:
"""Move a track down in the playlist."""
if not playlist.tracks_movable:
return False
if res := self.do_move_track_down(playlist, track):
if playlist.sort_order != "user":
playlist.sort_order = "user"
return res
def move_track_up(self, playlist: Playlist, track: Track) -> bool:
"""Move a track up in the playlist."""
if not playlist.tracks_movable:
return False
if res := self.do_move_track_up(playlist, track):
if playlist.sort_order != "user":
playlist.sort_order = "user"
return res
def refilter(self, change_how: Gtk.FilterChange) -> None:
"""Schedule refiltering the Table."""
self.queue.cancel_task(self.__refilter)
self.queue.push(self.__refilter, change_how, first=True)
def remove_system_track(self, playlist: Playlist, track: Track) -> bool:
"""Remove a Track from a system Playlist."""
return self.sql("""DELETE FROM system_tracks
WHERE propertyid=? AND trackid=?""",
playlist.propertyid, track.trackid).rowcount == 1
def remove_track(self, playlist: Playlist, track: Track) -> bool:
"""Remove a Track from a Playlist."""
if self.system_tracks:
res = self.remove_system_track(playlist, track)
else:
res = self.do_remove_track(playlist, track)
self.__autodelete(playlist)
return res
def update(self, playlist: Playlist, column: str, newval) -> bool:
"""Update a Playlist in the Database."""
match column:
case "active" | "loop" | "shuffle" | \
"sort-order" | "current-trackid":
return self.update_playlist_property(playlist, column, newval)
case _:
return super().update(playlist, column, newval)
def update_playlist_property(self, playlist: Playlist,
column: str, newval) -> bool:
"""Update the playlists_common table."""
match column:
case "active":
self.active_playlist = playlist if playlist.active else None
case "current-trackid":
column = "current_trackid"
newval = None if newval == 0 else newval
case "sort-order":
column = "sort_order"
return self.sql(f"""UPDATE playlist_properties
SET {column}=? WHERE propertyid=?""",
newval, playlist.propertyid) is not None