Adding transcoder
This commit is contained in:
parent
17a6dcb4d2
commit
f055511fe6
|
@ -110,6 +110,13 @@ class Console(_Console):
|
||||||
"""
|
"""
|
||||||
super().print(txt, overflow=self._overflow, **kwargs)
|
super().print(txt, overflow=self._overflow, **kwargs)
|
||||||
|
|
||||||
|
def debug(self, txt: str, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Print text to the console with the current theme's debug style applied, if debugging is enabled.
|
||||||
|
"""
|
||||||
|
if os.environ.get('DEBUG', None):
|
||||||
|
self.print(dedent(txt), style='debug')
|
||||||
|
|
||||||
def error(self, txt: str, **kwargs) -> None:
|
def error(self, txt: str, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Print text to the console with the current theme's error style applied.
|
Print text to the console with the current theme's error style applied.
|
||||||
|
|
0
groove/media/__init__.py
Normal file
0
groove/media/__init__.py
Normal file
139
groove/media/transcoder.py
Normal file
139
groove/media/transcoder.py
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from typing import Union, List
|
||||||
|
|
||||||
|
import rich.repr
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.progress import (
|
||||||
|
Progress,
|
||||||
|
TextColumn,
|
||||||
|
BarColumn,
|
||||||
|
SpinnerColumn,
|
||||||
|
TimeRemainingColumn
|
||||||
|
)
|
||||||
|
|
||||||
|
import groove.path
|
||||||
|
|
||||||
|
from groove.exceptions import ConfigurationError
|
||||||
|
|
||||||
|
|
||||||
|
@rich.repr.auto(angular=True)
|
||||||
|
class Transcoder:
|
||||||
|
"""
|
||||||
|
SYNOPSIS
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
|
||||||
|
ARGS
|
||||||
|
|
||||||
|
EXAMPLES
|
||||||
|
|
||||||
|
INSTANCE ATTRIBUTES
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, console: Union[Console, None] = None) -> None:
|
||||||
|
self.console = console or Console()
|
||||||
|
self._transcoded = 0
|
||||||
|
self._processed = 0
|
||||||
|
self._total = 0
|
||||||
|
|
||||||
|
def transcode(self, sources: List) -> int:
|
||||||
|
"""
|
||||||
|
Transcode the list of source files
|
||||||
|
"""
|
||||||
|
count = len(sources)
|
||||||
|
|
||||||
|
if not os.environ.get('TRANSCODER', None):
|
||||||
|
raise ConfigurationError("Cannot transcode tracks without a TRANSCODR defined in your environment.")
|
||||||
|
|
||||||
|
cache = groove.path.cache_root()
|
||||||
|
if not cache.exists():
|
||||||
|
cache.mkdir()
|
||||||
|
|
||||||
|
async def _do_transcode(progress, task_id):
|
||||||
|
tasks = set()
|
||||||
|
for relpath in sources:
|
||||||
|
self._total += 1
|
||||||
|
progress.update(task_id, total=self._total)
|
||||||
|
tasks.add(asyncio.create_task(self._transcode_one_track(relpath, progress, task_id)))
|
||||||
|
progress.start_task(task_id)
|
||||||
|
|
||||||
|
progress = Progress(
|
||||||
|
TimeRemainingColumn(compact=True, elapsed_when_finished=True),
|
||||||
|
BarColumn(bar_width=15),
|
||||||
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%", justify="left"),
|
||||||
|
TextColumn("[dim]|"),
|
||||||
|
TextColumn("[title]{task.total:-6d}[/title] [b]total", justify="right"),
|
||||||
|
TextColumn("[dim]|"),
|
||||||
|
TextColumn("[title]{task.fields[transcoded]:-6d}[/title] [b]new", justify="right"),
|
||||||
|
TextColumn("[dim]|"),
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
console=self.console,
|
||||||
|
)
|
||||||
|
with progress:
|
||||||
|
task_id = progress.add_task(
|
||||||
|
f"[bright]Transcoding [link]{count} tracks[/link]...",
|
||||||
|
transcoded=0,
|
||||||
|
total=0,
|
||||||
|
start=False
|
||||||
|
)
|
||||||
|
asyncio.run(_do_transcode(progress, task_id))
|
||||||
|
progress.update(
|
||||||
|
task_id,
|
||||||
|
transcoded=self._transcoded,
|
||||||
|
completed=self._total,
|
||||||
|
description=f"[bright]Transcode of [link]{count} tracks[/link] complete!",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_or_create_cache_dir(self, relpath):
|
||||||
|
cached_path = groove.path.transcoded_media(relpath)
|
||||||
|
cached_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
return cached_path
|
||||||
|
|
||||||
|
def _run_transcoder(self, infile, outfile):
|
||||||
|
cmd = []
|
||||||
|
for part in os.environ['TRANSCODER'].split():
|
||||||
|
if part == 'INFILE':
|
||||||
|
cmd.append(str(infile))
|
||||||
|
elif part == 'OUTFILE':
|
||||||
|
cmd.append(str(outfile))
|
||||||
|
else:
|
||||||
|
cmd.append(part)
|
||||||
|
|
||||||
|
output = ''
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
self.console.error(f"Transcoder failed with status {e.returncode}: {' '.join(cmd)}")
|
||||||
|
self.console.error(output)
|
||||||
|
|
||||||
|
async def _transcode_one_track(self, relpath, progress, task_id):
|
||||||
|
"""
|
||||||
|
Transcode one track, if it isn't already cached.
|
||||||
|
"""
|
||||||
|
self._processed += 1
|
||||||
|
|
||||||
|
source_path = groove.path.media(relpath)
|
||||||
|
if not source_path.exists():
|
||||||
|
logging.error(f"Source does not exist: [link]{source_path}[/link].")
|
||||||
|
return
|
||||||
|
|
||||||
|
cached_path = self._get_or_create_cache_dir(relpath)
|
||||||
|
if cached_path.exists():
|
||||||
|
self.console.debug(f"Skipping existing [link]{cached_path}[/link].")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.console.debug(f"Transcoding [link]{cached_path}[/link]")
|
||||||
|
self._run_transcoder(source_path, cached_path)
|
||||||
|
|
||||||
|
progress.update(
|
||||||
|
task_id,
|
||||||
|
transcoded=self._transcoded,
|
||||||
|
processed=self._processed,
|
||||||
|
description=f"[bright]Transcoded [link]{relpath}[/link]",
|
||||||
|
)
|
|
@ -32,10 +32,6 @@ def cache_root():
|
||||||
if not path:
|
if not path:
|
||||||
raise ConfigurationError(f"CACHE_ROOT is not defined in your environment.\n\n{_setup_hint}")
|
raise ConfigurationError(f"CACHE_ROOT is not defined in your environment.\n\n{_setup_hint}")
|
||||||
path = Path(path).expanduser()
|
path = Path(path).expanduser()
|
||||||
if not path.exists() or not path.is_dir():
|
|
||||||
raise ConfigurationError(
|
|
||||||
"The cache_root directory (CACHE_ROOT) doesn't exist, or isn't a directory.\n\n{_setup_hint}"
|
|
||||||
)
|
|
||||||
logging.debug(f"Media cache root is {path}")
|
logging.debug(f"Media cache root is {path}")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
@ -46,7 +42,7 @@ def media(relpath):
|
||||||
|
|
||||||
|
|
||||||
def transcoded_media(relpath):
|
def transcoded_media(relpath):
|
||||||
path = cache_root() / Path(relpath)
|
path = cache_root() / Path(relpath + '.webm')
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from slugify import slugify
|
||||||
|
|
||||||
from groove.db.manager import database_manager
|
from groove.db.manager import database_manager
|
||||||
from groove.media.scanner import MediaScanner
|
from groove.media.scanner import MediaScanner
|
||||||
|
from groove.media.transcoder import Transcoder
|
||||||
from groove.shell.base import BasePrompt, command
|
from groove.shell.base import BasePrompt, command
|
||||||
from groove.exceptions import InvalidPathError
|
from groove.exceptions import InvalidPathError
|
||||||
from groove import db
|
from groove import db
|
||||||
|
@ -90,6 +91,26 @@ class InteractiveShell(BasePrompt):
|
||||||
return True
|
return True
|
||||||
scanner.scan()
|
scanner.scan()
|
||||||
|
|
||||||
|
@command(usage="""
|
||||||
|
[title]TRANSCODING[/title]
|
||||||
|
|
||||||
|
Groove on Demand will stream audio to web clients in the native format of your source media files, but for maximum
|
||||||
|
portability, performance, and interoperability with reverse proxies, it's a good idea to transcode to .webm first.
|
||||||
|
Use the [b]transcode[/b] command to cache transcoded copies of every track currently in a playlist that isn't
|
||||||
|
already in .webm format. Existing cache entries will be skipped. See also [b]cache[/b].
|
||||||
|
|
||||||
|
[title]USAGE[/title]
|
||||||
|
|
||||||
|
[link]> transcode[/link]
|
||||||
|
""")
|
||||||
|
def transcode(self, parts):
|
||||||
|
"""
|
||||||
|
Run the transcoder.
|
||||||
|
"""
|
||||||
|
tracks = self.manager.session.query(db.track).filter(db.entry.c.track_id == db.track.c.id).all()
|
||||||
|
transcoder = Transcoder(console=self.console)
|
||||||
|
transcoder.transcode([track['relpath'] for track in tracks])
|
||||||
|
|
||||||
@command("""
|
@command("""
|
||||||
[title]LISTS FOR THE LIST LOVER[/title]
|
[title]LISTS FOR THE LIST LOVER[/title]
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ help = #999999
|
||||||
background = #001321
|
background = #001321
|
||||||
|
|
||||||
info = #88FF88
|
info = #88FF88
|
||||||
|
debug = #888888
|
||||||
error = #FF8888
|
error = #FF8888
|
||||||
danger = #FF8888
|
danger = #FF8888
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user