implementing themes and refactoring path operations

This commit is contained in:
evilchili
2022-12-04 12:09:27 -08:00
parent 50d2fe1349
commit d5934a99fb
9 changed files with 778 additions and 23 deletions
+3 -8
View File
@@ -1,8 +1,6 @@
import asyncio
import logging
import os
import sys
import music_tag
from pathlib import Path
@@ -10,20 +8,17 @@ from typing import Callable, Union, Iterable
from sqlalchemy import func
import groove.db
import groove.path
class MediaScanner:
"""
Scan a directory structure containing audio files and import them into the database.
"""
def __init__(self, root: Path, db: Callable, glob: Union[str, None] = None) -> None:
def __init__(self, root: Union[Path, None], db: Callable, glob: Union[str, None] = None) -> None:
self._db = db
self._glob = tuple((glob or os.environ.get('MEDIA_GLOB')).split(','))
try:
self._root = root or Path(os.environ.get('MEDIA_ROOT'))
except TypeError:
logging.error("Could not find media root. Do you need to define MEDIA_ROOT in your environment?")
sys.exit(1)
self._root = root or groove.path.media_root()
logging.debug(f"Configured media scanner for root: {self._root}")
@property
+18
View File
@@ -3,3 +3,21 @@ class APIHandlingException(Exception):
"""
An API reqeust could not be encoded or decoded.
"""
class ThemeMissingException(Exception):
"""
The specified theme could not be loaded.
"""
class ThemeConfigurationError(Exception):
"""
A theme is missing required files or configuration.
"""
class ConfigurationError(Exception):
"""
An error was discovered with the Groove on Demand configuration.
"""
+90
View File
@@ -0,0 +1,90 @@
import os
from pathlib import Path
from groove.exceptions import ConfigurationError, ThemeMissingException
_setup_hint = "You may be able to solve this error by running 'groove setup'."
_reinstall_hint = "You might need to reinstall Groove On Demand to fix this error."
def root():
path = os.environ.get('GROOVE_ON_DEMAND_ROOT', None)
if not path:
raise ConfigurationError(f"GROOVE_ON_DEMAND_ROOT is not defined in your environment.\n\n{_setup_hint}")
path = Path(path).expanduser()
if not path.exists or not path.is_dir:
raise ConfigurationError(
"The Groove on Demand root directory (GROOVE_ON_DEMAND_ROOT) "
f"does not exist or isn't a directory.\n\n{_reinstall_hint}"
)
return Path(path)
def media_root():
path = os.environ.get('MEDIA_ROOT', None)
if not path:
raise ConfigurationError(f"MEDIA_ROOT is not defined in your environment.\n\n{_setup_hint}")
path =Path(path).expanduser()
if not path.exists and path.is_dir:
raise ConfigurationError(
"The media_root directory (MEDIA_ROOT) doesn't exist, or isn't a directory.\n\n{_setup_hint}"
)
return path
def media(relpath):
return themes_root() / Path(relpath)
def static_root():
dirname = os.environ.get('STATIC_PATH', 'static')
path = root() / Path(dirname)
if not path.exists or not path.is_dir:
raise ConfigurationError(
f"The static assets directory {dirname} (STATIC_PATH) "
f"doesn't exist, or isn't a directory.\n\n{_reinstall_hint}"
)
return path
def static(relpath):
return static_root() / Path(relpath)
def themes_root():
dirname = os.environ.get('THEMES_PATH', 'themes')
path = root() / Path(dirname)
if not path.exists or not path.is_dir:
raise ConfigurationError(
f"The themes directory {dirname} (THEMES_PATH) "
f"doesn't exist, or isn't a directory.\n\n{_reinstall_hint}"
)
return path
def theme(name):
path = themes_root() / Path(name)
if not path.exists or not path.is_dir:
available = ','.join(available_themes)
raise ThemeMissingException(
f"A theme directory named {name} does not exist or isn't a directory. "
"Perhaps there is a typo in the name?\n"
f"Available themes: {available}"
)
return path
def theme_static(relpath):
return Path('static') / Path(relpath)
def theme_template(template_name):
return Path('templates') / Path(f"{template_name}.tpl")
def available_themes():
return [theme.name for theme in themes_root().iterdir() if theme.is_dir()]
def database():
return root() / Path(os.environ.get('DATABASE_PATH', 'groove_on_demand.db'))
+62
View File
@@ -0,0 +1,62 @@
import logging
import os
from collections import namedtuple
from pathlib import Path
from groove.exceptions import ThemeConfigurationError, ConfigurationError
import groove.path
Theme = namedtuple('Theme', 'path,author,author_link,version,about')
def load_theme(name=None):
name = os.environ.get('DEFAULT_THEME', name)
if not name:
raise ConfigurationError(
"It seems like DEFAULT_THEME is not set in your current environment.\n"
"Running 'groove setup' may help you fix this problem."
)
theme_path = groove.path.theme(name)
theme_info = _get_theme_info(theme_path)
return Theme(
path=theme_path,
**theme_info
)
def _get_theme_info(theme_path):
readme = theme_path / Path('README.md')
if not readme.exists:
raise ThemeConfigurationError(
"The theme is missing a required file: README.md.\n"
"Refer to the Groove On Demand documentation for help creating themes."
)
config = {'about': ''}
with readme.open() as fh:
in_credits = False
in_block = False
for line in fh.readlines():
line = line.strip()
if line == '## Credits':
in_credits = True
continue
config['about'] += line
if not in_credits:
continue
if line == '```':
if not in_block:
in_block = True
continue
break
try:
(key, value) = line.split(':', 1)
key = key.strip()
value = value.strip()
except ValueError:
logging.warn(f"Could not parse credits line: {line}")
continue
logging.debug(f"Setting theme '{key}' to '{value}'.")
config[key] = value
return config
+29 -15
View File
@@ -1,7 +1,6 @@
import logging
import json
import os
from pathlib import Path
import bottle
from bottle import HTTPResponse, template, static_file
@@ -11,9 +10,7 @@ import groove.db
from groove.auth import is_authenticated
from groove.db.manager import database_manager
from groove.playlist import Playlist
from groove.webserver import requests
# from groove.exceptions import APIHandlingException
from groove.webserver import requests, themes
server = bottle.Bottle()
@@ -42,6 +39,16 @@ def start(host: str, port: int, debug: bool) -> None: # pragma: no cover
)
def serve(template_name, theme=None, **template_args):
theme = themes.load_theme(theme)
return HTTPResponse(status=200, body=template(
str(theme.path / groove.path.theme_template(template_name)),
url=requests.url(),
theme=theme,
**template_args
))
@server.route('/')
def index():
return "Groovy."
@@ -55,7 +62,18 @@ def build():
@server.route('/static/<filepath:path>')
def server_static(filepath):
return static_file(filepath, root='static')
theme = themes.load_theme()
asset = theme.path / groove.path.theme_static(filepath)
if asset.exists():
root = asset.parent
else:
root = groove.path.static_root()
asset = groove.path.static(filepath)
if asset.is_dir():
logging.warning("Asset {asset} is a directory; returning 404.")
return HTTPResponse(status=404, body="Not found.")
logging.debug(f"Serving asset {asset.name} from {root}")
return static_file(asset.name, root=root)
@bottle.auth_basic(is_authenticated)
@@ -79,8 +97,11 @@ def serve_track(request, track_id, db):
groove.db.track.c.id == track_id
).one()
path = Path(os.environ['MEDIA_ROOT']) / Path(track['relpath'])
return static_file(path.name, root=path.parent)
path = groove.path.media(track['relpath'])
if path.exists:
return static_file(path.name, root=path.parent)
else:
return HTTPResponse(status=404, body="Not found")
@server.route('/playlist/<slug>')
@@ -97,15 +118,8 @@ def serve_playlist(slug, db):
logging.debug(playlist.as_dict['entries'])
pl = playlist.as_dict
for entry in pl['entries']:
sig = requests.encode([str(entry['track_id'])], uri='/track')
entry['url'] = f"/track/{sig}/{entry['track_id']}"
template_path = Path(os.environ['TEMPLATE_PATH']) / Path('playlist.tpl')
body = template(
str(template_path),
url=requests.url(),
playlist=pl
)
return HTTPResponse(status=200, body=body)
return serve('playlist', playlist=pl)