dnd-music-console/croaker/cli.py

208 lines
4.9 KiB
Python
Raw Normal View History

2024-03-01 01:00:17 -08:00
import io
import logging
import os
import sys
from pathlib import Path
from textwrap import dedent
from typing import List, Optional
import typer
from dotenv import load_dotenv
from typing_extensions import Annotated
import croaker.path
from croaker import client, controller, server
from croaker.exceptions import ConfigurationError
from croaker.playlist import Playlist
SETUP_HELP = """
# Root directory for croaker configuration and logs. See also croaker --root.
CROAKER_ROOT=~/.dnd/croaker
## COMMAND AND CONTROL WEBSERVER
# Please make sure you set SECRET_KEY in your environment if you are running
# the command and control webserver. Clients do not need this.
SECRET_KEY=
# Where the record the webserver daemon's PID
PIDFILE=~/.dnd/croaker/croaker.pid
# Web interface configuration
HOST=127.0.0.1
PORT=8003
## CONTROLLER CLIENT
# The host and port to use when connecting to the websever.
CONTROLLER_HOST=127.0.0.1
CONTROLLER_PORT=8003
## MEDIA
# where to store playlist sources
PLAYLIST_ROOT=~/.dnd/croaker/playlists
# where to cache transcoded media files
CACHE_ROOT=~/.dnd/croaker/cache
# the kinds of files to add to playlists
MEDIA_GLOB=*.mp3,*.flac,*.m4a
# If defined, transcode media before streaming it, and cache it to disk. The
# strings INFILE and OUTFILE will be replaced with the media source file and
# the cached output location, respectively.
TRANSCODER=/usr/bin/ffmpeg -i INFILE '-hide_banner -loglevel error -codec:v copy -codec:a libmp3lame -q:a 2' OUTFILE
## LIQUIDSOAP AND ICECAST
# The liquidsoap executable
LIQUIDSOAP=/usr/bin/liquidsoap
# Icecast2 configuration for Liquidsoap
ICECAST_PASSWORD=
ICECAST_MOUNT=
ICECAST_HOST=
ICECAST_PORT=
ICECAST_URL=
"""
app = typer.Typer()
app_state = {}
@app.callback()
def main(
context: typer.Context,
root: Optional[Path] = typer.Option(
Path("~/.dnd/croaker"),
help="Path to the Croaker environment",
),
host: Optional[str] = typer.Option(
None,
help="bind address",
),
port: Optional[int] = typer.Option(
None,
help="bind port",
),
debug: Optional[bool] = typer.Option(None, help="Enable debugging output"),
):
load_dotenv(root.expanduser() / Path("defaults"))
load_dotenv(stream=io.StringIO(SETUP_HELP))
if host:
os.environ["HOST"] = host
if port:
os.environ["PORT"] = port
if debug is not None:
if debug:
os.environ["DEBUG"] = 1
else:
del os.environ["DEBUG"]
logging.basicConfig(
format="%(message)s",
level=logging.DEBUG if debug else logging.INFO,
)
try:
croaker.path.media_root()
croaker.path.cache_root()
except ConfigurationError as e:
sys.stderr.write(f"{e}\n\n{SETUP_HELP}")
sys.exit(1)
app_state["client"] = client.Client(
host=os.environ["CONTROLLER_HOST"],
port=os.environ["CONTROLLER_PORT"],
)
if not context.invoked_subcommand:
return play(context)
@app.command()
def setup(context: typer.Context):
"""
(Re)Initialize Croaker.
"""
sys.stderr.write("Interactive setup is not yet available. Sorry!\n")
print(dedent(SETUP_HELP))
@app.command()
def start(
context: typer.Context,
daemonize: bool = typer.Option(True, help="Daemonize the webserver."),
):
"""
Start the Croaker command and control webserver.
"""
controller.start()
if daemonize:
server.daemonize()
else:
server.start()
@app.command()
def stop():
"""
Terminate the webserver process and liquidsoap.
"""
controller.stop()
server.stop()
@app.command()
def play(
playlist: str = typer.Argument(
...,
help="Playlist name",
)
):
"""
Begin playing tracks from the directory $PLAYLIST_ROOT/[NAME].
"""
res = app_state["client"].play(playlist)
if res.status_code == 200:
print("OK")
@app.command()
def skip():
"""
Play the next track on the current playlist.
"""
res = app_state["client"].skip()
if res.status_code == 200:
print("OK")
@app.command()
def add(
playlist: str = typer.Argument(
...,
help="Playlist name",
),
theme: Optional[bool] = typer.Option(False, help="Make the first track the theme song."),
tracks: Annotated[Optional[List[Path]], typer.Argument()] = None,
):
"""
Recursively add one or more paths to the specified playlist. Tracks can be
any combination of individual audio files and directories containing audio
files; anything not already on the playlist will be added to it.
If --theme is specified, the first track will be designated the playlist
"theme." Theme songs get played first whenever the playlist is loaded,
after which the playlist order is randomized.
"""
pl = Playlist(name=playlist)
pl.add(tracks, make_theme=theme)
print(pl)
if __name__ == "__main__":
app.main()