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
|
2024-03-04 17:56:32 -08:00
|
|
|
from croaker.server import server
|
2024-03-01 01:00:17 -08:00
|
|
|
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
|
|
|
|
|
2024-03-04 17:56:32 -08:00
|
|
|
HOST=0.0.0.0
|
2024-03-01 01:00:17 -08:00
|
|
|
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",
|
|
|
|
),
|
|
|
|
debug: Optional[bool] = typer.Option(None, help="Enable debugging output"),
|
|
|
|
):
|
|
|
|
load_dotenv(root.expanduser() / Path("defaults"))
|
|
|
|
load_dotenv(stream=io.StringIO(SETUP_HELP))
|
|
|
|
if debug is not None:
|
|
|
|
if debug:
|
2024-03-04 17:56:32 -08:00
|
|
|
os.environ["DEBUG"] = '1'
|
2024-03-01 01:00:17 -08:00
|
|
|
else:
|
|
|
|
del os.environ["DEBUG"]
|
|
|
|
|
|
|
|
logging.basicConfig(
|
2024-03-04 17:56:32 -08:00
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
2024-03-01 01:00:17 -08:00
|
|
|
level=logging.DEBUG if debug else logging.INFO,
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
2024-03-04 17:56:32 -08:00
|
|
|
croaker.path.root()
|
|
|
|
croaker.path.playlist_root()
|
2024-03-01 01:00:17 -08:00
|
|
|
except ConfigurationError as e:
|
|
|
|
sys.stderr.write(f"{e}\n\n{SETUP_HELP}")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
@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.
|
|
|
|
"""
|
2024-03-04 17:56:32 -08:00
|
|
|
logging.debug("Switching to session_start playlist...")
|
|
|
|
logging.debug("Starting server...")
|
2024-03-01 01:00:17 -08:00
|
|
|
if daemonize:
|
|
|
|
server.daemonize()
|
|
|
|
else:
|
|
|
|
server.start()
|
|
|
|
|
|
|
|
|
|
|
|
@app.command()
|
|
|
|
def stop():
|
|
|
|
"""
|
|
|
|
Terminate the webserver process and liquidsoap.
|
|
|
|
"""
|
|
|
|
server.stop()
|
|
|
|
|
|
|
|
|
|
|
|
@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,
|
|
|
|
):
|
|
|
|
"""
|
2024-03-04 17:56:32 -08:00
|
|
|
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.
|
2024-03-01 01:00:17 -08:00
|
|
|
|
|
|
|
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()
|