Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0194c8b6ca | ||
![]() |
d2b6bdc36f | ||
![]() |
5822702590 | ||
![]() |
69ab191227 | ||
![]() |
ecd514005a | ||
![]() |
5985aef79d | ||
![]() |
f055511fe6 | ||
![]() |
17a6dcb4d2 | ||
![]() |
3bb2f83464 | ||
![]() |
a4d56c44c1 | ||
![]() |
64925a4b1e | ||
![]() |
6ecec29f4f |
24
README.md
|
@ -12,29 +12,24 @@ Groove on Demand was developed against python 3.10 and should work on anything 3
|
|||
|
||||
I have no idea if it will function on platforms besides Linux. Code was written to be portable but not tested to be portable. I also don't know if the dependencies support diverse platforms or not. `¯\_(ツ)_/¯`
|
||||
|
||||
### 1. Download the latest release
|
||||
### 1. Download and install the latest release
|
||||
|
||||
[ check the releases tab ]
|
||||
|
||||
```
|
||||
wget https://github.com/evilchili/grooveondemand/releases/grooveondemand-0.9.tar.gz
|
||||
```
|
||||
|
||||
### 2. Install
|
||||
|
||||
```
|
||||
pip3 install grooveondemand-0.9.tar.gz
|
||||
pip3 install https://github.com/evilchili/grooveondemand/releases/download/beta/grooveondemand-0.10.tar.gz
|
||||
```
|
||||
|
||||
### 3. Generate the default configuration
|
||||
|
||||
```
|
||||
~/.local/bin/groove setup > ~/.groove
|
||||
mkdir ~/.groove
|
||||
~/.local/bin/groove setup > ~/.groove/defaults
|
||||
```
|
||||
|
||||
### 4. Set the Media Root
|
||||
### 3. Set the Media Root
|
||||
|
||||
Edit `~/.groove` and defined `MEDIA_ROOT` to point to the directory containing your local audio files. For example:
|
||||
Edit `~/.groove/defaults` and define `MEDIA_ROOT` to point to the directory containing your local audio files. For example:
|
||||
|
||||
```
|
||||
MEDIA_ROOT=/media/audio/lossless
|
||||
|
@ -48,7 +43,12 @@ Before creating playlists, you must scan your media and build a database of trac
|
|||
groove scan
|
||||
```
|
||||
|
||||
This may take a long time depending on the size of your library and the capabilities of your system. Progress will be displayed as the scan progresses.
|
||||
This may take a long time depending on the size of your library and the capabilities of your system. Progress will be displayed as the scan progresses:
|
||||
|
||||
<pre>
|
||||
groove> scan
|
||||
1:27:34 <font color="#70BC45">━━━━━━━━━━━━━━━</font> <font color="#70BC45">100%</font> <font color="#555555">|</font> <font color="#70BC45"> 29946</font> <font color="#F1F2F6"><b>total</b></font> <font color="#555555">|</font> <font color="#70BC45"> 29946</font> <font color="#F1F2F6"><b>new</b></font> <font color="#555555">|</font> <font color="#F1F2F6">Scan of </font><font color="#9999FF">/mnt/grunt/music/FLAC</font><font color="#F1F2F6"> complete!</font>
|
||||
</pre>
|
||||
|
||||
## Start the Interactive Shell
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ from groove.console import Console
|
|||
SETUP_HELP = """
|
||||
# Please make sure you set MEDIA_ROOT and SECRET_KEY in your environment.
|
||||
# By default, Groove on Demand will attempt to load these variables from
|
||||
# ~/.groove, which may contain the following variables as well. See also
|
||||
# the --env paramter.
|
||||
# ~/.groove/defaults, which may contain the following variables as well. See
|
||||
# also the --root paramter.
|
||||
|
||||
# Set this one. The path containing your media files
|
||||
MEDIA_ROOT=
|
||||
|
@ -32,6 +32,15 @@ MEDIA_ROOT=
|
|||
# the kinds of files to import
|
||||
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. The default below uses ffmpeg to
|
||||
# transcode to webm with a reasonable trade-off between file size and quality.
|
||||
TRANSCODER=/usr/bin/ffmpeg -i INFILE -c:v libvpx-vp9 -crf 30 -b:v 0 -b:a 256k -c:a libopus OUTFILE
|
||||
|
||||
# where to cache transcoded media files
|
||||
CACHE_ROOT=~/.groove/cache
|
||||
|
||||
# where to store the groove_on_demand.db sqlite database.
|
||||
DATABASE_PATH=~
|
||||
|
||||
|
@ -42,6 +51,9 @@ DEFAULT_THEME=blue_train
|
|||
HOST=127.0.0.1
|
||||
PORT=2323
|
||||
|
||||
# The URL to use when constructing links. Defaults to http://HOST:PORT.
|
||||
#BASE_URL=http://127.0.0.1:2323
|
||||
|
||||
# Set this to a suitably random string.
|
||||
SECRET_KEY=
|
||||
|
||||
|
@ -56,12 +68,12 @@ app = typer.Typer()
|
|||
@app.callback()
|
||||
def main(
|
||||
context: typer.Context,
|
||||
env: Optional[Path] = typer.Option(
|
||||
root: Optional[Path] = typer.Option(
|
||||
Path('~/.groove'),
|
||||
help="Path to the Groove on Demand environment",
|
||||
)
|
||||
):
|
||||
load_dotenv(env.expanduser())
|
||||
load_dotenv(root.expanduser() / Path('defaults'))
|
||||
load_dotenv(stream=io.StringIO(SETUP_HELP))
|
||||
debug = os.getenv('DEBUG', None)
|
||||
logging.basicConfig(
|
||||
|
|
|
@ -110,6 +110,13 @@ class Console(_Console):
|
|||
"""
|
||||
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:
|
||||
"""
|
||||
Print text to the console with the current theme's error style applied.
|
||||
|
|
0
groove/media/__init__.py
Normal file
138
groove/media/transcoder.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
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
|
||||
|
||||
|
||||
@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):
|
||||
self.console.error("Cannot transcode tracks without a TRANSCODER defined in your environment.")
|
||||
return
|
||||
|
||||
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]",
|
||||
)
|
|
@ -4,7 +4,7 @@ import os
|
|||
from pathlib import Path
|
||||
from groove.exceptions import ConfigurationError, ThemeMissingException, ThemeConfigurationError
|
||||
|
||||
_setup_hint = "You may be able to solve this error by running 'groove setup' or specifying the --env parameter."
|
||||
_setup_hint = "You may be able to solve this error by running 'groove setup' or specifying the --root parameter."
|
||||
_reinstall_hint = "You might need to reinstall Groove On Demand to fix this error."
|
||||
|
||||
|
||||
|
@ -27,11 +27,25 @@ def media_root():
|
|||
return path
|
||||
|
||||
|
||||
def cache_root():
|
||||
path = os.environ.get('CACHE_ROOT', None)
|
||||
if not path:
|
||||
raise ConfigurationError(f"CACHE_ROOT is not defined in your environment.\n\n{_setup_hint}")
|
||||
path = Path(path).expanduser()
|
||||
logging.debug(f"Media cache root is {path}")
|
||||
return path
|
||||
|
||||
|
||||
def media(relpath):
|
||||
path = media_root() / Path(relpath)
|
||||
return path
|
||||
|
||||
|
||||
def transcoded_media(relpath):
|
||||
path = cache_root() / Path(relpath + '.webm')
|
||||
return path
|
||||
|
||||
|
||||
def static_root():
|
||||
dirname = os.environ.get('STATIC_PATH', 'static')
|
||||
path = root() / Path(dirname)
|
||||
|
|
|
@ -73,7 +73,8 @@ class Playlist:
|
|||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return f"http://{os.environ['HOST']}:{os.environ['PORT']}/playlist/{self.slug}"
|
||||
base = os.environ.get('BASE_URL', f"http://{os.environ['HOST']}:{os.environ['PORT']}")
|
||||
return f"{base}/playlist/{self.slug}"
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
|
@ -127,7 +128,8 @@ class Playlist:
|
|||
playlist = {
|
||||
'name': self.name,
|
||||
'slug': self.slug,
|
||||
'description': self.description
|
||||
'description': self.description,
|
||||
'url': self.url
|
||||
}
|
||||
if self.record:
|
||||
playlist.update(dict(self.record))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from slugify import slugify
|
||||
|
||||
from groove.db.manager import database_manager
|
||||
from groove.db.scanner import MediaScanner
|
||||
from groove.media.scanner import MediaScanner
|
||||
from groove.media.transcoder import Transcoder
|
||||
from groove.shell.base import BasePrompt, command
|
||||
from groove.exceptions import InvalidPathError
|
||||
from groove import db
|
||||
|
@ -90,6 +91,28 @@ class InteractiveShell(BasePrompt):
|
|||
return True
|
||||
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.
|
||||
|
||||
The default Groove on Demand configuration uses ffmpeg; try [b]groove setup[/b] from the command-line.
|
||||
|
||||
[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("""
|
||||
[title]LISTS FOR THE LIST LOVER[/title]
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ help = #999999
|
|||
background = #001321
|
||||
|
||||
info = #88FF88
|
||||
debug = #888888
|
||||
error = #FF8888
|
||||
danger = #FF8888
|
||||
|
||||
|
|
89
groove/static/themes/blue_train/static/45.svg
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" id="svg2" viewBox="0 0 652.5 652.5" version="1.0">
|
||||
<defs id="defs4">
|
||||
<linearGradient id="linearGradient3181">
|
||||
<stop id="stop3183" stop-color="#fff" stop-opacity=".30928" offset="0"/>
|
||||
<stop id="stop3185" stop-color="#c3ffff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="radialGradient3306" xlink:href="#linearGradient3181" gradientUnits="userSpaceOnUse" cy="159.54" cx="163.41" gradientTransform="matrix(1.4977 0 0 1.4977 -121.21 -108.37)" r="120.19"/>
|
||||
<radialGradient id="radialGradient3320" xlink:href="#linearGradient3181" gradientUnits="userSpaceOnUse" cy="-138.31" cx="200.03" gradientTransform="matrix(1.1233 0 0 -1.1233 288.85 353.75)" r="120.19"/>
|
||||
<linearGradient id="linearGradient3342" y2="273.03" gradientUnits="userSpaceOnUse" y1="159.44" x2="93.862" x1="-20.319">
|
||||
<stop id="stop3352" stop-color="#fff" offset="0"/>
|
||||
<stop id="stop3354" stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3348" y2="273.03" gradientUnits="userSpaceOnUse" y1="334.97" x2="93.862" x1="156.12">
|
||||
<stop id="stop3338" stop-opacity=".50588" offset="0"/>
|
||||
<stop id="stop3340" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3258" y2="330.78" gradientUnits="userSpaceOnUse" y1="260.13" x2="331.06" x1="263.75">
|
||||
<stop id="stop3254" offset="0"/>
|
||||
<stop id="stop3256" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient3384" y2="330.78" gradientUnits="userSpaceOnUse" y1="394.54" x2="331.06" x1="391.81">
|
||||
<stop id="stop3380" stop-color="#fff" offset="0"/>
|
||||
<stop id="stop3382" stop-color="#fff" stop-opacity="0" offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="layer1">
|
||||
<path id="path2178" d="m326.25 16.875c-170.77 0-309.38 138.6-309.38 309.38 0.005 170.76 138.61 309.36 309.38 309.36s309.38-138.6 309.37-309.37c0-170.77-138.59-309.38-309.37-309.38zm0 237.38c39.74 0 72 32.26 72 72s-32.26 72-72 72-72-32.26-72-72 32.26-72 72-72z"/>
|
||||
<path id="path3189" opacity=".5" fill="url(#radialGradient3320)" d="m513.53 378.69c-5.17 0-10.29 0.27-15.31 0.84-17.53 56.57-62.23 101.23-118.81 118.72-0.58 5.06-0.88 10.22-0.88 15.44 0 74.52 60.48 135 135 135s135-60.48 135-135-60.48-135-135-135z"/>
|
||||
<path id="path3171" fill="url(#radialGradient3306)" d="m268.34 22.312c-124.26 23.562-222.28 121.51-245.96 245.72 26.392 45.7 72.284 78.74 126.28 87.72-1.59-9.6-2.41-19.45-2.41-29.5 0-99.36 80.64-180 180-180 10.11 0 20.03 0.83 29.69 2.44-8.92-54.014-41.93-99.935-87.6-126.38zm57.91 226.78c-42.58 0-77.16 34.58-77.16 77.16 0 5.73 0.66 11.31 1.85 16.69 3.03-1.34 6.02-2.78 8.97-4.28-0.75-4.03-1.16-8.17-1.16-12.41 0-37.26 30.24-67.5 67.5-67.5 4.26 0 8.42 0.43 12.47 1.19 1.5-2.94 2.94-5.94 4.28-8.97-5.4-1.2-10.99-1.88-16.75-1.88z"/>
|
||||
<path id="path3325" d="m326.25 16.875c-170.77 0-309.38 138.6-309.38 309.38 0.005 170.76 138.61 309.36 309.38 309.36s309.38-138.6 309.37-309.37c0-170.77-138.59-309.38-309.37-309.38zm0 10.125c165.19 0 299.25 134.06 299.25 299.25s-134.06 299.25-299.25 299.25-299.25-134.06-299.25-299.25 134.06-299.25 299.25-299.25z"/>
|
||||
<path id="path3332" d="m88.844 264.11c-53.275 53.67-142.62 53.79-196.23 0.56-53.98-53.58-54.11-143.45-0.57-197.38 53.896-54.293 144.28-54.42 198.53-0.569 54.61 54.209 54.73 145.12 0.569 199.68-54.525 54.93-145.96 55.06-200.84 0.58-55.24-54.84-55.37-146.81-0.58-202 55.161-55.566 147.66-55.695 203.17-0.584 55.88 55.474 56.01 148.5 0.585 204.34-55.798 56.2-149.36 56.33-205.52 0.58-56.53-56.12-56.66-150.22-0.59-206.7 56.439-56.851 151.08-56.982 207.89-0.589 57.18 56.769 57.31 151.96 0.59 209.08-57.091 57.51-152.83 57.64-210.28 0.6-57.83-57.42-57.97-153.7-0.6-211.49 57.747-58.16 154.58-58.293 212.69-0.596 58.5 58.076 58.63 155.47 0.607 213.91-58.407 58.83-156.35 58.96-215.13 0.61-59.16-58.74-59.3-157.24-0.61-216.35 59.077-59.502 158.14-59.638 217.58-0.614 59.84 59.414 59.97 159.02 0.62 218.81-59.75 60.17-159.93 60.31-220.06 0.62-60.51-60.09-60.65-160.84-0.62-221.3 60.428-60.859 161.75-60.998 222.55-0.624 61.2 60.764 61.34 162.65 0.63 223.8-61.112 61.54-163.58 61.68-225.07 0.63-61.89-61.46-62.03-164.5-0.63-226.34 61.8-62.232 165.42-62.372 227.61-0.626 62.58 62.146 62.72 166.35 0.63 228.88-62.494 62.94-167.28 63.08-230.16 0.64-63.29-62.84-63.43-168.22-0.64-231.45 63.192-63.64 169.15-63.783 232.74-0.642 64 63.552 64.14 170.1 0.65 234.04-63.906 64.35-171.05 64.5-235.35 0.65-64.71-64.26-64.85-172-0.65-236.66 64.615-65.066 172.96-65.211 237.97-0.65 65.43 64.97 65.57 173.91 0.65 239.29-65.329 65.79-174.87 65.94-240.61 0.66-66.15-65.7-66.3-175.85-0.66-241.95 66.057-66.519 176.82-66.666 243.28-0.662 66.89 66.422 67.04 177.79 0.67 244.62-66.791 67.26-178.78 67.41-245.97 0.67-67.63-67.16-67.78-179.76-0.68-247.32 67.53-68 180.75-68.15 248.69-0.679 68.37 67.899 68.52 181.74 0.68 250.05-68.274 68.74-182.74 68.89-251.42 0.68-69.12-68.64-69.27-183.73-0.69-252.8 69.022-69.49 184.74-69.643 254.18-0.681 69.87 69.401 70.03 185.75 0.69 255.56-69.776 70.26-186.76 70.41-256.96 0.69-70.63-70.15-70.79-187.77-0.69-258.35 70.534-71.021 188.79-71.176 259.76-0.697 71.4 70.917 71.56 189.82 0.7 261.17-71.307 71.79-190.85 71.95-262.59 0.7-72.18-71.69-72.34-191.88-0.7-264 72.074-72.572 192.92-72.729 265.43-0.71 72.96 72.47 73.12 193.96 0.71 266.87-72.857 73.35-195.01 73.51-268.3 0.71-73.75-73.25-73.91-196.06-0.72-269.75 73.644-74.144 197.11-74.304 271.2-0.719 74.54 74.039 74.7 198.17 0.72 272.66-74.436 74.94-199.23 75.1-274.12 0.72-75.34-74.83-75.5-200.3-0.72-275.58 75.233-75.748 201.37-75.911 277.05-0.734 76.15 75.634 76.32 202.44 0.74 278.53-76.044 76.56-203.53 76.72-280.02 0.74-76.97-76.45-77.13-204.61-0.75-281.51 76.86-77.376 205.71-77.541 283.02-0.747 77.78 77.267 77.95 206.8 0.74 284.52-77.67 78.19-207.89 78.36-286.02 0.75-78.61-78.09-78.78-209-0.75-287.54 78.495-79.027 210.1-79.195 289.06-0.758 79.44 78.918 79.61 211.21 0.76 290.59-79.335 79.86-212.33 80.03-292.13 0.76-80.28-79.75-80.45-213.44-0.76-293.66 80.169-80.702 214.57-80.873 295.21-0.767 81.12 80.591 81.29 215.7 0.77 296.76-81.017 81.55-216.83 81.73-298.32 0.78-81.98-81.44-82.15-217.97-0.78-299.88 81.87-82.413 219.11-82.587 301.45-0.784 82.84 82.294 83.02 220.25 0.79 303.02-82.727 83.27-221.41 83.45-304.61 0.79-83.71-83.16-83.88-222.56-0.79-306.2 83.589-84.14 223.72-84.316 307.79-0.791 84.58 84.024 84.76 224.88 0.8 309.39-84.465 85.02-226.06 85.2-311 0.8-85.46-84.9-85.64-227.22-0.81-312.61 85.345-85.904 228.41-86.083 314.24-0.8084 86.34 85.782 86.52 229.59 0.81 315.86-86.23 86.79-230.78 86.97-317.5 0.81-87.23-86.67-87.41-231.96-0.81-319.13 87.118-87.686 233.16-87.867 320.78-0.8157 88.13 87.567 88.32 234.36 0.82 322.43-88.022 88.59-235.57 88.77-324.09 0.82-89.04-88.47-89.23-236.77-0.83-325.74 88.929-89.506 237.99-89.69 327.42-0.8338 89.96 89.38 90.15 239.2 0.84 329.09-89.84 90.42-240.44 90.61-330.78 0.84-90.88-90.3-91.07-241.66-0.84-332.47 90.756-91.345 242.89-91.532 334.16-0.8434 91.81 91.22 92 244.13 0.85 335.87-91.685 92.27-245.38 92.46-337.58 0.84-92.74-92.15-92.93-246.62-0.85-339.29 92.619-93.214 247.87-93.404 341.02-0.8548 93.68 93.089 93.87 249.13 0.85 342.74-93.557 94.16-250.39 94.35-344.48 0.86-94.63-94.03-94.82-251.66-0.86-346.22 94.508-95.11 252.93-95.31 347.97-0.8688 95.59 94.987 95.79 254.21 0.87 349.73-95.464 96.07-255.49 96.27-351.49 0.87-96.55-95.94-96.75-256.77-0.87-353.26 96.424-97.04 258.06-97.23 355.03-0.876 97.52 96.912 97.72 259.36 0.88 356.82" stroke-opacity=".039216" transform="matrix(1.2034 0 0 1.2094 335.56 126.67)" stroke="#f0f0ff" stroke-width="1px" fill="none"/>
|
||||
<path id="path3193" fill="#ff8a3c" d="m326.25 168.75c-86.94 0-157.5 70.56-157.5 157.5s70.56 157.5 157.5 157.5 157.5-70.56 157.5-157.5-70.56-157.5-157.5-157.5zm0 90c37.26 0 67.5 30.24 67.5 67.5s-30.24 67.5-67.5 67.5-67.5-30.24-67.5-67.5 30.24-67.5 67.5-67.5z"/>
|
||||
<path id="path3374" d="m88.844 264.11c-53.275 53.67-142.62 53.79-196.23 0.56-53.98-53.58-54.11-143.45-0.57-197.38 53.896-54.293 144.28-54.42 198.53-0.569 54.61 54.209 54.73 145.12 0.569 199.68-54.525 54.93-145.96 55.06-200.84 0.58-55.24-54.84-55.37-146.81-0.58-202 55.161-55.566 147.66-55.695 203.17-0.584 55.88 55.474 56.01 148.5 0.585 204.34-55.798 56.2-149.36 56.33-205.52 0.58-56.53-56.12-56.66-150.22-0.59-206.7 56.439-56.851 151.08-56.982 207.89-0.589 57.18 56.769 57.31 151.96 0.59 209.08-57.091 57.51-152.83 57.64-210.28 0.6-57.83-57.42-57.97-153.7-0.6-211.49 57.747-58.16 154.58-58.293 212.69-0.596 58.5 58.076 58.63 155.47 0.607 213.91-58.407 58.83-156.35 58.96-215.13 0.61-59.16-58.74-59.3-157.24-0.61-216.35 59.077-59.502 158.14-59.638 217.58-0.614 59.84 59.414 59.97 159.02 0.62 218.81-59.75 60.17-159.93 60.31-220.06 0.62-60.51-60.09-60.65-160.84-0.62-221.3 60.428-60.859 161.75-60.998 222.55-0.624 61.2 60.764 61.34 162.65 0.63 223.8-61.112 61.54-163.58 61.68-225.07 0.63-61.89-61.46-62.03-164.5-0.63-226.34 61.8-62.232 165.42-62.372 227.61-0.626 62.58 62.146 62.72 166.35 0.63 228.88-62.494 62.94-167.28 63.08-230.16 0.64-63.29-62.84-63.43-168.22-0.64-231.45 63.192-63.64 169.15-63.783 232.74-0.642 64 63.552 64.14 170.1 0.65 234.04-63.906 64.35-171.05 64.5-235.35 0.65-64.71-64.26-64.85-172-0.65-236.66 64.615-65.066 172.96-65.211 237.97-0.65 65.43 64.97 65.57 173.91 0.65 239.29-65.329 65.79-174.87 65.94-240.61 0.66-66.15-65.7-66.3-175.85-0.66-241.95 66.057-66.519 176.82-66.666 243.28-0.662 66.89 66.422 67.04 177.79 0.67 244.62-66.791 67.26-178.78 67.41-245.97 0.67-67.63-67.16-67.78-179.76-0.68-247.32 67.53-68 180.75-68.15 248.69-0.679 68.37 67.899 68.52 181.74 0.68 250.05-68.274 68.74-182.74 68.89-251.42 0.68-69.12-68.64-69.27-183.73-0.69-252.8 69.022-69.49 184.74-69.643 254.18-0.681 69.87 69.401 70.03 185.75 0.69 255.56-69.776 70.26-186.76 70.41-256.96 0.69-70.63-70.15-70.79-187.77-0.69-258.35 70.534-71.021 188.79-71.176 259.76-0.697 71.4 70.917 71.56 189.82 0.7 261.17-71.307 71.79-190.85 71.95-262.59 0.7-72.18-71.69-72.34-191.88-0.7-264 72.074-72.572 192.92-72.729 265.43-0.71 72.96 72.47 73.12 193.96 0.71 266.87-72.857 73.35-195.01 73.51-268.3 0.71-73.75-73.25-73.91-196.06-0.72-269.75 73.644-74.144 197.11-74.304 271.2-0.719 74.54 74.039 74.7 198.17 0.72 272.66-74.436 74.94-199.23 75.1-274.12 0.72-75.34-74.83-75.5-200.3-0.72-275.58 75.233-75.748 201.37-75.911 277.05-0.734 76.15 75.634 76.32 202.44 0.74 278.53-76.044 76.56-203.53 76.72-280.02 0.74-76.97-76.45-77.13-204.61-0.75-281.51 76.86-77.376 205.71-77.541 283.02-0.747 77.78 77.267 77.95 206.8 0.74 284.52-77.67 78.19-207.89 78.36-286.02 0.75-78.61-78.09-78.78-209-0.75-287.54 78.495-79.027 210.1-79.195 289.06-0.758 79.44 78.918 79.61 211.21 0.76 290.59-79.335 79.86-212.33 80.03-292.13 0.76-80.28-79.75-80.45-213.44-0.76-293.66 80.169-80.702 214.57-80.873 295.21-0.767 81.12 80.591 81.29 215.7 0.77 296.76-81.017 81.55-216.83 81.73-298.32 0.78-81.98-81.44-82.15-217.97-0.78-299.88 81.87-82.413 219.11-82.587 301.45-0.784 82.84 82.294 83.02 220.25 0.79 303.02-82.727 83.27-221.41 83.45-304.61 0.79-83.71-83.16-83.88-222.56-0.79-306.2 83.589-84.14 223.72-84.316 307.79-0.791 84.58 84.024 84.76 224.88 0.8 309.39-84.465 85.02-226.06 85.2-311 0.8-85.46-84.9-85.64-227.22-0.81-312.61 85.345-85.904 228.41-86.083 314.24-0.8084 86.34 85.782 86.52 229.59 0.81 315.86-86.23 86.79-230.78 86.97-317.5 0.81-87.23-86.67-87.41-231.96-0.81-319.13 87.118-87.686 233.16-87.867 320.78-0.8157 88.13 87.567 88.32 234.36 0.82 322.43-88.022 88.59-235.57 88.77-324.09 0.82-89.04-88.47-89.23-236.77-0.83-325.74 88.929-89.506 237.99-89.69 327.42-0.8338 89.96 89.38 90.15 239.2 0.84 329.09-89.84 90.42-240.44 90.61-330.78 0.84-90.88-90.3-91.07-241.66-0.84-332.47 90.756-91.345 242.89-91.532 334.16-0.8434 91.81 91.22 92 244.13 0.85 335.87-91.685 92.27-245.38 92.46-337.58 0.84-92.74-92.15-92.93-246.62-0.85-339.29 92.619-93.214 247.87-93.404 341.02-0.8548 93.68 93.089 93.87 249.13 0.85 342.74-93.557 94.16-250.39 94.35-344.48 0.86-94.63-94.03-94.82-251.66-0.86-346.22 94.508-95.11 252.93-95.31 347.97-0.8688 95.59 94.987 95.79 254.21 0.87 349.73-95.464 96.07-255.49 96.27-351.49 0.87-96.55-95.94-96.75-256.77-0.87-353.26 96.424-97.04 258.06-97.23 355.03-0.876 97.52 96.912 97.72 259.36 0.88 356.82" stroke-opacity=".49727" transform="matrix(1.2034 0 0 1.2094 333.56 126.67)" stroke="#000" stroke-width="1px" fill="none"/>
|
||||
<path id="path3334" d="m155.49 247.16a87.629 87.18 0 1 1 -175.26 0 87.629 87.18 0 1 1 175.26 0z" stroke-opacity=".25098" transform="matrix(1.7862 0 0 1.7954 205.04 -117.5)" stroke="url(#linearGradient3342)" stroke-linecap="round" stroke-width="1.2564" fill="none"/>
|
||||
<path id="path3346" d="m155.49 247.16a87.629 87.18 0 1 1 -175.26 0 87.629 87.18 0 1 1 175.26 0z" stroke-opacity=".25098" transform="matrix(1.7862 0 0 1.7954 205.04 -117.5)" stroke="url(#linearGradient3348)" stroke-linecap="round" stroke-width="2.5128" fill="none"/>
|
||||
<path id="path2280" d="m390.96 327.15a63.363 66.508 0 1 1 -126.72 0 63.363 66.508 0 1 1 126.72 0z" stroke-opacity=".25098" transform="matrix(1.0746 0 0 1.0238 -25.801 -8.6896)" stroke="url(#linearGradient3258)" stroke-width="1.7311" fill="none"/>
|
||||
<path id="path3376" d="m390.96 327.15a63.363 66.508 0 1 1 -126.72 0 63.363 66.508 0 1 1 126.72 0z" stroke-opacity=".25098" transform="matrix(1.0746 0 0 1.0238 -25.801 -8.6896)" stroke="url(#linearGradient3384)" stroke-width="1.7311" fill="none"/>
|
||||
</g>
|
||||
<g id="layer3">
|
||||
<path id="path3273" d="m155.49 247.16a87.629 87.18 0 1 1 -175.26 0 87.629 87.18 0 1 1 175.26 0z" stroke-opacity=".37647" transform="matrix(1.6841 0 0 1.6928 211.97 -92.147)" stroke="#fff" stroke-linecap="round" stroke-width="1.0896" fill="none"/>
|
||||
</g>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
|
||||
<dc:publisher>
|
||||
<cc:Agent rdf:about="http://openclipart.org/">
|
||||
<dc:title>Openclipart</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:title>45 Record Album</dc:title>
|
||||
<dc:date>2007-08-18T16:13:11</dc:date>
|
||||
<dc:description>A 45 record album. 
\n
\nThe spiral grooves really slow a computer down so if you're editing it on older system, you may want to delete those two objects first thing.</dc:description>
|
||||
<dc:source>http://openclipart.org/detail/4835/45-record-album-by-masonmouse</dc:source>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>masonmouse</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>45 record</rdf:li>
|
||||
<rdf:li>album</rdf:li>
|
||||
<rdf:li>clip art</rdf:li>
|
||||
<rdf:li>clipart</rdf:li>
|
||||
<rdf:li>disc jockey</rdf:li>
|
||||
<rdf:li>dj</rdf:li>
|
||||
<rdf:li>how i did it</rdf:li>
|
||||
<rdf:li>image</rdf:li>
|
||||
<rdf:li>media</rdf:li>
|
||||
<rdf:li>music</rdf:li>
|
||||
<rdf:li>png</rdf:li>
|
||||
<rdf:li>public domain</rdf:li>
|
||||
<rdf:li>svg</rdf:li>
|
||||
<rdf:li>vinyl</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 36 KiB |
BIN
groove/static/themes/blue_train/static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
groove/static/themes/blue_train/static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
groove/static/themes/blue_train/static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
groove/static/themes/blue_train/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -6,16 +6,15 @@ html {
|
|||
margin: 0;
|
||||
outline: 0;
|
||||
font-family: 'Clarendon MT Std', sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: rgb(0,4,16);
|
||||
background: linear-gradient(0deg, rgba(0,4,16,1) 31%, rgba(1,125,147,1) 97%);
|
||||
background: rgb(0, 4, 16);
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
|
@ -27,11 +26,13 @@ a:active, a:hover {
|
|||
}
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
background: linear-gradient(0deg, rgba(0,4,16,1) 1%, rgba(1,125,147,1) 97%);
|
||||
overflow-x: wrap;
|
||||
width: 96%;
|
||||
height: 96%;
|
||||
margin: auto;
|
||||
margin-top: 1em;
|
||||
overflow-y: auto;
|
||||
width: calc(100% - 4em);
|
||||
height: calc(100% - 4em);
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#details {
|
||||
|
@ -115,14 +116,40 @@ a:active, a:hover {
|
|||
line-height: 5em;
|
||||
}
|
||||
#playBtn {
|
||||
font-family: sans-serif;
|
||||
font-size: 5em;
|
||||
padding-left: 0.1em;
|
||||
color: #000;
|
||||
font-size: 5em;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: block;
|
||||
transform: scale(var(--ggs,1));
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#playBtn:before {
|
||||
content: "";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0.20em;
|
||||
left:0.35em;
|
||||
border-top: 0.3em solid transparent;
|
||||
border-bottom: 0.3em solid transparent;
|
||||
border-left: 0.4em solid;
|
||||
}
|
||||
|
||||
#pauseBtn {
|
||||
font-family: sans-serif;
|
||||
display: none;
|
||||
font-size: 4em;
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
font-size: 5em;
|
||||
margin: 0px auto;
|
||||
left: 0.3em;
|
||||
top: 0.25em;
|
||||
border-color: #000;
|
||||
border-style: double;
|
||||
border-width: 0px 0px 0px 37px;
|
||||
|
||||
}
|
||||
#prevBtn {
|
||||
color: #fff;
|
||||
|
@ -181,10 +208,12 @@ a:active, a:hover {
|
|||
}
|
||||
.list-song {
|
||||
padding: 0.25em;
|
||||
color: #70bc45;
|
||||
}
|
||||
.list-song:hover {
|
||||
cursor: pointer;
|
||||
background: #f1f2f6;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#footer {
|
||||
|
@ -194,7 +223,6 @@ a:active, a:hover {
|
|||
color: #888;
|
||||
font-size: 0.9em;
|
||||
letter-spacing: 0.1em;
|
||||
margin: 0;
|
||||
margin-top: 1em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
@ -257,3 +285,4 @@ a:active, a:hover {
|
|||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{playlist['name']}} - Groove On Demand</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="user-scalable=no">
|
||||
<title>Groove On Demand</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{{playlist['name']}}: {{playlist['description']}}">
|
||||
|
||||
<meta name="og:title" content="{{playlist['name']}}">
|
||||
<meta name="og:description" content="{{playlist['description']}}">
|
||||
<meta name="og:url" content="{{playlist['url']}}">
|
||||
<meta name="og:type" content="audio">
|
||||
<meta name="og:provider_name" content="Groove on Demand">
|
||||
<meta name="og:image" content="/static/45.svg">
|
||||
|
||||
<link rel='stylesheet' href='/static/styles.css' />
|
||||
<link rel='stylesheet' href="https://fonts.cdnfonts.com/css/clarendon-mt-std" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
|
||||
<script defer crossorigin src='https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.core.min.js'></script>
|
||||
<script defer src='/static/player.js'></script>
|
||||
<script>
|
||||
|
@ -36,9 +51,9 @@
|
|||
<tr>
|
||||
<td id='controls'>
|
||||
<div id='big_button' class='btn'>
|
||||
<div id="loading"></div>
|
||||
<div id="playBtn">⏵</div>
|
||||
<div id="pauseBtn">⏸</div>
|
||||
<div id="loading"></div>
|
||||
<div id="playBtn"></div>
|
||||
<div id="pauseBtn"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -59,7 +74,7 @@
|
|||
<div id="playlist">
|
||||
<div id="list"></div>
|
||||
</div>
|
||||
<div id='footer'>groove on demand : an <a alt="evilchili at liner notes dot club" href="https://linernotes.club/@evilchili">@evilchili</a> jam</div>
|
||||
<div id='footer'>groove on demand : an <a alt="evilchili at liner notes dot club" href="https://linernotes.club/@evilchili">@evilchili</a> jam</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -9,7 +9,7 @@ def encode(args: List, uri: str) -> str:
|
|||
Encode a request and cryptographically sign it. This serves two purposes:
|
||||
First, it enables the handler that receives the request to verify that the
|
||||
request was meant for it, preventing various routing and relay-induced bugs.
|
||||
Second, it ensures the request wasn't corrutped or tampered with during
|
||||
Second, it ensures the request wasn't corrupted or tampered with during
|
||||
transmission.
|
||||
|
||||
Args:
|
||||
|
|
|
@ -78,8 +78,10 @@ def serve_track(request, track_id, db):
|
|||
except (NoResultFound, MultipleResultsFound):
|
||||
return HTTPResponse(status=404, body="Not found")
|
||||
|
||||
path = groove.path.media(track['relpath'])
|
||||
logging.debug(f"Service track {path.name} from {path.parent}")
|
||||
path = groove.path.transcoded_media(track['relpath'])
|
||||
if not path.exists():
|
||||
path = groove.path.media(track['relpath'])
|
||||
logging.debug(f"Serving track {path.name}")
|
||||
return static_file(path.name, root=path.parent)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "grooveondemand"
|
||||
version = "0.9"
|
||||
version = "0.10"
|
||||
description = "audio playlist server"
|
||||
authors = ["evilchili <evilchili@gmail.com>"]
|
||||
license = "MIT License"
|
||||
|
|
|
@ -5,7 +5,8 @@ from unittest.mock import MagicMock
|
|||
from sqlalchemy import func
|
||||
|
||||
import groove.exceptions
|
||||
from groove.db import scanner, track
|
||||
from groove.media import scanner
|
||||
from groove.db import track
|
||||
|
||||
|
||||
def test_scanner(monkeypatch, in_memory_db):
|
||||
|
|