WIP interactive shell
This commit is contained in:
parent
10122063f3
commit
26b7fde93d
|
@ -10,7 +10,7 @@ from rich import print
|
||||||
import rich.table
|
import rich.table
|
||||||
|
|
||||||
from groove import webserver
|
from groove import webserver
|
||||||
from groove.shell import start_shell
|
from groove.shell import interactive_shell
|
||||||
from groove.playlist import Playlist
|
from groove.playlist import Playlist
|
||||||
from groove import db
|
from groove import db
|
||||||
from groove.db.manager import database_manager
|
from groove.db.manager import database_manager
|
||||||
|
@ -156,7 +156,7 @@ def scan(
|
||||||
@app.command()
|
@app.command()
|
||||||
def shell():
|
def shell():
|
||||||
initialize()
|
initialize()
|
||||||
start_shell()
|
interactive_shell.start()
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from prompt_toolkit.completion import Completion, FuzzyCompleter
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from . import metadata
|
from . import metadata
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzyTableCompleter(FuzzyCompleter):
|
||||||
|
|
||||||
|
def __init__(self, table, column, formatter, session):
|
||||||
|
self._table = table
|
||||||
|
self._column = column
|
||||||
|
self._formatter = formatter
|
||||||
|
self._session = session
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
line = document.current_line_before_cursor
|
||||||
|
query = self._session.query(self._table).filter(self._column.ilike(f"%{line}%"))
|
||||||
|
for row in query.all():
|
||||||
|
yield Completion(
|
||||||
|
self._formatter(row),
|
||||||
|
start_position=-len(line)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
"""
|
"""
|
||||||
A context manager for working with sqllite database.
|
A context manager for working with sqllite database.
|
||||||
|
@ -18,7 +37,7 @@ class DatabaseManager:
|
||||||
@property
|
@property
|
||||||
def engine(self):
|
def engine(self):
|
||||||
if not self._engine:
|
if not self._engine:
|
||||||
self._engine = create_engine(f"sqlite:///{os.environ.get('DATABASE_PATH')}", future=True)
|
self._engine = create_engine(f"sqlite:///{os.environ.get('DATABASE_PATH')}?check_same_thread=False", future=True)
|
||||||
return self._engine
|
return self._engine
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -31,6 +50,9 @@ class DatabaseManager:
|
||||||
def import_from_filesystem(self):
|
def import_from_filesystem(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def fuzzy_table_completer(self, table, column, formatter):
|
||||||
|
return FuzzyTableCompleter(table, column, formatter, session=self.session)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
metadata.create_all(bind=self.engine)
|
metadata.create_all(bind=self.engine)
|
||||||
return self
|
return self
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
from prompt_toolkit import prompt
|
|
||||||
from prompt_toolkit.completion import Completion, FuzzyCompleter
|
|
||||||
from slugify import slugify
|
|
||||||
from sqlalchemy import func
|
|
||||||
from rich import print
|
|
||||||
|
|
||||||
from groove import db
|
|
||||||
from groove.playlist import Playlist
|
|
||||||
|
|
||||||
|
|
||||||
class FuzzyTableCompleter(FuzzyCompleter):
|
|
||||||
|
|
||||||
def __init__(self, table, column, formatter, session):
|
|
||||||
super(FuzzyTableCompleter).__init__()
|
|
||||||
self._table = table
|
|
||||||
self._column = column
|
|
||||||
self._formatter = formatter
|
|
||||||
self._session = session
|
|
||||||
|
|
||||||
def get_completions(self, document, complete_event):
|
|
||||||
word = document.get_word_before_cursor()
|
|
||||||
query = self._session.query(self._table).filter(self._column.ilike(f"%{word}%"))
|
|
||||||
for row in query.all():
|
|
||||||
yield Completion(
|
|
||||||
self._formatter(row),
|
|
||||||
start_position=-len(word)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
|
||||||
def __init__(self, processor):
|
|
||||||
self._processor = processor
|
|
||||||
|
|
||||||
def handle(self, *parts):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class help(Command):
|
|
||||||
"""Display the help documentation."""
|
|
||||||
def handle(self, *parts):
|
|
||||||
print("Available commands:")
|
|
||||||
for handler in Command.__subclasses__():
|
|
||||||
print(f"{handler.__name__}: {handler.__doc__}")
|
|
||||||
|
|
||||||
|
|
||||||
class add(Command):
|
|
||||||
"""Add a track to the current playlist."""
|
|
||||||
def handle(self, *parts):
|
|
||||||
if not self._processor.playlist:
|
|
||||||
print("Please select a playlist first, using the 'playlist' command.")
|
|
||||||
return
|
|
||||||
text = prompt(
|
|
||||||
'Add which track? > ',
|
|
||||||
completer=FuzzyTableCompleter(db.track, db.track.c.relpath, self._track_to_string, self._processor.session),
|
|
||||||
complete_in_thread=True, complete_while_typing=True
|
|
||||||
)
|
|
||||||
return text
|
|
||||||
|
|
||||||
def _track_to_string(row):
|
|
||||||
return f"{row.artist} - {row.title}"
|
|
||||||
|
|
||||||
|
|
||||||
class list(Command):
|
|
||||||
"""Display the current playlist."""
|
|
||||||
def handle(self, *parts):
|
|
||||||
if not self._processor.playlist:
|
|
||||||
print("Please select a playlist first, using the 'playlist' command.")
|
|
||||||
return
|
|
||||||
print(self._processor.playlist.as_dict)
|
|
||||||
|
|
||||||
|
|
||||||
class stats(Command):
|
|
||||||
"""Display database statistics."""
|
|
||||||
def handle(self, *parts):
|
|
||||||
sess = self._processor.session
|
|
||||||
playlists = sess.query(func.count(db.playlist.c.id)).scalar()
|
|
||||||
entries = sess.query(func.count(db.entry.c.track)).scalar()
|
|
||||||
tracks = sess.query(func.count(db.track.c.relpath)).scalar()
|
|
||||||
print(f"Database contains {playlists} playlists with a total of {entries} entries, from {tracks} known tracks.")
|
|
||||||
|
|
||||||
|
|
||||||
class quit(Command):
|
|
||||||
"""Exit the interactive shell."""
|
|
||||||
def handle(self, *parts):
|
|
||||||
raise SystemExit()
|
|
||||||
|
|
||||||
|
|
||||||
class playlist(Command):
|
|
||||||
"""Create or load a playlist."""
|
|
||||||
def handle(self, *parts):
|
|
||||||
name = ' '.join(parts)
|
|
||||||
slug = slugify(name)
|
|
||||||
self._processor.playlist = Playlist(
|
|
||||||
slug=slug,
|
|
||||||
name=name,
|
|
||||||
session=self._processor.session,
|
|
||||||
create_if_not_exists=True
|
|
||||||
)
|
|
||||||
self._processor.prompt = slug
|
|
||||||
print(f"Loaded playlist with slug {self._processor.playlist.record.slug}.")
|
|
||||||
|
|
||||||
|
|
||||||
def load(processor):
|
|
||||||
for handler in Command.__subclasses__():
|
|
||||||
yield handler.__name__, handler(processor)
|
|
|
@ -4,7 +4,9 @@ from sqlalchemy.orm.session import Session
|
||||||
from sqlalchemy.engine.row import Row
|
from sqlalchemy.engine.row import Row
|
||||||
from sqlalchemy.exc import NoResultFound, MultipleResultsFound
|
from sqlalchemy.exc import NoResultFound, MultipleResultsFound
|
||||||
from typing import Union, List
|
from typing import Union, List
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Playlist:
|
class Playlist:
|
||||||
|
@ -32,6 +34,14 @@ class Playlist:
|
||||||
"""
|
"""
|
||||||
return self.record is not None
|
return self.record is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def summary(self):
|
||||||
|
return ' :: '.join([
|
||||||
|
f"[ {self.record.id} ]",
|
||||||
|
self.record.name,
|
||||||
|
f"http://{os.environ['HOST']}/{self.slug}",
|
||||||
|
])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slug(self) -> Union[str, None]:
|
def slug(self) -> Union[str, None]:
|
||||||
return self._slug
|
return self._slug
|
||||||
|
@ -50,6 +60,7 @@ class Playlist:
|
||||||
self._record = self.session.query(db.playlist).filter(db.playlist.c.slug == self.slug).one()
|
self._record = self.session.query(db.playlist).filter(db.playlist.c.slug == self.slug).one()
|
||||||
logging.debug(f"Retrieved playlist {self._record.id}")
|
logging.debug(f"Retrieved playlist {self._record.id}")
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
|
logging.debug(f"Could not find a playlist with slug {self.slug}.")
|
||||||
pass
|
pass
|
||||||
if not self._record and self._create_if_not_exists:
|
if not self._record and self._create_if_not_exists:
|
||||||
self._record = self._create()
|
self._record = self._create()
|
||||||
|
@ -62,7 +73,7 @@ class Playlist:
|
||||||
"""
|
"""
|
||||||
Cache the list of entries on this playlist and return it.
|
Cache the list of entries on this playlist and return it.
|
||||||
"""
|
"""
|
||||||
if not self._entries and self.record:
|
if self.record and not self._entries:
|
||||||
query = self.session.query(
|
query = self.session.query(
|
||||||
db.entry,
|
db.entry,
|
||||||
db.track
|
db.track
|
||||||
|
@ -72,8 +83,11 @@ class Playlist:
|
||||||
db.entry.c.playlist_id == db.playlist.c.id
|
db.entry.c.playlist_id == db.playlist.c.id
|
||||||
).filter(
|
).filter(
|
||||||
db.entry.c.track_id == db.track.c.id
|
db.entry.c.track_id == db.track.c.id
|
||||||
|
).order_by(
|
||||||
|
db.entry.c.track
|
||||||
)
|
)
|
||||||
self._entries = db.windowed_query(query, db.entry.c.track_id, 1000)
|
# self._entries = list(db.windowed_query(query, db.entry.c.track_id, 1000))
|
||||||
|
self._entries = query.all()
|
||||||
return self._entries
|
return self._entries
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -87,6 +101,13 @@ class Playlist:
|
||||||
playlist['entries'] = [dict(entry) for entry in self.entries]
|
playlist['entries'] = [dict(entry) for entry in self.entries]
|
||||||
return playlist
|
return playlist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_string(self) -> str:
|
||||||
|
text = f"{self.summary}\n"
|
||||||
|
for entry in self.entries:
|
||||||
|
text += f" - {entry.track} {entry.artist} - {entry.title}\n"
|
||||||
|
return text
|
||||||
|
|
||||||
def add(self, paths: List[str]) -> int:
|
def add(self, paths: List[str]) -> int:
|
||||||
"""
|
"""
|
||||||
Add entries to the playlist. Each path should match one and only one track in the database (case-insensitive).
|
Add entries to the playlist. Each path should match one and only one track in the database (case-insensitive).
|
||||||
|
@ -100,7 +121,7 @@ class Playlist:
|
||||||
"""
|
"""
|
||||||
logging.debug(f"Attempting to add tracks matching: {paths}")
|
logging.debug(f"Attempting to add tracks matching: {paths}")
|
||||||
try:
|
try:
|
||||||
return self._create_entries(self._get_tracks_by_path(paths))
|
return self.create_entries(self._get_tracks_by_path(paths))
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
logging.error("One or more of the specified paths do not match any tracks in the database.")
|
logging.error("One or more of the specified paths do not match any tracks in the database.")
|
||||||
return 0
|
return 0
|
||||||
|
@ -133,7 +154,7 @@ class Playlist:
|
||||||
"""
|
"""
|
||||||
return [self.session.query(db.track).filter(db.track.c.relpath.ilike(f"%{path}%")).one() for path in paths]
|
return [self.session.query(db.track).filter(db.track.c.relpath.ilike(f"%{path}%")).one() for path in paths]
|
||||||
|
|
||||||
def _create_entries(self, tracks: List[Row]) -> int:
|
def create_entries(self, tracks: List[Row]) -> int:
|
||||||
"""
|
"""
|
||||||
Append a list of tracks to a playlist by populating the entries table with records referencing the playlist and
|
Append a list of tracks to a playlist by populating the entries table with records referencing the playlist and
|
||||||
the specified tracks.
|
the specified tracks.
|
||||||
|
@ -173,4 +194,4 @@ class Playlist:
|
||||||
return pl
|
return pl
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self.as_dict)
|
return self.as_string
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
from prompt_toolkit import prompt
|
|
||||||
from prompt_toolkit.completion import Completer, Completion
|
|
||||||
from rich import print
|
|
||||||
|
|
||||||
from groove import db
|
|
||||||
from groove import handlers
|
|
||||||
from groove.db.manager import database_manager
|
|
||||||
from groove.playlist import Playlist
|
|
||||||
|
|
||||||
|
|
||||||
class CommandProcessor(Completer):
|
|
||||||
|
|
||||||
prompt = ''
|
|
||||||
|
|
||||||
def __init__(self, session):
|
|
||||||
super(CommandProcessor, self).__init__()
|
|
||||||
self._session = session
|
|
||||||
self.playlist = None
|
|
||||||
self._handlers = dict(handlers.load(self))
|
|
||||||
print(f"Loaded command handlers: {' '.join(self._handlers.keys())}")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session(self):
|
|
||||||
return self._session
|
|
||||||
|
|
||||||
def get_completions(self, document, complete_event):
|
|
||||||
word = document.get_word_before_cursor()
|
|
||||||
found = False
|
|
||||||
for command_name in self._handlers.keys():
|
|
||||||
if word in command_name:
|
|
||||||
yield Completion(command_name, start_position=-len(word))
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
def _formatter(row):
|
|
||||||
self.playlist = Playlist.from_row(row, self._session)
|
|
||||||
return f'playlist {self.playlist.record.name}'
|
|
||||||
completer = handlers.FuzzyTableCompleter(
|
|
||||||
db.playlist,
|
|
||||||
db.playlist.c.name,
|
|
||||||
_formatter,
|
|
||||||
self._session
|
|
||||||
)
|
|
||||||
for res in completer.get_completions(document, complete_event):
|
|
||||||
yield res
|
|
||||||
|
|
||||||
def process(self, cmd):
|
|
||||||
if not cmd:
|
|
||||||
return
|
|
||||||
cmd, *parts = cmd.split()
|
|
||||||
if cmd in self._handlers:
|
|
||||||
self._handlers[cmd].handle(*parts)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
cmd = ''
|
|
||||||
while True:
|
|
||||||
cmd = prompt(f'{self.prompt} > ', completer=self)
|
|
||||||
self.process(cmd)
|
|
||||||
if not cmd:
|
|
||||||
self.cmd_exit()
|
|
||||||
|
|
||||||
|
|
||||||
def start_shell():
|
|
||||||
print("Groove On Demand interactive shell.")
|
|
||||||
with database_manager() as manager:
|
|
||||||
CommandProcessor(manager.session).start()
|
|
7
groove/shell/__init__.py
Normal file
7
groove/shell/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
from .quit import quit
|
||||||
|
from .help import help
|
||||||
|
from .browse import browse
|
||||||
|
from .stats import stats
|
||||||
|
from .playlist import _playlist
|
||||||
|
from .create import create
|
74
groove/shell/base.py
Normal file
74
groove/shell/base.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
from prompt_toolkit import prompt
|
||||||
|
from prompt_toolkit.completion import Completer, Completion
|
||||||
|
|
||||||
|
|
||||||
|
class BasePrompt(Completer):
|
||||||
|
|
||||||
|
def __init__(self, manager=None, parent=None):
|
||||||
|
super(BasePrompt, self).__init__()
|
||||||
|
|
||||||
|
if (not manager and not parent):
|
||||||
|
raise RuntimeError("Must define either a database manager or a parent object.")
|
||||||
|
|
||||||
|
self._prompt = ''
|
||||||
|
self._values = []
|
||||||
|
self._parent = parent
|
||||||
|
self._manager = manager
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usage(self):
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help_text(self):
|
||||||
|
return self.__doc__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manager(self):
|
||||||
|
if self._manager:
|
||||||
|
return self._manager
|
||||||
|
elif self._parent:
|
||||||
|
return self._parent.manager
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prompt(self):
|
||||||
|
return self._prompt
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self):
|
||||||
|
return self._values
|
||||||
|
|
||||||
|
def get_completions(self, document, complete_event):
|
||||||
|
word = document.get_word_before_cursor()
|
||||||
|
found = False
|
||||||
|
for value in self.values:
|
||||||
|
if word in value:
|
||||||
|
found = True
|
||||||
|
yield Completion(value, start_position=-len(word))
|
||||||
|
if not found:
|
||||||
|
try:
|
||||||
|
for result in self.default_completer(document, complete_event):
|
||||||
|
yield result
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def start(self, cmd=''):
|
||||||
|
while True:
|
||||||
|
if not cmd:
|
||||||
|
cmd = prompt(f'{self.prompt} ', completer=self)
|
||||||
|
if not cmd:
|
||||||
|
return
|
||||||
|
cmd, *parts = cmd.split()
|
||||||
|
if not self.process(cmd, *parts):
|
||||||
|
return
|
||||||
|
cmd = ''
|
||||||
|
|
||||||
|
def default_completer(self, document, complete_event):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
raise NotImplementedError()
|
26
groove/shell/browse.py
Normal file
26
groove/shell/browse.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
|
||||||
|
from rich.table import Table, Column
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from groove import db
|
||||||
|
from groove.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
class browse(BasePrompt):
|
||||||
|
"""Browse the playlists."""
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
count = self.parent.manager.session.query(func.count(db.playlist.c.id)).scalar()
|
||||||
|
print(f"Displaying {count} playlists:")
|
||||||
|
query = self.parent.manager.session.query(db.playlist)
|
||||||
|
table = Table(
|
||||||
|
*[Column(k.name.title()) for k in db.playlist.columns]
|
||||||
|
)
|
||||||
|
for row in db.windowed_query(query, db.playlist.c.id, 1000):
|
||||||
|
columns = tuple(Playlist.from_row(row, self.manager.session).record)[0:-1]
|
||||||
|
table.add_row(*[str(col) for col in columns])
|
||||||
|
print()
|
||||||
|
print(table)
|
||||||
|
print()
|
27
groove/shell/create.py
Normal file
27
groove/shell/create.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
from groove.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
class create(BasePrompt):
|
||||||
|
"""Create a new playlist."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usage(self):
|
||||||
|
return "create PLAYLIST_NAME"
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
name = ' '.join(parts)
|
||||||
|
if not name:
|
||||||
|
print(f"Usage: {self.usage}")
|
||||||
|
return
|
||||||
|
slug = slugify(name)
|
||||||
|
self.parent._playlist = Playlist(
|
||||||
|
slug=slug,
|
||||||
|
name=name,
|
||||||
|
session=self.manager.session,
|
||||||
|
create_if_not_exists=True
|
||||||
|
)
|
||||||
|
return self.parent.commands['_playlist'].start()
|
26
groove/shell/help.py
Normal file
26
groove/shell/help.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
|
||||||
|
from rich import print
|
||||||
|
import rich.table
|
||||||
|
|
||||||
|
|
||||||
|
class help(BasePrompt):
|
||||||
|
"""Display help documentation."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usage(self):
|
||||||
|
return "help [COMMAND]"
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
if not parts:
|
||||||
|
print("Available Commands:")
|
||||||
|
table = rich.table.Table()
|
||||||
|
table.add_column("Command", style="yellow", no_wrap=True)
|
||||||
|
table.add_column("Description")
|
||||||
|
for name, obj in self.parent.commands.items():
|
||||||
|
if name.startswith('_'):
|
||||||
|
continue
|
||||||
|
table.add_row(getattr(obj, 'usage', name), obj.__doc__)
|
||||||
|
print(table)
|
||||||
|
else:
|
||||||
|
print(f"Help for {parts}:")
|
68
groove/shell/interactive_shell.py
Normal file
68
groove/shell/interactive_shell.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from rich import print
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
from groove.db.manager import database_manager
|
||||||
|
from groove.shell.base import BasePrompt
|
||||||
|
from groove import db
|
||||||
|
from groove.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
class CommandPrompt(BasePrompt):
|
||||||
|
|
||||||
|
def __init__(self, manager):
|
||||||
|
super().__init__(manager=manager)
|
||||||
|
self._playlist = None
|
||||||
|
self._prompt = "Groove on Demand interactive shell. Try 'help' for help.\ngroove>"
|
||||||
|
self._completer = None
|
||||||
|
self._commands = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playlist(self):
|
||||||
|
return self._playlist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def commands(self):
|
||||||
|
if not self._commands:
|
||||||
|
self._commands = {}
|
||||||
|
for cmd in BasePrompt.__subclasses__():
|
||||||
|
if cmd.__name__ == self.__class__.__name__:
|
||||||
|
continue
|
||||||
|
self._commands[cmd.__name__] = cmd(manager=self.manager, parent=self)
|
||||||
|
return self._commands
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self):
|
||||||
|
return [k for k in self.commands.keys() if not k.startswith('_')]
|
||||||
|
|
||||||
|
def default_completer(self, document, complete_event):
|
||||||
|
def _formatter(row):
|
||||||
|
self._playlist = Playlist.from_row(row, self.manager)
|
||||||
|
return self.playlist.record.name
|
||||||
|
return self.manager.fuzzy_table_completer(
|
||||||
|
db.playlist,
|
||||||
|
db.playlist.c.name,
|
||||||
|
_formatter
|
||||||
|
).get_completions(document, complete_event)
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
name = cmd + ' ' + ' '.join(parts)
|
||||||
|
if cmd in self.commands:
|
||||||
|
self.commands[cmd].start(name)
|
||||||
|
elif not parts:
|
||||||
|
print(f"Command not understood: {cmd}")
|
||||||
|
else:
|
||||||
|
slug = slugify(name)
|
||||||
|
self._playlist = Playlist(
|
||||||
|
slug=slug,
|
||||||
|
name=name,
|
||||||
|
session=self.manager.session,
|
||||||
|
create_if_not_exists=False
|
||||||
|
)
|
||||||
|
self.commands['_playlist'].start()
|
||||||
|
self._playlist = None
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
with database_manager() as manager:
|
||||||
|
CommandPrompt(manager).start()
|
86
groove/shell/playlist.py
Normal file
86
groove/shell/playlist.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
|
||||||
|
from prompt_toolkit import prompt
|
||||||
|
from rich import print
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
|
|
||||||
|
from groove import db
|
||||||
|
|
||||||
|
|
||||||
|
class _playlist(BasePrompt):
|
||||||
|
|
||||||
|
def __init__(self, parent, manager=None):
|
||||||
|
super().__init__(manager=manager, parent=parent)
|
||||||
|
self._parent = parent
|
||||||
|
self._prompt = ''
|
||||||
|
self._commands = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prompt(self):
|
||||||
|
return f"{self.parent.playlist}\n{self.parent.playlist.slug}> "
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self):
|
||||||
|
return self.commands.keys()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def commands(self):
|
||||||
|
if not self._commands:
|
||||||
|
self._commands = {
|
||||||
|
'show': self.show,
|
||||||
|
'delete': self.delete,
|
||||||
|
'add': self.add,
|
||||||
|
}
|
||||||
|
return self._commands
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
res = True
|
||||||
|
if cmd in self.commands:
|
||||||
|
res = self.commands[cmd](parts)
|
||||||
|
else:
|
||||||
|
print(f"Command not understood: {cmd}")
|
||||||
|
return res is True
|
||||||
|
|
||||||
|
def show(self, parts):
|
||||||
|
print(self.parent.playlist)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add(self, parts):
|
||||||
|
print("Add tracks one at a time by title. ENTER to finish.")
|
||||||
|
while True:
|
||||||
|
text = prompt(
|
||||||
|
' ? ',
|
||||||
|
completer=self.manager.fuzzy_table_completer(
|
||||||
|
db.track,
|
||||||
|
db.track.c.relpath,
|
||||||
|
lambda row: row.relpath
|
||||||
|
),
|
||||||
|
complete_in_thread=True, complete_while_typing=True
|
||||||
|
)
|
||||||
|
if not text:
|
||||||
|
return True
|
||||||
|
self._add_track(text)
|
||||||
|
|
||||||
|
def _add_track(self, text):
|
||||||
|
sess = self.parent.manager.session
|
||||||
|
try:
|
||||||
|
track = sess.query(db.track).filter(db.track.c.relpath == text).one()
|
||||||
|
self.parent.playlist.create_entries([track])
|
||||||
|
except NoResultFound:
|
||||||
|
print("No match for '{text}'")
|
||||||
|
return
|
||||||
|
return text
|
||||||
|
|
||||||
|
def delete(self, parts):
|
||||||
|
res = prompt(
|
||||||
|
'Type DELETE to permanently delete the playlist '
|
||||||
|
f'"{self.parent.playlist.record.name}".\nDELETE {self.prompt}'
|
||||||
|
)
|
||||||
|
if res != 'DELETE':
|
||||||
|
print("Delete aborted. No changes have been made.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.parent.playlist.delete()
|
||||||
|
print("Deleted the playlist.")
|
||||||
|
self.parent._playlist = None
|
||||||
|
return False
|
8
groove/shell/quit.py
Normal file
8
groove/shell/quit.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
|
||||||
|
|
||||||
|
class quit(BasePrompt):
|
||||||
|
"""Exit the interactive shell."""
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
raise SystemExit()
|
16
groove/shell/stats.py
Normal file
16
groove/shell/stats.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from .base import BasePrompt
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from groove import db
|
||||||
|
|
||||||
|
|
||||||
|
class stats(BasePrompt):
|
||||||
|
|
||||||
|
def process(self, cmd, *parts):
|
||||||
|
sess = self.parent.manager.session
|
||||||
|
playlists = sess.query(func.count(db.playlist.c.id)).scalar()
|
||||||
|
entries = sess.query(func.count(db.entry.c.track)).scalar()
|
||||||
|
tracks = sess.query(func.count(db.track.c.relpath)).scalar()
|
||||||
|
print(f"Database contains {playlists} playlists with a total of {entries} entries, from {tracks} known tracks.")
|
Loading…
Reference in New Issue
Block a user