Compare commits

..

12 Commits
beta ... main

Author SHA1 Message Date
evilchili
0194c8b6ca adding favico 2022-12-31 15:48:22 -08:00
evilchili
d2b6bdc36f theme and card tweaks 2022-12-31 15:35:45 -08:00
evilchili
5822702590 adding mastodon meta card, BASE_URL config option. 2022-12-31 15:16:30 -08:00
evilchili
69ab191227 fixing css theme 2022-12-31 14:48:09 -08:00
evilchili
ecd514005a tweaking theme 2022-12-31 13:44:12 -08:00
evilchili
5985aef79d tweaking docs 2022-12-31 12:44:59 -08:00
evilchili
f055511fe6 Adding transcoder 2022-12-31 12:43:48 -08:00
evilchili
17a6dcb4d2 refactor media module, change --env to --root
This commit moves the media scanner out of the db module and into
a new module, media. We also change the --env parameter to --root,
which takes a path to a directory where the default configuration
and the transcoded media cache will live.
2022-12-31 12:43:48 -08:00
evilchili
3bb2f83464
Update README.md 2022-12-22 00:53:41 -08:00
evilchili
a4d56c44c1
Update README.md
added example progress bar.
2022-12-22 00:28:47 -08:00
evilchili
64925a4b1e
typos 2022-12-22 00:24:50 -08:00
evilchili
6ecec29f4f
Update README.md
updating example link to current beta release
2022-12-22 00:24:07 -08:00
23 changed files with 377 additions and 44 deletions

View File

@ -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&gt; 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

View File

@ -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(

View File

@ -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
View File

138
groove/media/transcoder.py Normal file
View 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]",
)

View File

@ -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)

View File

@ -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))

View File

@ -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]

View File

@ -18,6 +18,7 @@ help = #999999
background = #001321
info = #88FF88
debug = #888888
error = #FF8888
danger = #FF8888

View 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. &#xD;\n&#xD;\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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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; }
}

View File

@ -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>

View File

@ -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:

View File

@ -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)

View File

@ -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"

View File

@ -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):