formattazione con black

This commit is contained in:
boyska 2019-06-25 13:49:33 +02:00
parent 0a7cad0e2d
commit d19b18eb3c
40 changed files with 949 additions and 811 deletions

2
.flake8 Normal file
View file

@ -0,0 +1,2 @@
[flake8]
max-line-length=79

View file

@ -3,23 +3,24 @@ from wtforms import StringField, validators, SubmitField
class AudioForm(Form): class AudioForm(Form):
nick = StringField('Audio nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this audio') "Audio nick",
urls = StringField('URLs', validators=[validators.required()],
validators=[validators.required()], description="A simple name to recognize this audio",
description='URL of the file to download') )
submit = SubmitField('Submit') urls = StringField(
"URLs",
validators=[validators.required()],
description="URL of the file to download",
)
submit = SubmitField("Submit")
def populate_from_audiospec(self, audiospec): def populate_from_audiospec(self, audiospec):
if 'nick' in audiospec: if "nick" in audiospec:
self.nick.data = audiospec['nick'] self.nick.data = audiospec["nick"]
if 'urls' in audiospec: if "urls" in audiospec:
self.urls.data = ';'.join(audiospec['urls']) self.urls.data = ";".join(audiospec["urls"])
def audio_receive(form): def audio_receive(form):
return { return {"kind": "http", "nick": form.nick.data, "urls": form.urls.data.split(";")}
'kind': 'http',
'nick': form.nick.data,
'urls': form.urls.data.split(';'),
}

View file

@ -6,40 +6,49 @@ from larigira.formutils import AutocompleteStringField
class AudioForm(Form): class AudioForm(Form):
nick = StringField('Audio nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this audio') "Audio nick",
path = AutocompleteStringField('dl-suggested-dirs', validators=[validators.required()],
'Path', validators=[validators.required()], description="A simple name to recognize this audio",
description='Directory to pick file from') )
maxage = StringField('Max age', path = AutocompleteStringField(
validators=[validators.required()], "dl-suggested-dirs",
description='in seconds, or human-readable ' "Path",
'(like 9w3d12h)') validators=[validators.required()],
submit = SubmitField('Submit') description="Directory to pick file from",
)
maxage = StringField(
"Max age",
validators=[validators.required()],
description="in seconds, or human-readable " "(like 9w3d12h)",
)
submit = SubmitField("Submit")
def validate_maxage(self, field): def validate_maxage(self, field):
try: try:
int(field.data) int(field.data)
except ValueError: except ValueError:
if timeparse(field.data) is None: if timeparse(field.data) is None:
raise ValidationError("maxage must either be a number " raise ValidationError(
"(in seconds) or a human-readable " "maxage must either be a number "
"string like '1h2m' or '1d12h'") "(in seconds) or a human-readable "
"string like '1h2m' or '1d12h'"
)
def populate_from_audiospec(self, audiospec): def populate_from_audiospec(self, audiospec):
if 'nick' in audiospec: if "nick" in audiospec:
self.nick.data = audiospec['nick'] self.nick.data = audiospec["nick"]
if 'path' in audiospec: if "path" in audiospec:
self.path.data = audiospec['path'] self.path.data = audiospec["path"]
if 'maxage' in audiospec: if "maxage" in audiospec:
self.maxage.data = audiospec['maxage'] self.maxage.data = audiospec["maxage"]
def audio_receive(form): def audio_receive(form):
return { return {
'kind': 'mostrecent', "kind": "mostrecent",
'nick': form.nick.data, "nick": form.nick.data,
'path': form.path.data, "path": form.path.data,
'maxage': form.maxage.data, "maxage": form.maxage.data,
'howmany': 1 "howmany": 1,
} }

View file

@ -5,33 +5,40 @@ from larigira.formutils import AutocompleteStringField
class Form(flask_wtf.Form): class Form(flask_wtf.Form):
nick = StringField('Audio nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this audio') "Audio nick",
path = AutocompleteStringField('dl-suggested-dirs', validators=[validators.required()],
'Path', validators=[validators.required()], description="A simple name to recognize this audio",
description='Full path to source directory') )
howmany = IntegerField('Number', validators=[validators.optional()], path = AutocompleteStringField(
default=1, "dl-suggested-dirs",
description='How many songs to be picked' "Path",
'from this dir; defaults to 1') validators=[validators.required()],
submit = SubmitField('Submit') description="Full path to source directory",
)
howmany = IntegerField(
"Number",
validators=[validators.optional()],
default=1,
description="How many songs to be picked" "from this dir; defaults to 1",
)
submit = SubmitField("Submit")
def populate_from_audiospec(self, audiospec): def populate_from_audiospec(self, audiospec):
if 'nick' in audiospec: if "nick" in audiospec:
self.nick.data = audiospec['nick'] self.nick.data = audiospec["nick"]
if 'paths' in audiospec: if "paths" in audiospec:
self.path.data = audiospec['paths'][0] self.path.data = audiospec["paths"][0]
if 'howmany' in audiospec: if "howmany" in audiospec:
self.howmany.data = audiospec['howmany'] self.howmany.data = audiospec["howmany"]
else: else:
self.howmany.data = 1 self.howmany.data = 1
def receive(form): def receive(form):
return { return {
'kind': 'randomdir', "kind": "randomdir",
'nick': form.nick.data, "nick": form.nick.data,
'paths': [form.path.data], "paths": [form.path.data],
'howmany': form.howmany.data or 1 "howmany": form.howmany.data or 1,
} }

View file

@ -3,38 +3,44 @@ from wtforms import StringField, validators, SubmitField, ValidationError
from larigira.formutils import AutocompleteStringField from larigira.formutils import AutocompleteStringField
class ScriptAudioForm(Form): class ScriptAudioForm(Form):
nick = StringField('Audio nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this audio') "Audio nick",
validators=[validators.required()],
description="A simple name to recognize this audio",
)
name = AutocompleteStringField( name = AutocompleteStringField(
'dl-suggested-scripts', "dl-suggested-scripts",
'Name', validators=[validators.required()], "Name",
description='filename (NOT path) of the script') validators=[validators.required()],
args = StringField('Arguments', description="filename (NOT path) of the script",
description='arguments, separated by ";"') )
submit = SubmitField('Submit') args = StringField("Arguments", description='arguments, separated by ";"')
submit = SubmitField("Submit")
def populate_from_audiospec(self, audiospec): def populate_from_audiospec(self, audiospec):
if 'nick' in audiospec: if "nick" in audiospec:
self.nick.data = audiospec['nick'] self.nick.data = audiospec["nick"]
if 'name' in audiospec: if "name" in audiospec:
self.name.data = audiospec['name'] self.name.data = audiospec["name"]
if 'args' in audiospec: if "args" in audiospec:
if type(audiospec['args']) is str: # legacy compatibility if type(audiospec["args"]) is str: # legacy compatibility
self.args.data = audiospec['args'].replace(' ', ';') self.args.data = audiospec["args"].replace(" ", ";")
else: else:
self.args.data = ';'.join(audiospec['args']) self.args.data = ";".join(audiospec["args"])
def validate_name(self, field): def validate_name(self, field):
if '/' in field.data: if "/" in field.data:
raise ValidationError("Name cannot have slashes: " raise ValidationError(
"it's a name, not a path") "Name cannot have slashes: " "it's a name, not a path"
)
def scriptaudio_receive(form): def scriptaudio_receive(form):
return { return {
'kind': 'script', "kind": "script",
'nick': form.nick.data, "nick": form.nick.data,
'name': form.name.data, "name": form.name.data,
'args': form.args.data.split(';') "args": form.args.data.split(";"),
} }

View file

@ -5,23 +5,25 @@ from larigira.formutils import AutocompleteStringField
class StaticAudioForm(Form): class StaticAudioForm(Form):
nick = StringField('Audio nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this audio') "Audio nick",
path = AutocompleteStringField('dl-suggested-files', validators=[validators.required()],
'Path', validators=[validators.required()], description="A simple name to recognize this audio",
description='Full path to audio file') )
submit = SubmitField('Submit') path = AutocompleteStringField(
"dl-suggested-files",
"Path",
validators=[validators.required()],
description="Full path to audio file",
)
submit = SubmitField("Submit")
def populate_from_audiospec(self, audiospec): def populate_from_audiospec(self, audiospec):
if 'nick' in audiospec: if "nick" in audiospec:
self.nick.data = audiospec['nick'] self.nick.data = audiospec["nick"]
if 'paths' in audiospec: if "paths" in audiospec:
self.path.data = audiospec['paths'][0] self.path.data = audiospec["paths"][0]
def staticaudio_receive(form): def staticaudio_receive(form):
return { return {"kind": "static", "nick": form.nick.data, "paths": [form.path.data]}
'kind': 'static',
'nick': form.nick.data,
'paths': [form.path.data]
}

View file

@ -4,25 +4,30 @@ import argparse
from .entrypoints_utils import get_one_entrypoint from .entrypoints_utils import get_one_entrypoint
import json import json
from logging import getLogger from logging import getLogger
log = getLogger('audiogen')
log = getLogger("audiogen")
def get_audiogenerator(kind): def get_audiogenerator(kind):
'''Messes with entrypoints to return an audiogenerator function''' """Messes with entrypoints to return an audiogenerator function"""
return get_one_entrypoint('larigira.audiogenerators', kind) return get_one_entrypoint("larigira.audiogenerators", kind)
def get_parser(): def get_parser():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="Generate audio and output paths")
description='Generate audio and output paths') parser.add_argument(
parser.add_argument('audiospec', metavar='AUDIOSPEC', type=str, nargs=1, "audiospec",
help='filename for audiospec, formatted in json') metavar="AUDIOSPEC",
type=str,
nargs=1,
help="filename for audiospec, formatted in json",
)
return parser return parser
def read_spec(fname): def read_spec(fname):
try: try:
if fname == '-': if fname == "-":
return json.load(sys.stdin) return json.load(sys.stdin)
with open(fname) as buf: with open(fname) as buf:
return json.load(buf) return json.load(buf)
@ -32,28 +37,28 @@ def read_spec(fname):
def check_spec(spec): def check_spec(spec):
if 'kind' not in spec: if "kind" not in spec:
yield "Missing field 'kind'" yield "Missing field 'kind'"
def audiogenerate(spec): def audiogenerate(spec):
gen = get_audiogenerator(spec['kind']) gen = get_audiogenerator(spec["kind"])
return tuple(gen(spec)) return tuple(gen(spec))
def main(): def main():
'''Main function for the "larigira-audiogen" executable''' """Main function for the "larigira-audiogen" executable"""
args = get_parser().parse_args() args = get_parser().parse_args()
spec = read_spec(args.audiospec[0]) spec = read_spec(args.audiospec[0])
errors = tuple(check_spec(spec)) errors = tuple(check_spec(spec))
if errors: if errors:
log.error("Errors in audiospec") log.error("Errors in audiospec")
for err in errors: for err in errors:
sys.stderr.write('Error: {}\n'.format(err)) sys.stderr.write("Error: {}\n".format(err))
sys.exit(1) sys.exit(1)
for path in audiogenerate(spec): for path in audiogenerate(spec):
print(path) print(path)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View file

@ -9,40 +9,39 @@ log = logging.getLogger(__name__)
def put(url, destdir=None, copy=False): def put(url, destdir=None, copy=False):
if url.split(':')[0] not in ('http', 'https'): if url.split(":")[0] not in ("http", "https"):
log.warning('Not a valid URL: %s', url) log.warning("Not a valid URL: %s", url)
return None return None
ext = url.split('.')[-1] ext = url.split(".")[-1]
if ext.lower() not in ('mp3', 'ogg', 'oga', 'wma', 'm4a'): if ext.lower() not in ("mp3", "ogg", "oga", "wma", "m4a"):
log.warning('Invalid format (%s) for "%s"', ext, url) log.warning('Invalid format (%s) for "%s"', ext, url)
return None return None
if not copy: if not copy:
return url return url
fname = posixpath.basename(urlparse(url).path) fname = posixpath.basename(urlparse(url).path)
# sanitize # sanitize
fname = "".join(c for c in fname fname = "".join(c for c in fname if c.isalnum() or c in list("._-")).rstrip()
if c.isalnum() or c in list('._-')).rstrip() tmp = mkstemp(suffix="." + ext, prefix="http-%s-" % fname, dir=destdir)
tmp = mkstemp(suffix='.' + ext, prefix='http-%s-' % fname, dir=destdir)
os.close(tmp[0]) os.close(tmp[0])
log.info("downloading %s -> %s", url, tmp[1]) log.info("downloading %s -> %s", url, tmp[1])
fname, headers = urllib.request.urlretrieve(url, tmp[1]) fname, headers = urllib.request.urlretrieve(url, tmp[1])
return 'file://%s' % os.path.realpath(tmp[1]) return "file://%s" % os.path.realpath(tmp[1])
def generate(spec): def generate(spec):
''' """
resolves audiospec-static resolves audiospec-static
Recognized argument is "paths" (list of static paths) Recognized argument is "paths" (list of static paths)
''' """
if 'urls' not in spec: if "urls" not in spec:
raise ValueError("Malformed audiospec: missing 'paths'") raise ValueError("Malformed audiospec: missing 'paths'")
for url in spec['urls']: for url in spec["urls"]:
ret = put(url, copy=True) ret = put(url, copy=True)
if ret is None: if ret is None:
continue continue
yield ret yield ret
generate.description = 'Fetch audio from an URL' generate.description = "Fetch audio from an URL"

View file

@ -7,6 +7,7 @@ from tempfile import mkstemp
from pytimeparse.timeparse import timeparse from pytimeparse.timeparse import timeparse
from larigira.fsutils import scan_dir, shortname from larigira.fsutils import scan_dir, shortname
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -23,36 +24,40 @@ def recent_choose(paths, howmany, minepoch):
if os.path.isfile(path): if os.path.isfile(path):
found_files[path] = get_mtime(path) found_files[path] = get_mtime(path)
elif os.path.isdir(path): elif os.path.isdir(path):
found_files.update({fname: get_mtime(fname) found_files.update({fname: get_mtime(fname) for fname in scan_dir(path)})
for fname in scan_dir(path)}) found_files = [
found_files = [(fname, mtime) (fname, mtime) for (fname, mtime) in found_files.items() if mtime >= minepoch
for (fname, mtime) in found_files.items() ]
if mtime >= minepoch]
return [fname for fname, mtime in return [
sorted(found_files, key=lambda x: x[1], reverse=True)[:howmany]] fname
for fname, mtime in sorted(found_files, key=lambda x: x[1], reverse=True)[
:howmany
]
]
def generate(spec): def generate(spec):
''' """
resolves audiospec-randomdir resolves audiospec-randomdir
Recognized arguments: Recognized arguments:
- path [mandatory] source dir - path [mandatory] source dir
- maxage [default=ignored] max age of audio files to pick - maxage [default=ignored] max age of audio files to pick
''' """
for attr in ('path', 'maxage'): for attr in ("path", "maxage"):
if attr not in spec: if attr not in spec:
raise ValueError("Malformed audiospec: missing '%s'" % attr) raise ValueError("Malformed audiospec: missing '%s'" % attr)
if spec['maxage'].strip(): if spec["maxage"].strip():
try: try:
maxage = int(spec['maxage']) maxage = int(spec["maxage"])
except ValueError: except ValueError:
maxage = timeparse(spec['maxage']) maxage = timeparse(spec["maxage"])
if maxage is None: if maxage is None:
raise ValueError("Unknown format for maxage: '{}'" raise ValueError(
.format(spec['maxage'])) "Unknown format for maxage: '{}'".format(spec["maxage"])
)
assert type(maxage) is int assert type(maxage) is int
else: else:
maxage = None maxage = None
@ -60,15 +65,16 @@ def generate(spec):
now = int(time.time()) now = int(time.time())
minepoch = 0 if maxage is None else now - maxage minepoch = 0 if maxage is None else now - maxage
picked = recent_choose([spec['path']], 1, minepoch) picked = recent_choose([spec["path"]], 1, minepoch)
for path in picked: for path in picked:
tmp = mkstemp(suffix=os.path.splitext(path)[-1], tmp = mkstemp(
prefix='randomdir-%s-' % shortname(path)) suffix=os.path.splitext(path)[-1], prefix="randomdir-%s-" % shortname(path)
)
os.close(tmp[0]) os.close(tmp[0])
shutil.copy(path, tmp[1]) shutil.copy(path, tmp[1])
log.info("copying %s -> %s", path, os.path.basename(tmp[1])) log.info("copying %s -> %s", path, os.path.basename(tmp[1]))
yield 'file://{}'.format(tmp[1]) yield "file://{}".format(tmp[1])
generate.description = 'Select most recent file inside a directory' generate.description = "Select most recent file inside a directory"

View file

@ -1,5 +1,6 @@
import logging import logging
log = logging.getLogger('mpdrandom')
log = logging.getLogger("mpdrandom")
import random import random
from mpd import MPDClient from mpd import MPDClient
@ -8,17 +9,17 @@ from .config import get_conf
def generate_by_artist(spec): def generate_by_artist(spec):
'''choose HOWMANY random artists, and for each one choose a random song''' """choose HOWMANY random artists, and for each one choose a random song"""
spec.setdefault('howmany', 1) spec.setdefault("howmany", 1)
log.info('generating') log.info("generating")
conf = get_conf() conf = get_conf()
c = MPDClient(use_unicode=True) c = MPDClient(use_unicode=True)
c.connect(conf['MPD_HOST'], conf['MPD_PORT']) c.connect(conf["MPD_HOST"], conf["MPD_PORT"])
artists = c.list('artist') artists = c.list("artist")
log.debug("got %d artists", len(artists)) log.debug("got %d artists", len(artists))
if not artists: if not artists:
raise ValueError("no artists in your mpd database") raise ValueError("no artists in your mpd database")
for _ in range(spec['howmany']): for _ in range(spec["howmany"]):
artist = random.choice(artists) artist = random.choice(artists)
yield random.choice(c.find('artist', artist))['file'] yield random.choice(c.find("artist", artist))["file"]

View file

@ -6,6 +6,7 @@ from tempfile import mkstemp
from pathlib import Path from pathlib import Path
from larigira.fsutils import scan_dir_audio, shortname, is_audio from larigira.fsutils import scan_dir_audio, shortname, is_audio
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -23,36 +24,37 @@ def candidates(paths):
def generate(spec): def generate(spec):
''' """
resolves audiospec-randomdir resolves audiospec-randomdir
Recognized arguments: Recognized arguments:
- paths [mandatory] list of source paths - paths [mandatory] list of source paths
- howmany [default=1] number of audio files to pick - howmany [default=1] number of audio files to pick
''' """
spec.setdefault('howmany', 1) spec.setdefault("howmany", 1)
for attr in ('paths', ): for attr in ("paths",):
if attr not in spec: if attr not in spec:
raise ValueError("Malformed audiospec: missing '%s'" % attr) raise ValueError("Malformed audiospec: missing '%s'" % attr)
found_files = candidates([Path(p) for p in spec['paths']]) found_files = candidates([Path(p) for p in spec["paths"]])
picked = random.sample(found_files, int(spec['howmany'])) picked = random.sample(found_files, int(spec["howmany"]))
nick = spec.get('nick', '') nick = spec.get("nick", "")
if not nick: if not nick:
if hasattr(spec, 'eid'): if hasattr(spec, "eid"):
nick = spec.eid nick = spec.eid
else: else:
nick = 'NONICK' nick = "NONICK"
for path in picked: for path in picked:
tmp = mkstemp(suffix=os.path.splitext(path)[-1], tmp = mkstemp(
prefix='randomdir-%s-%s-' % (shortname(nick), suffix=os.path.splitext(path)[-1],
shortname(path))) prefix="randomdir-%s-%s-" % (shortname(nick), shortname(path)),
)
os.close(tmp[0]) os.close(tmp[0])
shutil.copy(path, tmp[1]) shutil.copy(path, tmp[1])
log.info("copying %s -> %s", path, os.path.basename(tmp[1])) log.info("copying %s -> %s", path, os.path.basename(tmp[1]))
yield "file://{}".format(tmp[1]) yield "file://{}".format(tmp[1])
generate.description = 'Picks random files from a specified directory' generate.description = "Picks random files from a specified directory"

View file

@ -1,4 +1,4 @@
''' """
script audiogenerator: uses an external program to generate audio URIs script audiogenerator: uses an external program to generate audio URIs
a script can be any valid executable in a script can be any valid executable in
@ -14,64 +14,65 @@ The output MUST be UTF-8-encoded.
Empty lines will be skipped. stderr will be logged, so please be careful. any Empty lines will be skipped. stderr will be logged, so please be careful. any
non-zero exit code will result in no files being added.and an exception being non-zero exit code will result in no files being added.and an exception being
logged. logged.
''' """
import logging import logging
import os import os
import subprocess import subprocess
from .config import get_conf from .config import get_conf
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def generate(spec): def generate(spec):
''' """
Recognized arguments (fields in spec): Recognized arguments (fields in spec):
- name [mandatory] script name - name [mandatory] script name
- args [default=empty] arguments, colon-separated - args [default=empty] arguments, colon-separated
''' """
conf = get_conf() conf = get_conf()
spec.setdefault('args', '') spec.setdefault("args", "")
if type(spec['args']) is str: if type(spec["args"]) is str:
args = spec['args'].split(';') args = spec["args"].split(";")
args = list(spec['args']) args = list(spec["args"])
for attr in ('name', ): for attr in ("name",):
if attr not in spec: if attr not in spec:
raise ValueError("Malformed audiospec: missing '%s'" % attr) raise ValueError("Malformed audiospec: missing '%s'" % attr)
if '/' in spec['name']: if "/" in spec["name"]:
raise ValueError("Script name is a filename, not a path ({} provided)" raise ValueError(
.format(spec['name'])) "Script name is a filename, not a path ({} provided)".format(spec["name"])
scriptpath = os.path.join(conf['SCRIPTS_PATH'], spec['name']) )
scriptpath = os.path.join(conf["SCRIPTS_PATH"], spec["name"])
if not os.path.exists(scriptpath): if not os.path.exists(scriptpath):
raise ValueError("Script %s not found", spec['name']) raise ValueError("Script %s not found", spec["name"])
if not os.access(scriptpath, os.R_OK | os.X_OK): if not os.access(scriptpath, os.R_OK | os.X_OK):
raise ValueError("Insufficient privileges for script %s" % scriptpath) raise ValueError("Insufficient privileges for script %s" % scriptpath)
if os.stat(scriptpath).st_uid != os.getuid(): if os.stat(scriptpath).st_uid != os.getuid():
raise ValueError("Script %s owned by %d, should be owned by %d" raise ValueError(
% (spec['name'], os.stat(scriptpath).st_uid, "Script %s owned by %d, should be owned by %d"
os.getuid())) % (spec["name"], os.stat(scriptpath).st_uid, os.getuid())
try:
log.info('Going to run %s', [scriptpath] + args)
env = dict(
HOME=os.environ['HOME'],
PATH=os.environ['PATH'],
MPD_HOST=conf['MPD_HOST'],
MPD_PORT=str(conf['MPD_PORT'])
) )
if 'TMPDIR' in os.environ: try:
env['TMPDIR'] = os.environ['TMPDIR'] log.info("Going to run %s", [scriptpath] + args)
out = subprocess.check_output([scriptpath] + args, env = dict(
env=env, HOME=os.environ["HOME"],
cwd='/') PATH=os.environ["PATH"],
MPD_HOST=conf["MPD_HOST"],
MPD_PORT=str(conf["MPD_PORT"]),
)
if "TMPDIR" in os.environ:
env["TMPDIR"] = os.environ["TMPDIR"]
out = subprocess.check_output([scriptpath] + args, env=env, cwd="/")
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
log.error("Error %d when running script %s", log.error("Error %d when running script %s", exc.returncode, spec["name"])
exc.returncode, spec['name'])
return [] return []
out = out.decode('utf-8') out = out.decode("utf-8")
out = [p for p in out.split('\n') if p] out = [p for p in out.split("\n") if p]
logging.debug('Script %s produced %d files', spec['name'], len(out)) logging.debug("Script %s produced %d files", spec["name"], len(out))
return out return out
generate.description = 'Generate audio through an external script. ' \
'Experts only.'
generate.description = "Generate audio through an external script. " "Experts only."

View file

@ -4,28 +4,30 @@ import shutil
from tempfile import mkstemp from tempfile import mkstemp
from larigira.fsutils import shortname from larigira.fsutils import shortname
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def generate(spec): def generate(spec):
''' """
resolves audiospec-static resolves audiospec-static
Recognized argument is "paths" (list of static paths) Recognized argument is "paths" (list of static paths)
''' """
if 'paths' not in spec: if "paths" not in spec:
raise ValueError("Malformed audiospec: missing 'paths'") raise ValueError("Malformed audiospec: missing 'paths'")
for path in spec['paths']: for path in spec["paths"]:
if not os.path.exists(path): if not os.path.exists(path):
log.warning("Can't find requested path: %s", path) log.warning("Can't find requested path: %s", path)
continue continue
tmp = mkstemp(suffix=os.path.splitext(path)[-1], tmp = mkstemp(
prefix='static-%s-' % shortname(path)) suffix=os.path.splitext(path)[-1], prefix="static-%s-" % shortname(path)
)
os.close(tmp[0]) os.close(tmp[0])
log.info("copying %s -> %s", path, os.path.basename(tmp[1])) log.info("copying %s -> %s", path, os.path.basename(tmp[1]))
shutil.copy(path, tmp[1]) shutil.copy(path, tmp[1])
yield 'file://{}'.format(tmp[1]) yield "file://{}".format(tmp[1])
generate.description = 'Picks always the same specified file' generate.description = "Picks always the same specified file"

View file

@ -1,5 +1,6 @@
from tinydb import TinyDB from tinydb import TinyDB
class EventModel(object): class EventModel(object):
def __init__(self, uri): def __init__(self, uri):
self.uri = uri self.uri = uri
@ -10,8 +11,8 @@ class EventModel(object):
if self.db is not None: if self.db is not None:
self.db.close() self.db.close()
self.db = TinyDB(self.uri, indent=2) self.db = TinyDB(self.uri, indent=2)
self._actions = self.db.table('actions') self._actions = self.db.table("actions")
self._alarms = self.db.table('alarms') self._alarms = self.db.table("alarms")
def get_action_by_id(self, action_id): def get_action_by_id(self, action_id):
return self._actions.get(eid=action_id) return self._actions.get(eid=action_id)
@ -20,7 +21,7 @@ class EventModel(object):
return self._alarms.get(eid=alarm_id) return self._alarms.get(eid=alarm_id)
def get_actions_by_alarm(self, alarm): def get_actions_by_alarm(self, alarm):
for action_id in alarm.get('actions', []): for action_id in alarm.get("actions", []):
action = self.get_action_by_id(action_id) action = self.get_action_by_id(action_id)
if action is None: if action is None:
continue continue
@ -39,7 +40,7 @@ class EventModel(object):
def add_event(self, alarm, actions): def add_event(self, alarm, actions):
action_ids = [self.add_action(a) for a in actions] action_ids = [self.add_action(a) for a in actions]
alarm['actions'] = action_ids alarm["actions"] = action_ids
return self._alarms.insert(alarm) return self._alarms.insert(alarm)
def add_action(self, action): def add_action(self, action):

View file

@ -1,14 +1,23 @@
''' """
This module contains a flask blueprint for db administration stuff This module contains a flask blueprint for db administration stuff
Templates are self-contained in this directory Templates are self-contained in this directory
''' """
from __future__ import print_function from __future__ import print_function
from datetime import datetime, timedelta, time from datetime import datetime, timedelta, time
from collections import defaultdict from collections import defaultdict
from flask import current_app, Blueprint, render_template, jsonify, abort, \ from flask import (
request, redirect, url_for, flash current_app,
Blueprint,
render_template,
jsonify,
abort,
request,
redirect,
url_for,
flash,
)
from larigira.entrypoints_utils import get_avail_entrypoints from larigira.entrypoints_utils import get_avail_entrypoints
from larigira.audiogen import get_audiogenerator from larigira.audiogen import get_audiogenerator
@ -17,56 +26,63 @@ from larigira.timegen import get_timegenerator, timegenerate
from larigira import forms from larigira import forms
from larigira.config import get_conf from larigira.config import get_conf
from .suggestions import get_suggestions from .suggestions import get_suggestions
db = Blueprint('db', __name__,
url_prefix=get_conf()['ROUTE_PREFIX'] + '/db', db = Blueprint(
template_folder='templates') "db",
__name__,
url_prefix=get_conf()["ROUTE_PREFIX"] + "/db",
template_folder="templates",
)
def request_wants_json(): def request_wants_json():
best = request.accept_mimetypes \ best = request.accept_mimetypes.best_match(["application/json", "text/html"])
.best_match(['application/json', 'text/html']) return (
return best == 'application/json' and \ best == "application/json"
request.accept_mimetypes[best] > \ and request.accept_mimetypes[best] > request.accept_mimetypes["text/html"]
request.accept_mimetypes['text/html'] )
def get_model(): def get_model():
return current_app.larigira.controller.monitor.model return current_app.larigira.controller.monitor.model
@db.route('/') @db.route("/")
def home(): def home():
return render_template('dbadmin_base.html') return render_template("dbadmin_base.html")
@db.route('/list') @db.route("/list")
def events_list(): def events_list():
model = current_app.larigira.controller.monitor.model model = current_app.larigira.controller.monitor.model
alarms = tuple(model.get_all_alarms()) alarms = tuple(model.get_all_alarms())
events = [(alarm, model.get_actions_by_alarm(alarm)) events = [(alarm, model.get_actions_by_alarm(alarm)) for alarm in alarms]
for alarm in alarms] return render_template("list.html", events=events)
return render_template('list.html', events=events)
@db.route('/calendar') @db.route("/calendar")
def events_calendar(): def events_calendar():
model = current_app.larigira.controller.monitor.model model = current_app.larigira.controller.monitor.model
today = datetime.now().date() today = datetime.now().date()
maxdays = 30 maxdays = 30
# {date: {datetime: [(alarm1,actions1), (alarm2,actions2)]}} # {date: {datetime: [(alarm1,actions1), (alarm2,actions2)]}}
days = defaultdict(lambda: defaultdict(list)) days = defaultdict(lambda: defaultdict(list))
freq_threshold = get_conf()['UI_CALENDAR_FREQUENCY_THRESHOLD'] freq_threshold = get_conf()["UI_CALENDAR_FREQUENCY_THRESHOLD"]
for alarm in model.get_all_alarms(): for alarm in model.get_all_alarms():
if freq_threshold and alarm['kind'] == 'frequency' and \ if (
FrequencyAlarm(alarm).interval < freq_threshold: freq_threshold
and alarm["kind"] == "frequency"
and FrequencyAlarm(alarm).interval < freq_threshold
):
continue continue
actions = tuple(model.get_actions_by_alarm(alarm)) actions = tuple(model.get_actions_by_alarm(alarm))
if not actions: if not actions:
continue continue
t = datetime.fromtimestamp(int(today.strftime('%s'))) t = datetime.fromtimestamp(int(today.strftime("%s")))
for t in timegenerate(alarm, now=t, howmany=maxdays): for t in timegenerate(alarm, now=t, howmany=maxdays):
if t is None or \ if t is None or t > datetime.combine(
t > datetime.combine(today+timedelta(days=maxdays), time()): today + timedelta(days=maxdays), time()
):
break break
days[t.date()][t].append((alarm, actions)) days[t.date()][t].append((alarm, actions))
@ -75,105 +91,101 @@ def events_calendar():
for d in sorted(days.keys()): for d in sorted(days.keys()):
weeks[d.isocalendar()[:2]].append(d) weeks[d.isocalendar()[:2]].append(d)
return render_template('calendar.html', days=days, weeks=weeks) return render_template("calendar.html", days=days, weeks=weeks)
@db.route('/add/time') @db.route("/add/time")
def addtime(): def addtime():
kinds = get_avail_entrypoints('larigira.timeform_create') kinds = get_avail_entrypoints("larigira.timeform_create")
def gen_info(gen): def gen_info(gen):
return dict(description=getattr(gen, 'description', '')) return dict(description=getattr(gen, "description", ""))
info = {kind: gen_info(get_timegenerator(kind))
for kind in kinds} info = {kind: gen_info(get_timegenerator(kind)) for kind in kinds}
return render_template('add_time.html', kinds=kinds, info=info) return render_template("add_time.html", kinds=kinds, info=info)
@db.route('/edit/time/<int:alarmid>', methods=['GET', 'POST']) @db.route("/edit/time/<int:alarmid>", methods=["GET", "POST"])
def edit_time(alarmid): def edit_time(alarmid):
model = get_model() model = get_model()
timespec = model.get_alarm_by_id(alarmid) timespec = model.get_alarm_by_id(alarmid)
kind = timespec['kind'] kind = timespec["kind"]
Form, receiver = tuple(forms.get_timeform(kind)) Form, receiver = tuple(forms.get_timeform(kind))
form = Form() form = Form()
if request.method == 'GET': if request.method == "GET":
form.populate_from_timespec(timespec) form.populate_from_timespec(timespec)
if request.method == 'POST' and form.validate(): if request.method == "POST" and form.validate():
data = receiver(form) data = receiver(form)
model.update_alarm(alarmid, data) model.update_alarm(alarmid, data)
model.reload() model.reload()
return redirect(url_for('db.events_list', return redirect(url_for("db.events_list", _anchor="event-%d" % alarmid))
_anchor='event-%d' % alarmid)) return render_template(
return render_template('add_time_kind.html', "add_time_kind.html", form=form, kind=kind, mode="edit", alarmid=alarmid
form=form, )
kind=kind,
mode='edit',
alarmid=alarmid,
)
@db.route('/add/time/<kind>', methods=['GET', 'POST']) @db.route("/add/time/<kind>", methods=["GET", "POST"])
def addtime_kind(kind): def addtime_kind(kind):
Form, receiver = tuple(forms.get_timeform(kind)) Form, receiver = tuple(forms.get_timeform(kind))
form = Form() form = Form()
if request.method == 'POST' and form.validate(): if request.method == "POST" and form.validate():
data = receiver(form) data = receiver(form)
eid = get_model().add_alarm(data) eid = get_model().add_alarm(data)
return redirect(url_for('db.edit_event', alarmid=eid)) return redirect(url_for("db.edit_event", alarmid=eid))
return render_template('add_time_kind.html', return render_template("add_time_kind.html", form=form, kind=kind, mode="add")
form=form, kind=kind, mode='add')
@db.route('/add/audio') @db.route("/add/audio")
def addaudio(): def addaudio():
kinds = get_avail_entrypoints('larigira.audioform_create') kinds = get_avail_entrypoints("larigira.audioform_create")
def gen_info(gen): def gen_info(gen):
return dict(description=getattr(gen, 'description', '')) return dict(description=getattr(gen, "description", ""))
info = {kind: gen_info(get_audiogenerator(kind))
for kind in kinds} info = {kind: gen_info(get_audiogenerator(kind)) for kind in kinds}
return render_template('add_audio.html', kinds=kinds, info=info) return render_template("add_audio.html", kinds=kinds, info=info)
@db.route('/add/audio/<kind>', methods=['GET', 'POST']) @db.route("/add/audio/<kind>", methods=["GET", "POST"])
def addaudio_kind(kind): def addaudio_kind(kind):
Form, receiver = tuple(forms.get_audioform(kind)) Form, receiver = tuple(forms.get_audioform(kind))
form = Form() form = Form()
if request.method == 'POST' and form.validate(): if request.method == "POST" and form.validate():
data = receiver(form) data = receiver(form)
model = current_app.larigira.controller.monitor.model model = current_app.larigira.controller.monitor.model
eid = model.add_action(data) eid = model.add_action(data)
return jsonify(dict(inserted=eid, data=data)) return jsonify(dict(inserted=eid, data=data))
return render_template('add_audio_kind.html', form=form, kind=kind, return render_template(
suggestions=get_suggestions() "add_audio_kind.html", form=form, kind=kind, suggestions=get_suggestions()
) )
@db.route('/edit/audio/<int:actionid>', methods=['GET', 'POST']) @db.route("/edit/audio/<int:actionid>", methods=["GET", "POST"])
def edit_audio(actionid): def edit_audio(actionid):
model = get_model() model = get_model()
audiospec = model.get_action_by_id(actionid) audiospec = model.get_action_by_id(actionid)
kind = audiospec['kind'] kind = audiospec["kind"]
Form, receiver = tuple(forms.get_audioform(kind)) Form, receiver = tuple(forms.get_audioform(kind))
form = Form() form = Form()
if request.method == 'GET': if request.method == "GET":
form.populate_from_audiospec(audiospec) form.populate_from_audiospec(audiospec)
if request.method == 'POST' and form.validate(): if request.method == "POST" and form.validate():
data = receiver(form) data = receiver(form)
model.update_action(actionid, data) model.update_action(actionid, data)
model.reload() model.reload()
return redirect(url_for('db.events_list')) return redirect(url_for("db.events_list"))
return render_template('add_audio_kind.html', return render_template(
form=form, "add_audio_kind.html",
kind=kind, form=form,
mode='edit', kind=kind,
suggestions=get_suggestions() mode="edit",
) suggestions=get_suggestions(),
)
@db.route('/edit/event/<alarmid>') @db.route("/edit/event/<alarmid>")
def edit_event(alarmid): def edit_event(alarmid):
model = current_app.larigira.controller.monitor.model model = current_app.larigira.controller.monitor.model
alarm = model.get_alarm_by_id(int(alarmid)) alarm = model.get_alarm_by_id(int(alarmid))
@ -181,35 +193,37 @@ def edit_event(alarmid):
abort(404) abort(404)
allactions = model.get_all_actions() allactions = model.get_all_actions()
actions = tuple(model.get_actions_by_alarm(alarm)) actions = tuple(model.get_actions_by_alarm(alarm))
return render_template('edit_event.html', return render_template(
alarm=alarm, all_actions=allactions, "edit_event.html",
actions=actions, alarm=alarm,
routeprefix=get_conf()['ROUTE_PREFIX'] all_actions=allactions,
) actions=actions,
routeprefix=get_conf()["ROUTE_PREFIX"],
)
@db.route('/api/alarm/<alarmid>/actions', methods=['POST']) @db.route("/api/alarm/<alarmid>/actions", methods=["POST"])
def change_actions(alarmid): def change_actions(alarmid):
new_actions = request.form.getlist('actions[]') new_actions = request.form.getlist("actions[]")
if new_actions is None: if new_actions is None:
new_actions = [] new_actions = []
model = current_app.larigira.controller.monitor.model model = current_app.larigira.controller.monitor.model
ret = model.update_alarm(int(alarmid), ret = model.update_alarm(
new_fields={'actions': [int(a) for a in int(alarmid), new_fields={"actions": [int(a) for a in new_actions]}
new_actions]}) )
return jsonify(dict(updated=alarmid, ret=ret)) return jsonify(dict(updated=alarmid, ret=ret))
@db.route('/api/alarm/<int:alarmid>/delete', methods=['POST']) @db.route("/api/alarm/<int:alarmid>/delete", methods=["POST"])
def delete_alarm(alarmid): def delete_alarm(alarmid):
model = current_app.larigira.controller.monitor.model model = current_app.larigira.controller.monitor.model
try: try:
alarm = model.get_alarm_by_id(int(alarmid)) alarm = model.get_alarm_by_id(int(alarmid))
print(alarm['nick']) print(alarm["nick"])
model.delete_alarm(alarmid) model.delete_alarm(alarmid)
except KeyError: except KeyError:
abort(404) abort(404)
if request_wants_json(): if request_wants_json():
return jsonify(dict(deleted=alarmid)) return jsonify(dict(deleted=alarmid))
flash('Evento %d `%s` cancellato' % (alarmid, alarm['nick']) ) flash("Evento %d `%s` cancellato" % (alarmid, alarm["nick"]))
return redirect(url_for('db.events_list')) return redirect(url_for("db.events_list"))

View file

@ -7,22 +7,23 @@ from larigira.fsutils import scan_dir_audio
def get_suggested_files(): def get_suggested_files():
if not get_conf()['FILE_PATH_SUGGESTION']: if not get_conf()["FILE_PATH_SUGGESTION"]:
return [] return []
if current_app.cache.has('dbadmin.get_suggested_files'): if current_app.cache.has("dbadmin.get_suggested_files"):
return current_app.cache.get('dbadmin.get_suggested_files') return current_app.cache.get("dbadmin.get_suggested_files")
current_app.logger.debug('get_suggested_files MISS in cache') current_app.logger.debug("get_suggested_files MISS in cache")
files = [] files = []
for path in get_conf()['FILE_PATH_SUGGESTION']: for path in get_conf()["FILE_PATH_SUGGESTION"]:
if not os.path.isdir(path): if not os.path.isdir(path):
current_app.logger.warning('Invalid suggestion path: %s' % path) current_app.logger.warning("Invalid suggestion path: %s" % path)
continue continue
pathfiles = scan_dir_audio(path) pathfiles = scan_dir_audio(path)
files.extend(pathfiles) files.extend(pathfiles)
current_app.logger.debug('Suggested files: %s' % ', '.join(files)) current_app.logger.debug("Suggested files: %s" % ", ".join(files))
current_app.cache.set('dbadmin.get_suggested_files', files, current_app.cache.set(
timeout=600) # ten minutes "dbadmin.get_suggested_files", files, timeout=600
) # ten minutes
return files return files
@ -40,11 +41,14 @@ def get_suggested_dirs():
def get_suggested_scripts(): def get_suggested_scripts():
base = get_conf()['SCRIPTS_PATH'] base = get_conf()["SCRIPTS_PATH"]
if not base or not os.path.isdir(base): if not base or not os.path.isdir(base):
return [] return []
fnames = [f for f in os.listdir(base) fnames = [
if os.access(os.path.join(base, f), os.R_OK | os.X_OK)] f
for f in os.listdir(base)
if os.access(os.path.join(base, f), os.R_OK | os.X_OK)
]
return fnames return fnames
@ -53,10 +57,4 @@ def get_suggestions():
if len(files) > 200: if len(files) > 200:
current_app.logger.warning("Too many suggested files, cropping") current_app.logger.warning("Too many suggested files, cropping")
files = files[:200] files = files[:200]
return dict( return dict(files=files, dirs=get_suggested_dirs(), scripts=get_suggested_scripts())
files=files,
dirs=get_suggested_dirs(),
scripts=get_suggested_scripts(),
)

View file

@ -1,13 +1,14 @@
from logging import getLogger from logging import getLogger
log = getLogger('entrypoints_utils')
log = getLogger("entrypoints_utils")
from pkg_resources import iter_entry_points from pkg_resources import iter_entry_points
def get_one_entrypoint(group, kind): def get_one_entrypoint(group, kind):
'''Messes with entrypoints to return an entrypoint of a given group/kind''' """Messes with entrypoints to return an entrypoint of a given group/kind"""
points = tuple(iter_entry_points(group=group, name=kind)) points = tuple(iter_entry_points(group=group, name=kind))
if not points: if not points:
raise ValueError('cant find an entrypoint %s:%s' % (group, kind)) raise ValueError("cant find an entrypoint %s:%s" % (group, kind))
if len(points) > 1: if len(points) > 1:
log.warning("Found more than one timeform for %s:%s", group, kind) log.warning("Found more than one timeform for %s:%s", group, kind)
return points[0].load() return points[0].load()

View file

@ -1,5 +1,6 @@
from __future__ import print_function from __future__ import print_function
from gevent import monkey from gevent import monkey
monkey.patch_all(subprocess=True) monkey.patch_all(subprocess=True)
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -12,10 +13,11 @@ from .timegen import timegenerate
from .audiogen import audiogenerate from .audiogen import audiogenerate
from .db import EventModel from .db import EventModel
logging.getLogger('mpd').setLevel(logging.WARNING) logging.getLogger("mpd").setLevel(logging.WARNING)
class Monitor(ParentedLet): class Monitor(ParentedLet):
''' """
Manages timegenerators and audiogenerators for DB events Manages timegenerators and audiogenerators for DB events
The mechanism is partially based on ticks, partially on scheduled actions. The mechanism is partially based on ticks, partially on scheduled actions.
@ -30,23 +32,25 @@ class Monitor(ParentedLet):
The scheduling mechanism allows for more precision, catching exactly the The scheduling mechanism allows for more precision, catching exactly the
right time. Being accurate only with ticks would have required very right time. Being accurate only with ticks would have required very
frequent ticks, which is cpu-intensive. frequent ticks, which is cpu-intensive.
''' """
def __init__(self, parent_queue, conf): def __init__(self, parent_queue, conf):
ParentedLet.__init__(self, parent_queue) ParentedLet.__init__(self, parent_queue)
self.log = logging.getLogger(self.__class__.__name__) self.log = logging.getLogger(self.__class__.__name__)
self.running = {} self.running = {}
self.conf = conf self.conf = conf
self.q = Queue() self.q = Queue()
self.model = EventModel(self.conf['DB_URI']) self.model = EventModel(self.conf["DB_URI"])
self.ticker = Timer(int(self.conf['EVENT_TICK_SECS']) * 1000, self.q) self.ticker = Timer(int(self.conf["EVENT_TICK_SECS"]) * 1000, self.q)
def _alarm_missing_time(self, timespec): def _alarm_missing_time(self, timespec):
now = datetime.now() + timedelta(seconds=self.conf['CACHING_TIME']) now = datetime.now() + timedelta(seconds=self.conf["CACHING_TIME"])
try: try:
when = next(timegenerate(timespec, now=now)) when = next(timegenerate(timespec, now=now))
except: except:
logging.exception("Could not generate " logging.exception(
"an alarm from timespec %s", timespec) "Could not generate " "an alarm from timespec %s", timespec
)
if when is None: if when is None:
# expired # expired
return None return None
@ -55,12 +59,12 @@ class Monitor(ParentedLet):
return delta return delta
def on_tick(self): def on_tick(self):
''' """
this is called every EVENT_TICK_SECS. this is called every EVENT_TICK_SECS.
Checks every event in the DB (which might be slightly CPU-intensive, so Checks every event in the DB (which might be slightly CPU-intensive, so
it is advisable to run it in its own greenlet); if the event is "near it is advisable to run it in its own greenlet); if the event is "near
enough", schedule it; if it is too far, or already expired, ignore it. enough", schedule it; if it is too far, or already expired, ignore it.
''' """
self.model.reload() self.model.reload()
for alarm in self.model.get_all_alarms(): for alarm in self.model.get_all_alarms():
actions = list(self.model.get_actions_by_alarm(alarm)) actions = list(self.model.get_actions_by_alarm(alarm))
@ -71,75 +75,84 @@ class Monitor(ParentedLet):
# but it is "tricky"; any small delay would cause the event to be # but it is "tricky"; any small delay would cause the event to be
# missed # missed
if delta is None: if delta is None:
self.log.debug('Skipping event %s: will never ring', self.log.debug(
alarm.get('nick', alarm.eid)) "Skipping event %s: will never ring", alarm.get("nick", alarm.eid)
elif delta <= 2*self.conf['EVENT_TICK_SECS']: )
self.log.debug('Scheduling event %s (%ds) => %s', elif delta <= 2 * self.conf["EVENT_TICK_SECS"]:
alarm.get('nick', alarm.eid), self.log.debug(
delta, "Scheduling event %s (%ds) => %s",
[a.get('nick', a.eid) for a in actions] alarm.get("nick", alarm.eid),
) delta,
[a.get("nick", a.eid) for a in actions],
)
self.schedule(alarm, actions, delta) self.schedule(alarm, actions, delta)
else: else:
self.log.debug('Skipping event %s too far (%ds)', self.log.debug(
alarm.get('nick', alarm.eid), "Skipping event %s too far (%ds)",
delta, alarm.get("nick", alarm.eid),
) delta,
)
def schedule(self, timespec, audiospecs, delta=None): def schedule(self, timespec, audiospecs, delta=None):
''' """
prepare an event to be run at a specified time with the specified prepare an event to be run at a specified time with the specified
actions; the DB won't be read anymore after this call. actions; the DB won't be read anymore after this call.
This means that this call should not be done too early, or any update This means that this call should not be done too early, or any update
to the DB will be ignored. to the DB will be ignored.
''' """
if delta is None: if delta is None:
delta = self._alarm_missing_time(timespec) delta = self._alarm_missing_time(timespec)
audiogen = gevent.spawn_later(delta, audiogen = gevent.spawn_later(delta, self.process_action, timespec, audiospecs)
self.process_action,
timespec, audiospecs)
audiogen.parent_greenlet = self audiogen.parent_greenlet = self
audiogen.doc = 'Will wait {} seconds, then generate audio "{}"'.format( audiogen.doc = 'Will wait {} seconds, then generate audio "{}"'.format(
delta, delta, ",".join(aspec.get("nick", "") for aspec in audiospecs)
','.join(aspec.get('nick', '') for aspec in audiospecs),
) )
self.running[timespec.eid] = { self.running[timespec.eid] = {
'greenlet': audiogen, "greenlet": audiogen,
'running_time': datetime.now() + timedelta(seconds=delta), "running_time": datetime.now() + timedelta(seconds=delta),
'timespec': timespec, "timespec": timespec,
'audiospecs': audiospecs "audiospecs": audiospecs,
} }
def process_action(self, timespec, audiospecs): def process_action(self, timespec, audiospecs):
'''Generate audio and submit it to Controller''' """Generate audio and submit it to Controller"""
if timespec.eid in self.running: if timespec.eid in self.running:
del self.running[timespec.eid] del self.running[timespec.eid]
else: else:
self.log.warning('Timespec %s completed but not in running ' self.log.warning(
'registry; this is most likely a bug', "Timespec %s completed but not in running "
timespec.get('nick', timespec.eid)) "registry; this is most likely a bug",
timespec.get("nick", timespec.eid),
)
uris = [] uris = []
for audiospec in audiospecs: for audiospec in audiospecs:
try: try:
uris.extend(audiogenerate(audiospec)) uris.extend(audiogenerate(audiospec))
except Exception as exc: except Exception as exc:
self.log.error('audiogenerate for <%s> failed; reason: %s', self.log.error(
str(audiospec), str(exc)) "audiogenerate for <%s> failed; reason: %s",
self.send_to_parent('uris_enqueue', str(audiospec),
dict(uris=uris, str(exc),
timespec=timespec, )
audiospecs=audiospecs, self.send_to_parent(
aids=[a.eid for a in audiospecs])) "uris_enqueue",
dict(
uris=uris,
timespec=timespec,
audiospecs=audiospecs,
aids=[a.eid for a in audiospecs],
),
)
def _run(self): def _run(self):
self.ticker.start() self.ticker.start()
gevent.spawn(self.on_tick) gevent.spawn(self.on_tick)
while True: while True:
value = self.q.get() value = self.q.get()
kind = value['kind'] kind = value["kind"]
if kind in ('forcetick', 'timer'): if kind in ("forcetick", "timer"):
gevent.spawn(self.on_tick) gevent.spawn(self.on_tick)
else: else:
self.log.warning("Unknown message: %s", str(value)) self.log.warning("Unknown message: %s", str(value))

View file

@ -14,25 +14,28 @@ def main_list(args):
def main_add(args): def main_add(args):
m = EventModel(args.file) m = EventModel(args.file)
m.add_event(dict(kind='frequency', interval=args.interval, start=1), m.add_event(
[dict(kind='mpd', howmany=1)] dict(kind="frequency", interval=args.interval, start=1),
) [dict(kind="mpd", howmany=1)],
)
def main(): def main():
conf = get_conf() conf = get_conf()
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
p.add_argument('-f', '--file', help="Filepath for DB", required=False, p.add_argument(
default=conf['DB_URI']) "-f", "--file", help="Filepath for DB", required=False, default=conf["DB_URI"]
)
sub = p.add_subparsers() sub = p.add_subparsers()
sub_list = sub.add_parser('list') sub_list = sub.add_parser("list")
sub_list.set_defaults(func=main_list) sub_list.set_defaults(func=main_list)
sub_add = sub.add_parser('add') sub_add = sub.add_parser("add")
sub_add.add_argument('--interval', type=int, default=3600) sub_add.add_argument("--interval", type=int, default=3600)
sub_add.set_defaults(func=main_add) sub_add.set_defaults(func=main_add)
args = p.parse_args() args = p.parse_args()
args.func(args) args.func(args)
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

View file

@ -2,13 +2,14 @@ import gevent
class ParentedLet(gevent.Greenlet): class ParentedLet(gevent.Greenlet):
''' """
ParentedLet is just a helper subclass that will help you when your ParentedLet is just a helper subclass that will help you when your
greenlet main duty is to "signal" things to a parent_queue. greenlet main duty is to "signal" things to a parent_queue.
It won't save you much code, but "standardize" messages and make explicit It won't save you much code, but "standardize" messages and make explicit
the role of that greenlet the role of that greenlet
''' """
def __init__(self, queue): def __init__(self, queue):
gevent.Greenlet.__init__(self) gevent.Greenlet.__init__(self)
self.parent_queue = queue self.parent_queue = queue
@ -17,36 +18,38 @@ class ParentedLet(gevent.Greenlet):
def parent_msg(self, kind, *args): def parent_msg(self, kind, *args):
return { return {
'emitter': self, "emitter": self,
'class': self.__class__.__name__, "class": self.__class__.__name__,
'tracker': self.tracker, "tracker": self.tracker,
'kind': kind, "kind": kind,
'args': args "args": args,
} }
def send_to_parent(self, kind, *args): def send_to_parent(self, kind, *args):
self.parent_queue.put(self.parent_msg(kind, *args)) self.parent_queue.put(self.parent_msg(kind, *args))
def _run(self): def _run(self):
if not hasattr(self, 'do_business'): if not hasattr(self, "do_business"):
raise Exception("do_business method not implemented by %s" % raise Exception(
self.__class__.__name__) "do_business method not implemented by %s" % self.__class__.__name__
)
for msg in self.do_business(): for msg in self.do_business():
self.send_to_parent(*msg) self.send_to_parent(*msg)
class Timer(ParentedLet): class Timer(ParentedLet):
'''continously sleeps some time, then send a "timer" message to parent''' """continously sleeps some time, then send a "timer" message to parent"""
def __init__(self, milliseconds, queue): def __init__(self, milliseconds, queue):
ParentedLet.__init__(self, queue) ParentedLet.__init__(self, queue)
self.ms = milliseconds self.ms = milliseconds
def parent_msg(self, kind, *args): def parent_msg(self, kind, *args):
msg = ParentedLet.parent_msg(self, kind, *args) msg = ParentedLet.parent_msg(self, kind, *args)
msg['period'] = self.ms msg["period"] = self.ms
return msg return msg
def do_business(self): def do_business(self):
while True: while True:
gevent.sleep(self.ms / 1000.0) gevent.sleep(self.ms / 1000.0)
yield ('timer', ) yield ("timer",)

View file

@ -2,22 +2,22 @@ import wave
def maxwait(songs, context, conf): def maxwait(songs, context, conf):
wait = int(conf.get('EF_MAXWAIT_SEC', 0)) wait = int(conf.get("EF_MAXWAIT_SEC", 0))
if wait == 0: if wait == 0:
return True return True
if 'time' not in context['status']: if "time" not in context["status"]:
return True, 'no song playing?' return True, "no song playing?"
curpos, duration = map(int, context['status']['time'].split(':')) curpos, duration = map(int, context["status"]["time"].split(":"))
remaining = duration - curpos remaining = duration - curpos
if remaining > wait: if remaining > wait:
return False, 'remaining %d max allowed %d' % (remaining, wait) return False, "remaining %d max allowed %d" % (remaining, wait)
return True return True
def get_duration(path): def get_duration(path):
'''get track duration in seconds''' """get track duration in seconds"""
if path.lower().endswith('.wav'): if path.lower().endswith(".wav"):
with wave.open(path, 'r') as f: with wave.open(path, "r") as f:
frames = f.getnframes() frames = f.getnframes()
rate = f.getframerate() rate = f.getframerate()
duration = frames / rate duration = frames / rate
@ -34,7 +34,7 @@ def get_duration(path):
def percentwait(songs, context, conf, getdur=get_duration): def percentwait(songs, context, conf, getdur=get_duration):
''' """
Similar to maxwait, but the maximum waiting time is proportional to the Similar to maxwait, but the maximum waiting time is proportional to the
duration of the audio we're going to add. duration of the audio we're going to add.
@ -46,25 +46,25 @@ def percentwait(songs, context, conf, getdur=get_duration):
are adding a jingle of 40seconds, then if EF_MAXWAIT_PERC==200 the audio are adding a jingle of 40seconds, then if EF_MAXWAIT_PERC==200 the audio
will be added (40s*200% = 1m20s) while if EF_MAXWAIT_PERC==100 it will be will be added (40s*200% = 1m20s) while if EF_MAXWAIT_PERC==100 it will be
filtered out. filtered out.
''' """
percentwait = int(conf.get('EF_MAXWAIT_PERC', 0)) percentwait = int(conf.get("EF_MAXWAIT_PERC", 0))
if percentwait == 0: if percentwait == 0:
return True return True
if 'time' not in context['status']: if "time" not in context["status"]:
return True, 'no song playing?' return True, "no song playing?"
curpos, duration = map(int, context['status']['time'].split(':')) curpos, duration = map(int, context["status"]["time"].split(":"))
remaining = duration - curpos remaining = duration - curpos
eventduration = 0 eventduration = 0
for uri in songs['uris']: for uri in songs["uris"]:
if not uri.startswith('file://'): if not uri.startswith("file://"):
return True, '%s is not a file' % uri return True, "%s is not a file" % uri
path = uri[len('file://'):] # strips file:// path = uri[len("file://") :] # strips file://
songduration = getdur(path) songduration = getdur(path)
if songduration is None: if songduration is None:
continue continue
eventduration += songduration eventduration += songduration
wait = eventduration * (percentwait/100.) wait = eventduration * (percentwait / 100.0)
if remaining > wait: if remaining > wait:
return False, 'remaining %d max allowed %d' % (remaining, wait) return False, "remaining %d max allowed %d" % (remaining, wait)
return True return True

View file

@ -9,17 +9,26 @@ def matchval(d):
if k in input_: # string matching if k in input_: # string matching
return d[k] return d[k]
raise Exception("This test case is bugged! No value for %s" % input_) raise Exception("This test case is bugged! No value for %s" % input_)
return mocked return mocked
durations = dict(one=60, two=120, three=180, four=240, ten=600, twenty=1200, durations = dict(
thirty=1800, nonexist=None) one=60,
two=120,
three=180,
four=240,
ten=600,
twenty=1200,
thirty=1800,
nonexist=None,
)
dur = matchval(durations) dur = matchval(durations)
def normalize_ret(ret): def normalize_ret(ret):
if type(ret) is bool: if type(ret) is bool:
return ret, '' return ret, ""
return ret return ret
@ -28,7 +37,7 @@ def mw(*args, **kwargs):
def pw(*args, **kwargs): def pw(*args, **kwargs):
kwargs['getdur'] = dur kwargs["getdur"] = dur
return normalize_ret(percentwait(*args, **kwargs)) return normalize_ret(percentwait(*args, **kwargs))
@ -38,26 +47,26 @@ def test_maxwait_nonpresent_disabled():
def test_maxwait_explicitly_disabled(): def test_maxwait_explicitly_disabled():
ret = mw([], {}, {'EF_MAXWAIT_SEC': 0}) ret = mw([], {}, {"EF_MAXWAIT_SEC": 0})
assert ret[0] is True assert ret[0] is True
def test_maxwait_ok(): def test_maxwait_ok():
ret = mw([], {'status': {'time': '250:300'}}, {'EF_MAXWAIT_SEC': 100}) ret = mw([], {"status": {"time": "250:300"}}, {"EF_MAXWAIT_SEC": 100})
assert ret[0] is True assert ret[0] is True
def test_maxwait_exceeded(): def test_maxwait_exceeded():
ret = mw([], {'status': {'time': '100:300'}}, {'EF_MAXWAIT_SEC': 100}) ret = mw([], {"status": {"time": "100:300"}}, {"EF_MAXWAIT_SEC": 100})
assert ret[0] is False assert ret[0] is False
def test_maxwait_limit(): def test_maxwait_limit():
ret = mw([], {'status': {'time': '199:300'}}, {'EF_MAXWAIT_SEC': 100}) ret = mw([], {"status": {"time": "199:300"}}, {"EF_MAXWAIT_SEC": 100})
assert ret[0] is False assert ret[0] is False
ret = mw([], {'status': {'time': '200:300'}}, {'EF_MAXWAIT_SEC': 100}) ret = mw([], {"status": {"time": "200:300"}}, {"EF_MAXWAIT_SEC": 100})
assert ret[0] is True assert ret[0] is True
ret = mw([], {'status': {'time': '201:300'}}, {'EF_MAXWAIT_SEC': 100}) ret = mw([], {"status": {"time": "201:300"}}, {"EF_MAXWAIT_SEC": 100})
assert ret[0] is True assert ret[0] is True
@ -67,32 +76,40 @@ def test_percentwait_nonpresent_disabled():
def test_percentwait_explicitly_disabled(): def test_percentwait_explicitly_disabled():
ret = pw([], {}, {'EF_MAXWAIT_PERC': 0}) ret = pw([], {}, {"EF_MAXWAIT_PERC": 0})
assert ret[0] is True assert ret[0] is True
def test_percentwait_ok(): def test_percentwait_ok():
# less than one minute missing # less than one minute missing
ret = pw(dict(uris=['file:///oneminute.ogg']), ret = pw(
{'status': {'time': '250:300'}}, dict(uris=["file:///oneminute.ogg"]),
{'EF_MAXWAIT_PERC': 100}) {"status": {"time": "250:300"}},
{"EF_MAXWAIT_PERC": 100},
)
assert ret[0] is True assert ret[0] is True
# more than one minute missing # more than one minute missing
ret = pw(dict(uris=['file:///oneminute.ogg']), ret = pw(
{'status': {'time': '220:300'}}, dict(uris=["file:///oneminute.ogg"]),
{'EF_MAXWAIT_PERC': 100}) {"status": {"time": "220:300"}},
{"EF_MAXWAIT_PERC": 100},
)
assert ret[0] is False assert ret[0] is False
def test_percentwait_morethan100(): def test_percentwait_morethan100():
# requiring 5*10 = 50mins = 3000sec # requiring 5*10 = 50mins = 3000sec
ret = pw(dict(uris=['file:///tenminute.ogg']), ret = pw(
{'status': {'time': '4800:6000'}}, dict(uris=["file:///tenminute.ogg"]),
{'EF_MAXWAIT_PERC': 500}) {"status": {"time": "4800:6000"}},
{"EF_MAXWAIT_PERC": 500},
)
assert ret[0] is True assert ret[0] is True
ret = pw(dict(uris=['file:///oneminute.ogg']), ret = pw(
{'status': {'time': '2000:6000'}}, dict(uris=["file:///oneminute.ogg"]),
{'EF_MAXWAIT_PERC': 500}) {"status": {"time": "2000:6000"}},
{"EF_MAXWAIT_PERC": 500},
)
assert ret[0] is False assert ret[0] is False

View file

@ -1,15 +1,16 @@
from logging import getLogger from logging import getLogger
log = getLogger('timeform')
log = getLogger("timeform")
from .entrypoints_utils import get_one_entrypoint from .entrypoints_utils import get_one_entrypoint
def get_timeform(kind): def get_timeform(kind):
'''Messes with entrypoints to return a TimeForm''' """Messes with entrypoints to return a TimeForm"""
for group in ('larigira.timeform_create', 'larigira.timeform_receive'): for group in ("larigira.timeform_create", "larigira.timeform_receive"):
yield get_one_entrypoint(group, kind) yield get_one_entrypoint(group, kind)
def get_audioform(kind): def get_audioform(kind):
'''Messes with entrypoints to return a AudioForm''' """Messes with entrypoints to return a AudioForm"""
for group in ('larigira.audioform_create', 'larigira.audioform_receive'): for group in ("larigira.audioform_create", "larigira.audioform_receive"):
yield get_one_entrypoint(group, kind) yield get_one_entrypoint(group, kind)

View file

@ -11,17 +11,16 @@ log = logging.getLogger(__name__)
class AutocompleteTextInput(wtforms.widgets.Input): class AutocompleteTextInput(wtforms.widgets.Input):
def __init__(self, datalist=None): def __init__(self, datalist=None):
super().__init__('text') super().__init__("text")
self.datalist = datalist self.datalist = datalist
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
# every second can be specified # every second can be specified
if self.datalist is not None: if self.datalist is not None:
return super(AutocompleteTextInput, self).__call__( return super(AutocompleteTextInput, self).__call__(
field, list=self.datalist, autocomplete="autocomplete", field, list=self.datalist, autocomplete="autocomplete", **kwargs
**kwargs) )
return super(AutocompleteTextInput, self).__call__( return super(AutocompleteTextInput, self).__call__(field, **kwargs)
field, **kwargs)
class AutocompleteStringField(StringField): class AutocompleteStringField(StringField):
@ -31,15 +30,15 @@ class AutocompleteStringField(StringField):
class DateTimeInput(wtforms.widgets.Input): class DateTimeInput(wtforms.widgets.Input):
input_type = 'datetime-local' input_type = "datetime-local"
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
# every second can be specified # every second can be specified
return super(DateTimeInput, self).__call__(field, step='1', **kwargs) return super(DateTimeInput, self).__call__(field, step="1", **kwargs)
class EasyDateTimeField(Field): class EasyDateTimeField(Field):
''' """
a "fork" of DateTimeField which uses HTML5 datetime-local a "fork" of DateTimeField which uses HTML5 datetime-local
The format is not customizable, because it is imposed by the HTML5 The format is not customizable, because it is imposed by the HTML5
@ -47,27 +46,28 @@ class EasyDateTimeField(Field):
This field does not ensure that browser actually supports datetime-local This field does not ensure that browser actually supports datetime-local
input type, nor does it provide polyfills. input type, nor does it provide polyfills.
''' """
widget = DateTimeInput() widget = DateTimeInput()
formats = ('%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M') formats = ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M")
def __init__(self, label=None, validators=None, **kwargs): def __init__(self, label=None, validators=None, **kwargs):
super(EasyDateTimeField, self).__init__(label, validators, **kwargs) super(EasyDateTimeField, self).__init__(label, validators, **kwargs)
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
return ' '.join(self.raw_data) return " ".join(self.raw_data)
return self.data and self.data.strftime(self.formats[0]) or '' return self.data and self.data.strftime(self.formats[0]) or ""
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
if valuelist: if valuelist:
date_str = ' '.join(valuelist) date_str = " ".join(valuelist)
for fmt in self.formats: for fmt in self.formats:
try: try:
self.data = datetime.strptime(date_str, fmt) self.data = datetime.strptime(date_str, fmt)
return return
except ValueError: except ValueError:
log.debug('Format `%s` not valid for `%s`', log.debug("Format `%s` not valid for `%s`", fmt, date_str)
fmt, date_str) raise ValueError(
raise ValueError(self.gettext( self.gettext("Not a valid datetime value <tt>{}</tt>").format(date_str)
'Not a valid datetime value <tt>{}</tt>').format(date_str)) )

View file

@ -5,7 +5,7 @@ import mimetypes
def scan_dir(dirname, extension=None): def scan_dir(dirname, extension=None):
if extension is None: if extension is None:
extension = '*' extension = "*"
for root, dirnames, filenames in os.walk(dirname): for root, dirnames, filenames in os.walk(dirname):
for fname in fnmatch.filter(filenames, extension): for fname in fnmatch.filter(filenames, extension):
yield os.path.join(root, fname) yield os.path.join(root, fname)
@ -13,7 +13,7 @@ def scan_dir(dirname, extension=None):
def multi_fnmatch(fname, extensions): def multi_fnmatch(fname, extensions):
for ext in extensions: for ext in extensions:
if fnmatch.fnmatch(fname, '*.' + ext): if fnmatch.fnmatch(fname, "*." + ext):
return True return True
return False return False
@ -22,10 +22,10 @@ def is_audio(fname):
mimetype = mimetypes.guess_type(fname)[0] mimetype = mimetypes.guess_type(fname)[0]
if mimetype is None: if mimetype is None:
return False return False
return mimetype.split('/')[0] == 'audio' return mimetype.split("/")[0] == "audio"
def scan_dir_audio(dirname, extensions=('mp3', 'oga', 'wav', 'ogg')): def scan_dir_audio(dirname, extensions=("mp3", "oga", "wav", "ogg")):
for root, dirnames, filenames in os.walk(dirname): for root, dirnames, filenames in os.walk(dirname):
for fname in filenames: for fname in filenames:
if is_audio(fname): if is_audio(fname):
@ -34,6 +34,6 @@ def scan_dir_audio(dirname, extensions=('mp3', 'oga', 'wav', 'ogg')):
def shortname(path): def shortname(path):
name = os.path.basename(path) # filename name = os.path.basename(path) # filename
name = name.rsplit('.', 1)[0] # no extension name = name.rsplit(".", 1)[0] # no extension
name = ''.join(c for c in name if c.isalnum()) # no strange chars name = "".join(c for c in name if c.isalnum()) # no strange chars
return name return name

View file

@ -16,15 +16,16 @@ from .entrypoints_utils import get_avail_entrypoints
def get_mpd_client(conf): def get_mpd_client(conf):
client = mpd.MPDClient(use_unicode=True) client = mpd.MPDClient(use_unicode=True)
client.connect(conf['MPD_HOST'], conf['MPD_PORT']) client.connect(conf["MPD_HOST"], conf["MPD_PORT"])
return client return client
class MPDWatcher(ParentedLet): class MPDWatcher(ParentedLet):
''' """
MPDWatcher notifies parent about any mpd event MPDWatcher notifies parent about any mpd event
''' """
def __init__(self, queue, conf, client=None): def __init__(self, queue, conf, client=None):
ParentedLet.__init__(self, queue) ParentedLet.__init__(self, queue)
self.log = logging.getLogger(self.__class__.__name__) self.log = logging.getLogger(self.__class__.__name__)
@ -41,46 +42,51 @@ class MPDWatcher(ParentedLet):
if self.client is None: if self.client is None:
self.refresh_client() self.refresh_client()
if first_after_connection: if first_after_connection:
yield('mpc', 'connect') yield ("mpc", "connect")
status = self.client.idle()[0] status = self.client.idle()[0]
except (mpd.ConnectionError, ConnectionRefusedError, except (
FileNotFoundError) as exc: mpd.ConnectionError,
self.log.warning('Connection to MPD failed (%s: %s)', ConnectionRefusedError,
exc.__class__.__name__, exc) FileNotFoundError,
) as exc:
self.log.warning(
"Connection to MPD failed (%s: %s)", exc.__class__.__name__, exc
)
self.client = None self.client = None
first_after_connection = True first_after_connection = True
gevent.sleep(5) gevent.sleep(5)
continue continue
else: else:
first_after_connection = False first_after_connection = False
yield ('mpc', status) yield ("mpc", status)
class Player: class Player:
''' """
The player contains different mpd-related methods The player contains different mpd-related methods
check_playlist determines whether the playlist is long enough and run audiogenerator accordingly check_playlist determines whether the playlist is long enough and run audiogenerator accordingly
enqueue receive audios that have been generated by Monitor and (if filters allow it) enqueue it to MPD playlist enqueue receive audios that have been generated by Monitor and (if filters allow it) enqueue it to MPD playlist
''' """
def __init__(self, conf): def __init__(self, conf):
self.conf = conf self.conf = conf
self.log = logging.getLogger(self.__class__.__name__) self.log = logging.getLogger(self.__class__.__name__)
self.min_playlist_length = 10 self.min_playlist_length = 10
self.tmpcleaner = UnusedCleaner(conf) self.tmpcleaner = UnusedCleaner(conf)
self._continous_audiospec = self.conf['CONTINOUS_AUDIOSPEC'] self._continous_audiospec = self.conf["CONTINOUS_AUDIOSPEC"]
self.events_enabled = True self.events_enabled = True
def _get_mpd(self): def _get_mpd(self):
mpd_client = mpd.MPDClient(use_unicode=True) mpd_client = mpd.MPDClient(use_unicode=True)
try: try:
mpd_client.connect(self.conf['MPD_HOST'], self.conf['MPD_PORT']) mpd_client.connect(self.conf["MPD_HOST"], self.conf["MPD_PORT"])
except (mpd.ConnectionError, ConnectionRefusedError, except (mpd.ConnectionError, ConnectionRefusedError, FileNotFoundError) as exc:
FileNotFoundError) as exc: self.log.warning(
self.log.warning('Connection to MPD failed (%s: %s)', "Connection to MPD failed (%s: %s)", exc.__class__.__name__, exc
exc.__class__.__name__, exc) )
raise gevent.GreenletExit() raise gevent.GreenletExit()
return mpd_client return mpd_client
@ -90,16 +96,17 @@ class Player:
@continous_audiospec.setter @continous_audiospec.setter
def continous_audiospec(self, spec): def continous_audiospec(self, spec):
self._continous_audiospec = self.conf['CONTINOUS_AUDIOSPEC'] \ self._continous_audiospec = (
if spec is None else spec self.conf["CONTINOUS_AUDIOSPEC"] if spec is None else spec
)
def clear_everything_but_current_song(): def clear_everything_but_current_song():
mpdc = self._get_mpd() mpdc = self._get_mpd()
current = mpdc.currentsong() current = mpdc.currentsong()
pos = int(current.get('pos', 0)) pos = int(current.get("pos", 0))
for song in mpdc.playlistid(): for song in mpdc.playlistid():
if int(song['pos']) != pos: if int(song["pos"]) != pos:
mpdc.deleteid(song['id']) mpdc.deleteid(song["id"])
gevent.Greenlet.spawn(clear_everything_but_current_song) gevent.Greenlet.spawn(clear_everything_but_current_song)
@ -107,12 +114,11 @@ class Player:
mpd_client = self._get_mpd() mpd_client = self._get_mpd()
songs = mpd_client.playlist() songs = mpd_client.playlist()
current = mpd_client.currentsong() current = mpd_client.currentsong()
pos = int(current.get('pos', 0)) + 1 pos = int(current.get("pos", 0)) + 1
if (len(songs) - pos) >= self.min_playlist_length: if (len(songs) - pos) >= self.min_playlist_length:
return return
self.log.info('need to add new songs') self.log.info("need to add new songs")
picker = gevent.Greenlet(audiogenerate, picker = gevent.Greenlet(audiogenerate, self.continous_audiospec)
self.continous_audiospec)
def add(greenlet): def add(greenlet):
uris = greenlet.value uris = greenlet.value
@ -120,69 +126,71 @@ class Player:
assert type(uri) is str, type(uri) assert type(uri) is str, type(uri)
self.tmpcleaner.watch(uri.strip()) self.tmpcleaner.watch(uri.strip())
mpd_client.add(uri.strip()) mpd_client.add(uri.strip())
picker.link_value(add) picker.link_value(add)
picker.start() picker.start()
def enqueue_filter(self, songs): def enqueue_filter(self, songs):
eventfilters = self.conf['EVENT_FILTERS'] eventfilters = self.conf["EVENT_FILTERS"]
if not eventfilters: if not eventfilters:
return True, '' return True, ""
availfilters = get_avail_entrypoints('larigira.eventfilter') availfilters = get_avail_entrypoints("larigira.eventfilter")
if len([ef for ef in eventfilters if ef in availfilters]) == 0: if len([ef for ef in eventfilters if ef in availfilters]) == 0:
return True, '' return True, ""
mpdc = self._get_mpd() mpdc = self._get_mpd()
status = mpdc.status() status = mpdc.status()
ctx = { ctx = {"playlist": mpdc.playlist(), "status": status, "durations": []}
'playlist': mpdc.playlist(), for entrypoint in iter_entry_points("larigira.eventfilter"):
'status': status,
'durations': []
}
for entrypoint in iter_entry_points('larigira.eventfilter'):
if entrypoint.name in eventfilters: if entrypoint.name in eventfilters:
ef = entrypoint.load() ef = entrypoint.load()
try: try:
ret = ef(songs=songs, context=ctx, conf=self.conf) ret = ef(songs=songs, context=ctx, conf=self.conf)
except ImportError as exc: except ImportError as exc:
self.log.warn("Filter %s skipped: %s" % (entrypoint.name, self.log.warn("Filter %s skipped: %s" % (entrypoint.name, exc))
exc))
continue continue
if ret is None: # bad behavior! if ret is None: # bad behavior!
continue continue
if type(ret) is bool: if type(ret) is bool:
reason = '' reason = ""
else: else:
ret, reason = ret ret, reason = ret
reason = 'Filtered by %s (%s)' % (entrypoint.name, reason) reason = "Filtered by %s (%s)" % (entrypoint.name, reason)
if ret is False: if ret is False:
return ret, reason return ret, reason
return True, 'Passed through %s' % ','.join(availfilters) return True, "Passed through %s" % ",".join(availfilters)
def enqueue(self, songs): def enqueue(self, songs):
assert type(songs) is dict assert type(songs) is dict
assert 'uris' in songs assert "uris" in songs
spec = [aspec.get('nick', aspec.eid) for aspec in songs['audiospecs']] spec = [aspec.get("nick", aspec.eid) for aspec in songs["audiospecs"]]
nicks = ','.join((aspec.get('nick', aspec.eid) nicks = ",".join(
for aspec in songs['audiospecs'])) (aspec.get("nick", aspec.eid) for aspec in songs["audiospecs"])
)
if not self.events_enabled: if not self.events_enabled:
self.log.debug('Ignoring <%s> (events disabled)', nicks self.log.debug("Ignoring <%s> (events disabled)", nicks)
)
return return
filterok, reason = self.enqueue_filter(songs) filterok, reason = self.enqueue_filter(songs)
if not filterok: if not filterok:
self.log.debug('Ignoring <%s>, filtered: %s', nicks, reason) self.log.debug("Ignoring <%s>, filtered: %s", nicks, reason)
# delete those files # delete those files
for uri in reversed(songs['uris']): for uri in reversed(songs["uris"]):
self.tmpcleaner.watch(uri.strip()) self.tmpcleaner.watch(uri.strip())
return return
mpd_client = self._get_mpd() mpd_client = self._get_mpd()
for uri in reversed(songs['uris']): for uri in reversed(songs["uris"]):
assert type(uri) is str assert type(uri) is str
self.log.info('Adding %s to playlist (from <%s>:%s=%s)', self.log.info(
uri, "Adding %s to playlist (from <%s>:%s=%s)",
songs['timespec'].get('nick', ''), uri,
songs['aids'], spec) songs["timespec"].get("nick", ""),
insert_pos = 0 if len(mpd_client.playlistid()) == 0 else \ songs["aids"],
int(mpd_client.currentsong().get('pos', 0)) + 1 spec,
)
insert_pos = (
0
if len(mpd_client.playlistid()) == 0
else int(mpd_client.currentsong().get("pos", 0)) + 1
)
try: try:
mpd_client.addid(uri, insert_pos) mpd_client.addid(uri, insert_pos)
except mpd.CommandError: except mpd.CommandError:
@ -197,7 +205,7 @@ class Controller(gevent.Greenlet):
self.conf = conf self.conf = conf
self.q = Queue() self.q = Queue()
self.player = Player(self.conf) self.player = Player(self.conf)
if 'DB_URI' in self.conf: if "DB_URI" in self.conf:
self.monitor = Monitor(self.q, self.conf) self.monitor = Monitor(self.q, self.conf)
self.monitor.parent_greenlet = self self.monitor.parent_greenlet = self
else: else:
@ -209,28 +217,28 @@ class Controller(gevent.Greenlet):
mw = MPDWatcher(self.q, self.conf, client=None) mw = MPDWatcher(self.q, self.conf, client=None)
mw.parent_greenlet = self mw.parent_greenlet = self
mw.start() mw.start()
t = Timer(int(self.conf['CHECK_SECS']) * 1000, self.q) t = Timer(int(self.conf["CHECK_SECS"]) * 1000, self.q)
t.parent_greenlet = self t.parent_greenlet = self
t.start() t.start()
# at the very start, run a check! # at the very start, run a check!
gevent.Greenlet.spawn(self.player.check_playlist) gevent.Greenlet.spawn(self.player.check_playlist)
while True: while True:
value = self.q.get() value = self.q.get()
self.log.debug('<- %s', str(value)) self.log.debug("<- %s", str(value))
# emitter = value['emitter'] # emitter = value['emitter']
kind = value['kind'] kind = value["kind"]
args = value['args'] args = value["args"]
if kind == 'timer' or (kind == 'mpc' and if kind == "timer" or (
args[0] in ('player', 'playlist', kind == "mpc" and args[0] in ("player", "playlist", "connect")
'connect')): ):
gevent.Greenlet.spawn(self.player.check_playlist) gevent.Greenlet.spawn(self.player.check_playlist)
try: try:
self.player.tmpcleaner.check_playlist() self.player.tmpcleaner.check_playlist()
except: except:
pass pass
elif kind == 'mpc': elif kind == "mpc":
pass pass
elif kind == 'uris_enqueue': elif kind == "uris_enqueue":
# TODO: uris_enqueue messages should be delivered directly to Player.enqueue # TODO: uris_enqueue messages should be delivered directly to Player.enqueue
# probably we need a MPDEnqueuer that receives every uri we want to add # probably we need a MPDEnqueuer that receives every uri we want to add
try: try:
@ -238,13 +246,13 @@ class Controller(gevent.Greenlet):
except AssertionError: except AssertionError:
raise raise
except Exception: except Exception:
self.log.exception("Error while adding to queue; " self.log.exception(
"bad audiogen output?") "Error while adding to queue; " "bad audiogen output?"
elif (kind == 'signal' and args[0] == signal.SIGALRM) or \ )
kind == 'refresh': elif (kind == "signal" and args[0] == signal.SIGALRM) or kind == "refresh":
# it's a tick! # it's a tick!
self.log.debug("Reload") self.log.debug("Reload")
self.monitor.q.put(dict(kind='forcetick')) self.monitor.q.put(dict(kind="forcetick"))
gevent.Greenlet.spawn(self.player.check_playlist) gevent.Greenlet.spawn(self.player.check_playlist)
else: else:
self.log.warning("Unknown message: %s", str(value)) self.log.warning("Unknown message: %s", str(value))

View file

@ -6,26 +6,27 @@ from .config import get_conf
@pytest.fixture @pytest.fixture
def unusedcleaner(): def unusedcleaner():
return UnusedCleaner(get_conf(prefix='LARIGIRATEST_')) return UnusedCleaner(get_conf(prefix="LARIGIRATEST_"))
# this test suite heavily assumes that TMPDIR == /tmp/, which is the default # this test suite heavily assumes that TMPDIR == /tmp/, which is the default
# indeed. However, the code does not rely on this assumption. # indeed. However, the code does not rely on this assumption.
def test_watch_file(unusedcleaner): def test_watch_file(unusedcleaner):
# despite not existing, the file is added # despite not existing, the file is added
unusedcleaner.watch('file:///tmp/gnam') unusedcleaner.watch("file:///tmp/gnam")
assert len(unusedcleaner.waiting_removal_files) == 1 assert len(unusedcleaner.waiting_removal_files) == 1
assert list(unusedcleaner.waiting_removal_files)[0] == '/tmp/gnam' assert list(unusedcleaner.waiting_removal_files)[0] == "/tmp/gnam"
def test_watch_path_error(unusedcleaner): def test_watch_path_error(unusedcleaner):
'''paths are not valid thing to watch. URIs only, thanks''' """paths are not valid thing to watch. URIs only, thanks"""
unusedcleaner.watch('/tmp/foo') unusedcleaner.watch("/tmp/foo")
assert len(unusedcleaner.waiting_removal_files) == 0 assert len(unusedcleaner.waiting_removal_files) == 0
def test_watch_notmp_error(unusedcleaner): def test_watch_notmp_error(unusedcleaner):
'''Files not in TMPDIR are not added''' """Files not in TMPDIR are not added"""
unusedcleaner.watch('file:///not/in/tmp') unusedcleaner.watch("file:///not/in/tmp")
assert len(unusedcleaner.waiting_removal_files) == 0 assert len(unusedcleaner.waiting_removal_files) == 0

View file

@ -14,19 +14,19 @@ def now(request):
@pytest.fixture @pytest.fixture
def yesterday(request): def yesterday(request):
return int(time.time()) - 24*60*60 return int(time.time()) - 24 * 60 * 60
@pytest.fixture @pytest.fixture
def empty_dir(): def empty_dir():
dirpath = tempfile.mkdtemp(prefix='mostrecent.') dirpath = tempfile.mkdtemp(prefix="mostrecent.")
yield dirpath yield dirpath
os.removedirs(dirpath) os.removedirs(dirpath)
@pytest.fixture @pytest.fixture
def dir_with_old_file(empty_dir): def dir_with_old_file(empty_dir):
fd, fname = tempfile.mkstemp(prefix='old.', dir=empty_dir) fd, fname = tempfile.mkstemp(prefix="old.", dir=empty_dir)
os.close(fd) os.close(fd)
os.utime(fname, times=(0, 0)) os.utime(fname, times=(0, 0))
yield empty_dir yield empty_dir
@ -35,7 +35,7 @@ def dir_with_old_file(empty_dir):
@pytest.fixture @pytest.fixture
def dir_with_yesterday_file(empty_dir, yesterday): def dir_with_yesterday_file(empty_dir, yesterday):
fd, fname = tempfile.mkstemp(prefix='yesterday.', dir=empty_dir) fd, fname = tempfile.mkstemp(prefix="yesterday.", dir=empty_dir)
os.close(fd) os.close(fd)
os.utime(fname, times=(yesterday, yesterday)) os.utime(fname, times=(yesterday, yesterday))
yield empty_dir yield empty_dir
@ -44,7 +44,7 @@ def dir_with_yesterday_file(empty_dir, yesterday):
@pytest.fixture @pytest.fixture
def dir_with_new_file(dir_with_old_file, now): def dir_with_new_file(dir_with_old_file, now):
fd, fname = tempfile.mkstemp(prefix='new.', dir=dir_with_old_file) fd, fname = tempfile.mkstemp(prefix="new.", dir=dir_with_old_file)
os.close(fd) os.close(fd)
os.utime(fname, times=(now, now)) os.utime(fname, times=(now, now))
yield dir_with_old_file yield dir_with_old_file
@ -53,7 +53,7 @@ def dir_with_new_file(dir_with_old_file, now):
@pytest.fixture @pytest.fixture
def dir_with_two_recent_files(dir_with_yesterday_file, now): def dir_with_two_recent_files(dir_with_yesterday_file, now):
fd, fname = tempfile.mkstemp(prefix='new.', dir=dir_with_yesterday_file) fd, fname = tempfile.mkstemp(prefix="new.", dir=dir_with_yesterday_file)
os.close(fd) os.close(fd)
os.utime(fname, times=(now, now)) os.utime(fname, times=(now, now))
yield dir_with_yesterday_file yield dir_with_yesterday_file
@ -61,7 +61,7 @@ def dir_with_two_recent_files(dir_with_yesterday_file, now):
def test_empty_is_empty(empty_dir, now): def test_empty_is_empty(empty_dir, now):
'''nothing can be picked from a empty dir''' """nothing can be picked from a empty dir"""
picked = recent_choose([empty_dir], 1, now) picked = recent_choose([empty_dir], 1, now)
assert len(picked) == 0 assert len(picked) == 0
@ -74,17 +74,17 @@ def test_old_files(dir_with_old_file, now):
def test_new_files_found(dir_with_new_file): def test_new_files_found(dir_with_new_file):
picked = recent_choose([dir_with_new_file], 1, 1) picked = recent_choose([dir_with_new_file], 1, 1)
assert len(picked) == 1 assert len(picked) == 1
assert os.path.basename(picked[0]).startswith('new.') assert os.path.basename(picked[0]).startswith("new.")
def test_only_new_files_found(dir_with_new_file): def test_only_new_files_found(dir_with_new_file):
picked = recent_choose([dir_with_new_file], 2, 1) picked = recent_choose([dir_with_new_file], 2, 1)
assert len(picked) == 1 assert len(picked) == 1
assert os.path.basename(picked[0]).startswith('new.') assert os.path.basename(picked[0]).startswith("new.")
def test_correct_sorting(dir_with_two_recent_files): def test_correct_sorting(dir_with_two_recent_files):
picked = recent_choose([dir_with_two_recent_files], 1, 1) picked = recent_choose([dir_with_two_recent_files], 1, 1)
assert len(picked) == 1 assert len(picked) == 1
assert not os.path.basename(picked[0]).startswith('yesterday.') assert not os.path.basename(picked[0]).startswith("yesterday.")
assert os.path.basename(picked[0]).startswith('new.') assert os.path.basename(picked[0]).startswith("new.")

View file

@ -1,5 +1,6 @@
from __future__ import print_function from __future__ import print_function
from gevent import monkey from gevent import monkey
monkey.patch_all(subprocess=True) monkey.patch_all(subprocess=True)
import pytest import pytest
@ -9,10 +10,9 @@ from larigira.audiogen_mpdrandom import generate_by_artist
@pytest.fixture @pytest.fixture
def simplerandom(): def simplerandom():
return { return {}
}
def test_accepted_syntax(simplerandom): def test_accepted_syntax(simplerandom):
'''Check the minimal needed configuration for mpdrandom''' """Check the minimal needed configuration for mpdrandom"""
generate_by_artist(simplerandom) generate_by_artist(simplerandom)

View file

@ -9,51 +9,51 @@ def P(pypathlocal):
def test_txt_files_are_excluded(tmpdir): def test_txt_files_are_excluded(tmpdir):
p = tmpdir.join("foo.txt") p = tmpdir.join("foo.txt")
p.write('') p.write("")
assert len(candidates([P(p)])) == 0 assert len(candidates([P(p)])) == 0
assert len(candidates([P(tmpdir)])) == 0 assert len(candidates([P(tmpdir)])) == 0
def test_nested_txt_files_are_excluded(tmpdir): def test_nested_txt_files_are_excluded(tmpdir):
p = tmpdir.mkdir('one').mkdir('two').join("foo.txt") p = tmpdir.mkdir("one").mkdir("two").join("foo.txt")
p.write('') p.write("")
assert len(candidates([P(p)])) == 0 assert len(candidates([P(p)])) == 0
assert len(candidates([P(tmpdir)])) == 0 assert len(candidates([P(tmpdir)])) == 0
def test_mp3_files_are_considered(tmpdir): def test_mp3_files_are_considered(tmpdir):
p = tmpdir.join("foo.mp3") p = tmpdir.join("foo.mp3")
p.write('') p.write("")
assert len(candidates([P(p)])) == 1 assert len(candidates([P(p)])) == 1
assert len(candidates([P(tmpdir)])) == 1 assert len(candidates([P(tmpdir)])) == 1
def test_nested_mp3_files_are_considered(tmpdir): def test_nested_mp3_files_are_considered(tmpdir):
p = tmpdir.mkdir('one').mkdir('two').join("foo.mp3") p = tmpdir.mkdir("one").mkdir("two").join("foo.mp3")
p.write('') p.write("")
assert len(candidates([P(p)])) == 1 assert len(candidates([P(p)])) == 1
assert len(candidates([P(tmpdir)])) == 1 assert len(candidates([P(tmpdir)])) == 1
def test_same_name(tmpdir): def test_same_name(tmpdir):
'''file with same name on different dir should not be confused''' """file with same name on different dir should not be confused"""
p = tmpdir.mkdir('one').mkdir('two').join("foo.mp3") p = tmpdir.mkdir("one").mkdir("two").join("foo.mp3")
p.write('') p.write("")
p = tmpdir.join("foo.mp3") p = tmpdir.join("foo.mp3")
p.write('') p.write("")
assert len(candidates([P(tmpdir)])) == 2 assert len(candidates([P(tmpdir)])) == 2
def test_unknown_mime_ignore(tmpdir): def test_unknown_mime_ignore(tmpdir):
p = tmpdir.join("foo.???") p = tmpdir.join("foo.???")
p.write('') p.write("")
assert len(candidates([P(tmpdir)])) == 0 assert len(candidates([P(tmpdir)])) == 0
def test_unknown_mime_nocrash(tmpdir): def test_unknown_mime_nocrash(tmpdir):
p = tmpdir.join("foo.???") p = tmpdir.join("foo.???")
p.write('') p.write("")
p = tmpdir.join("foo.ogg") p = tmpdir.join("foo.ogg")
p.write('') p.write("")
assert len(candidates([P(tmpdir)])) == 1 assert len(candidates([P(tmpdir)])) == 1

View file

@ -4,14 +4,14 @@ from larigira.unused import old_commonpath
def test_same(): def test_same():
assert old_commonpath(['/foo/bar', '/foo/bar/']) == '/foo/bar' assert old_commonpath(["/foo/bar", "/foo/bar/"]) == "/foo/bar"
def test_prefix(): def test_prefix():
assert old_commonpath(['/foo/bar', '/foo/zap/']) == '/foo' assert old_commonpath(["/foo/bar", "/foo/zap/"]) == "/foo"
assert old_commonpath(['/foo/bar/', '/foo/zap/']) == '/foo' assert old_commonpath(["/foo/bar/", "/foo/zap/"]) == "/foo"
assert old_commonpath(['/foo/bar/', '/foo/zap']) == '/foo' assert old_commonpath(["/foo/bar/", "/foo/zap"]) == "/foo"
assert old_commonpath(['/foo/bar', '/foo/zap']) == '/foo' assert old_commonpath(["/foo/bar", "/foo/zap"]) == "/foo"
try: try:
@ -22,17 +22,18 @@ else:
# these tests are only available on python >= 3.5. That's fine though, as # these tests are only available on python >= 3.5. That's fine though, as
# our CI will perform validation of those cases to see if they match python # our CI will perform validation of those cases to see if they match python
# behavior # behavior
@pytest.fixture(params=['a', 'a/', 'a/b', 'a/b/', 'a/b/c', ]) @pytest.fixture(params=["a", "a/", "a/b", "a/b/", "a/b/c"])
def relpath(request): def relpath(request):
return request.param return request.param
@pytest.fixture @pytest.fixture
def abspath(relpath): def abspath(relpath):
return '/' + relpath return "/" + relpath
@pytest.fixture(params=['', '/']) @pytest.fixture(params=["", "/"])
def slashed_abspath(abspath, request): def slashed_abspath(abspath, request):
return '%s%s' % (abspath, request.param) return "%s%s" % (abspath, request.param)
slashed_abspath_b = slashed_abspath slashed_abspath_b = slashed_abspath
@pytest.fixture @pytest.fixture
@ -42,11 +43,12 @@ else:
def test_abspath_match(abspath_couple): def test_abspath_match(abspath_couple):
assert commonpath(abspath_couple) == old_commonpath(abspath_couple) assert commonpath(abspath_couple) == old_commonpath(abspath_couple)
@pytest.fixture(params=['', '/']) @pytest.fixture(params=["", "/"])
def slashed_relpath(relpath, request): def slashed_relpath(relpath, request):
s = '%s%s' % (relpath, request.param) s = "%s%s" % (relpath, request.param)
if s: if s:
return s return s
slashed_relpath_b = slashed_relpath slashed_relpath_b = slashed_relpath
@pytest.fixture @pytest.fixture

View file

@ -2,6 +2,7 @@ import tempfile
import os import os
from gevent import monkey from gevent import monkey
monkey.patch_all(subprocess=True) monkey.patch_all(subprocess=True)
import pytest import pytest
@ -11,7 +12,7 @@ from larigira.db import EventModel
@pytest.yield_fixture @pytest.yield_fixture
def db(): def db():
fname = tempfile.mktemp(suffix='.json', prefix='larigira-test') fname = tempfile.mktemp(suffix=".json", prefix="larigira-test")
yield EventModel(uri=fname) yield EventModel(uri=fname)
os.unlink(fname) os.unlink(fname)
@ -22,36 +23,39 @@ def test_empty(db):
def test_add_basic(db): def test_add_basic(db):
assert len(db.get_all_alarms()) == 0 assert len(db.get_all_alarms()) == 0
alarm_id = db.add_event(dict(kind='frequency', interval=60*3, start=1), alarm_id = db.add_event(
[dict(kind='mpd', paths=['foo.mp3'], howmany=1)]) dict(kind="frequency", interval=60 * 3, start=1),
[dict(kind="mpd", paths=["foo.mp3"], howmany=1)],
)
assert len(db.get_all_alarms()) == 1 assert len(db.get_all_alarms()) == 1
assert db.get_alarm_by_id(alarm_id) is not None assert db.get_alarm_by_id(alarm_id) is not None
assert len(tuple(db.get_actions_by_alarm( assert len(tuple(db.get_actions_by_alarm(db.get_alarm_by_id(alarm_id)))) == 1
db.get_alarm_by_id(alarm_id)))) == 1
def test_add_multiple_alarms(db): def test_add_multiple_alarms(db):
assert len(db.get_all_alarms()) == 0 assert len(db.get_all_alarms()) == 0
alarm_id = db.add_event(dict(kind='frequency', interval=60*3, start=1), alarm_id = db.add_event(
[dict(kind='mpd', paths=['foo.mp3'], howmany=1), dict(kind="frequency", interval=60 * 3, start=1),
dict(kind='foo', a=3)]) [dict(kind="mpd", paths=["foo.mp3"], howmany=1), dict(kind="foo", a=3)],
)
assert len(db.get_all_alarms()) == 1 assert len(db.get_all_alarms()) == 1
assert db.get_alarm_by_id(alarm_id) is not None assert db.get_alarm_by_id(alarm_id) is not None
assert len(db.get_all_actions()) == 2 assert len(db.get_all_actions()) == 2
assert len(tuple(db.get_actions_by_alarm( assert len(tuple(db.get_actions_by_alarm(db.get_alarm_by_id(alarm_id)))) == 2
db.get_alarm_by_id(alarm_id)))) == 2
def test_delete_alarm(db): def test_delete_alarm(db):
assert len(db.get_all_alarms()) == 0 assert len(db.get_all_alarms()) == 0
alarm_id = db.add_event(dict(kind='frequency', interval=60*3, start=1), alarm_id = db.add_event(
[dict(kind='mpd', paths=['foo.mp3'], howmany=1)]) dict(kind="frequency", interval=60 * 3, start=1),
[dict(kind="mpd", paths=["foo.mp3"], howmany=1)],
)
action_id = next(db.get_actions_by_alarm(db.get_alarm_by_id(alarm_id))).eid action_id = next(db.get_actions_by_alarm(db.get_alarm_by_id(alarm_id))).eid
assert len(db.get_all_alarms()) == 1 assert len(db.get_all_alarms()) == 1
db.delete_alarm(alarm_id) db.delete_alarm(alarm_id)
assert len(db.get_all_alarms()) == 0 # alarm deleted assert len(db.get_all_alarms()) == 0 # alarm deleted
assert db.get_action_by_id(action_id) is not None assert db.get_action_by_id(action_id) is not None
assert 'kind' in db.get_action_by_id(action_id) # action still there assert "kind" in db.get_action_by_id(action_id) # action still there
def test_delete_alarm_nonexisting(db): def test_delete_alarm_nonexisting(db):
@ -60,8 +64,10 @@ def test_delete_alarm_nonexisting(db):
def test_delete_action(db): def test_delete_action(db):
alarm_id = db.add_event(dict(kind='frequency', interval=60*3, start=1), alarm_id = db.add_event(
[dict(kind='mpd', paths=['foo.mp3'], howmany=1)]) dict(kind="frequency", interval=60 * 3, start=1),
[dict(kind="mpd", paths=["foo.mp3"], howmany=1)],
)
alarm = db.get_alarm_by_id(alarm_id) alarm = db.get_alarm_by_id(alarm_id)
assert len(tuple(db.get_actions_by_alarm(alarm))) == 1 assert len(tuple(db.get_actions_by_alarm(alarm))) == 1
action = next(db.get_actions_by_alarm(alarm)) action = next(db.get_actions_by_alarm(alarm))

View file

@ -2,15 +2,15 @@ from larigira.fsutils import shortname
def test_shortname_self(): def test_shortname_self():
'''sometimes, shortname is just filename without extension''' """sometimes, shortname is just filename without extension"""
assert shortname('/tmp/asd/foo.bar') == 'foo' assert shortname("/tmp/asd/foo.bar") == "foo"
def test_shortname_has_numbers(): def test_shortname_has_numbers():
'''shortname will preserve numbers''' """shortname will preserve numbers"""
assert shortname('/tmp/asd/foo1.bar') == 'foo1' assert shortname("/tmp/asd/foo1.bar") == "foo1"
def test_shortname_has_no_hyphen(): def test_shortname_has_no_hyphen():
'''shortname will not preserve hyphens''' """shortname will not preserve hyphens"""
assert shortname('/tmp/asd/foo-1.bar') == 'foo1' assert shortname("/tmp/asd/foo-1.bar") == "foo1"

View file

@ -1,5 +1,6 @@
from __future__ import print_function from __future__ import print_function
from gevent import monkey from gevent import monkey
monkey.patch_all(subprocess=True) monkey.patch_all(subprocess=True)
import pytest import pytest
@ -21,7 +22,8 @@ def range_parentlet():
def do_business(self): def do_business(self):
for i in range(self.howmany): for i in range(self.howmany):
yield('range', i) yield ("range", i)
return RangeLet return RangeLet
@ -33,7 +35,8 @@ def single_value_parentlet():
self.val = val self.val = val
def do_business(self): def do_business(self):
yield('single', self.val) yield ("single", self.val)
return SingleLet return SingleLet
@ -44,7 +47,7 @@ def test_parented_range(queue, range_parentlet):
assert queue.qsize() == 5 assert queue.qsize() == 5
while not queue.empty(): while not queue.empty():
msg = queue.get() msg = queue.get()
assert msg['kind'] == 'range' assert msg["kind"] == "range"
def test_parented_single(queue, single_value_parentlet): def test_parented_single(queue, single_value_parentlet):
@ -52,25 +55,25 @@ def test_parented_single(queue, single_value_parentlet):
t.start() t.start()
gevent.sleep(0.01) gevent.sleep(0.01)
msg = queue.get_nowait() msg = queue.get_nowait()
assert msg['args'][0] == 123 assert msg["args"][0] == 123
def test_timer_finally(queue): def test_timer_finally(queue):
'''at somepoint, it will get results''' """at somepoint, it will get results"""
period = 10 period = 10
t = Timer(period, queue) t = Timer(period, queue)
t.start() t.start()
gevent.sleep(period*3/1000.0) gevent.sleep(period * 3 / 1000.0)
queue.get_nowait() queue.get_nowait()
def test_timer_righttime(queue): def test_timer_righttime(queue):
'''not too early, not too late''' """not too early, not too late"""
period = 500 period = 500
t = Timer(period, queue) t = Timer(period, queue)
t.start() t.start()
gevent.sleep(period/(10*1000.0)) gevent.sleep(period / (10 * 1000.0))
assert queue.empty() is True assert queue.empty() is True
gevent.sleep(period*(2/1000.0)) gevent.sleep(period * (2 / 1000.0))
assert not queue.empty() assert not queue.empty()
queue.get_nowait() queue.get_nowait()

View file

@ -8,7 +8,7 @@ from larigira.timegen import timegenerate
def eq_(a, b, reason=None): def eq_(a, b, reason=None):
'''migrating tests from nose''' """migrating tests from nose"""
if reason is not None: if reason is not None:
assert a == b, reason assert a == b, reason
else: else:
@ -20,63 +20,55 @@ def now():
return datetime.now() return datetime.now()
@pytest.fixture(params=['seconds', 'human', 'humanlong', 'coloned']) @pytest.fixture(params=["seconds", "human", "humanlong", "coloned"])
def onehour(now, request): def onehour(now, request):
'''a FrequencyAlarm: every hour for one day''' """a FrequencyAlarm: every hour for one day"""
intervals = dict(seconds=3600, human='1h', humanlong='30m 1800s', intervals = dict(
coloned='01:00:00') seconds=3600, human="1h", humanlong="30m 1800s", coloned="01:00:00"
return FrequencyAlarm({ )
'start': now - timedelta(days=1), return FrequencyAlarm(
'interval': intervals[request.param], {
'end': now + days(1)}) "start": now - timedelta(days=1),
"interval": intervals[request.param],
"end": now + days(1),
}
)
@pytest.fixture(params=[1, '1']) @pytest.fixture(params=[1, "1"])
def onehour_monday(request): def onehour_monday(request):
weekday = request.param weekday = request.param
yield FrequencyAlarm({ yield FrequencyAlarm({"interval": 3600 * 12, "weekdays": [weekday], "start": 0})
'interval': 3600*12,
'weekdays': [weekday],
'start': 0
})
@pytest.fixture(params=[7, '7']) @pytest.fixture(params=[7, "7"])
def onehour_sunday(request): def onehour_sunday(request):
weekday = request.param weekday = request.param
yield FrequencyAlarm({ yield FrequencyAlarm({"interval": 3600 * 12, "weekdays": [weekday], "start": 0})
'interval': 3600*12,
'weekdays': [weekday],
'start': 0
})
@pytest.fixture(params=[1, 2, 3, 4, 5, 6, 7]) @pytest.fixture(params=[1, 2, 3, 4, 5, 6, 7])
def singledow(request): def singledow(request):
weekday = request.param weekday = request.param
yield FrequencyAlarm({ yield FrequencyAlarm({"interval": 3600 * 24, "weekdays": [weekday], "start": 0})
'interval': 3600*24,
'weekdays': [weekday],
'start': 0
})
@pytest.fixture(params=['seconds', 'human', 'coloned']) @pytest.fixture(params=["seconds", "human", "coloned"])
def tenseconds(now, request): def tenseconds(now, request):
'''a FrequencyAlarm: every 10 seconds for one day''' """a FrequencyAlarm: every 10 seconds for one day"""
intervals = dict(seconds=10, human='10s', coloned='00:10') intervals = dict(seconds=10, human="10s", coloned="00:10")
return FrequencyAlarm({ return FrequencyAlarm(
'start': now - timedelta(days=1), {
'interval': intervals[request.param], "start": now - timedelta(days=1),
'end': now + days(1)}) "interval": intervals[request.param],
"end": now + days(1),
}
)
@pytest.fixture(params=[1, 2, 3, 4, 5, 6, 7, 8]) @pytest.fixture(params=[1, 2, 3, 4, 5, 6, 7, 8])
def manyweeks(request): def manyweeks(request):
yield FrequencyAlarm({ yield FrequencyAlarm({"interval": "{}w".format(request.param), "start": 0})
'interval': '{}w'.format(request.param),
'start': 0
})
def days(n): def days(n):
@ -84,24 +76,21 @@ def days(n):
def test_single_creations(now): def test_single_creations(now):
return SingleAlarm({ return SingleAlarm({"timestamp": now})
'timestamp': now
})
def test_freq_creations(now): def test_freq_creations(now):
return FrequencyAlarm({ return FrequencyAlarm(
'start': now - timedelta(days=1), {"start": now - timedelta(days=1), "interval": 3600, "end": now}
'interval': 3600, )
'end': now})
@pytest.mark.timeout(1) @pytest.mark.timeout(1)
def test_single_ring(now): def test_single_ring(now):
dt = now + days(1) dt = now + days(1)
s = SingleAlarm({'timestamp': dt}) s = SingleAlarm({"timestamp": dt})
eq_(s.next_ring(), dt) eq_(s.next_ring(), dt)
eq_(s.next_ring(now), dt) eq_(s.next_ring(now), dt)
assert s.next_ring(dt) is None, "%s - %s" % (str(s.next_ring(dt)), str(dt)) assert s.next_ring(dt) is None, "%s - %s" % (str(s.next_ring(dt)), str(dt))
assert s.next_ring(now + days(2)) is None assert s.next_ring(now + days(2)) is None
assert s.has_ring(dt) assert s.has_ring(dt)
@ -112,10 +101,10 @@ def test_single_ring(now):
@pytest.mark.timeout(1) @pytest.mark.timeout(1)
def test_single_all(now): def test_single_all(now):
dt = now + timedelta(days=1) dt = now + timedelta(days=1)
s = SingleAlarm({'timestamp': dt}) s = SingleAlarm({"timestamp": dt})
eq_(list(s.all_rings()), [dt]) eq_(list(s.all_rings()), [dt])
eq_(list(s.all_rings(now)), [dt]) eq_(list(s.all_rings(now)), [dt])
eq_(list(s.all_rings(now + days(2))), []) eq_(list(s.all_rings(now + days(2))), [])
def test_freq_short(now, tenseconds): def test_freq_short(now, tenseconds):
@ -165,12 +154,8 @@ def test_weekday_skip_2(onehour_sunday):
def test_sunday_is_not_0(): def test_sunday_is_not_0():
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
FrequencyAlarm({ FrequencyAlarm({"interval": 3600 * 12, "weekdays": [0], "start": 0})
'interval': 3600*12, assert "Not a valid weekday:" in excinfo.value.args[0]
'weekdays': [0],
'start': 0
})
assert 'Not a valid weekday:' in excinfo.value.args[0]
def test_long_interval(manyweeks): def test_long_interval(manyweeks):
@ -178,7 +163,7 @@ def test_long_interval(manyweeks):
expected = manyweeks.interval expected = manyweeks.interval
got = manyweeks.next_ring(t) got = manyweeks.next_ring(t)
assert got is not None assert got is not None
assert int(got.strftime('%s')) == expected assert int(got.strftime("%s")) == expected
assert manyweeks.next_ring(got) is not None assert manyweeks.next_ring(got) is not None
@ -194,15 +179,8 @@ def test_singledow(singledow):
def test_single_registered(): def test_single_registered():
timegenerate({ timegenerate({"kind": "single", "timestamp": 1234567890})
'kind': 'single',
'timestamp': 1234567890
})
def test_frequency_registered(): def test_frequency_registered():
timegenerate({ timegenerate({"kind": "frequency", "start": 1234567890, "interval": 60 * 15})
'kind': 'frequency',
'start': 1234567890,
'interval': 60*15
})

View file

@ -1,5 +1,6 @@
from __future__ import print_function from __future__ import print_function
from gevent import monkey from gevent import monkey
monkey.patch_all(subprocess=True) monkey.patch_all(subprocess=True)
import pytest import pytest
@ -15,5 +16,5 @@ def app(queue):
def test_refresh(app): def test_refresh(app):
assert app.queue.empty() assert app.queue.empty()
app.test_client().get('/api/refresh') app.test_client().get("/api/refresh")
assert not app.queue.empty() assert not app.queue.empty()

View file

@ -4,101 +4,125 @@ from datetime import datetime
from pytimeparse.timeparse import timeparse from pytimeparse.timeparse import timeparse
from flask_wtf import Form from flask_wtf import Form
from wtforms import StringField, validators, SubmitField, \ from wtforms import (
SelectMultipleField, ValidationError StringField,
validators,
SubmitField,
SelectMultipleField,
ValidationError,
)
from larigira.formutils import EasyDateTimeField from larigira.formutils import EasyDateTimeField
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SingleAlarmForm(Form): class SingleAlarmForm(Form):
nick = StringField('Alarm nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this alarm') "Alarm nick",
dt = EasyDateTimeField('Date and time', validators=[validators.required()], validators=[validators.required()],
description='Date to ring on, expressed as ' description="A simple name to recognize this alarm",
'2000-12-31T13:42:00') )
submit = SubmitField('Submit') dt = EasyDateTimeField(
"Date and time",
validators=[validators.required()],
description="Date to ring on, expressed as " "2000-12-31T13:42:00",
)
submit = SubmitField("Submit")
def populate_from_timespec(self, timespec): def populate_from_timespec(self, timespec):
if 'nick' in timespec: if "nick" in timespec:
self.nick.data = timespec['nick'] self.nick.data = timespec["nick"]
if 'timestamp' in timespec: if "timestamp" in timespec:
self.dt.data = datetime.fromtimestamp(timespec['timestamp']) self.dt.data = datetime.fromtimestamp(timespec["timestamp"])
def singlealarm_receive(form): def singlealarm_receive(form):
return { return {
'kind': 'single', "kind": "single",
'nick': form.nick.data, "nick": form.nick.data,
'timestamp': int(form.dt.data.strftime('%s')) "timestamp": int(form.dt.data.strftime("%s")),
} }
class FrequencyAlarmForm(Form): class FrequencyAlarmForm(Form):
nick = StringField('Alarm nick', validators=[validators.required()], nick = StringField(
description='A simple name to recognize this alarm') "Alarm nick",
interval = StringField('Frequency', validators=[validators.required()],
validators=[validators.required()], description="A simple name to recognize this alarm",
description='in seconds, or human-readable ' )
'(like 9w3d12h)') interval = StringField(
start = EasyDateTimeField('Start date and time', "Frequency",
validators=[validators.optional()], validators=[validators.required()],
description='Before this, no alarm will ring. ' description="in seconds, or human-readable " "(like 9w3d12h)",
'Expressed as YYYY-MM-DDTHH:MM:SS. If omitted, ' )
'the alarm will always ring') start = EasyDateTimeField(
end = EasyDateTimeField('End date and time', "Start date and time",
validators=[validators.optional()], validators=[validators.optional()],
description='After this, no alarm will ring. ' description="Before this, no alarm will ring. "
'Expressed as YYYY-MM-DDTHH:MM:SS. If omitted, ' "Expressed as YYYY-MM-DDTHH:MM:SS. If omitted, "
'the alarm will always ring') "the alarm will always ring",
weekdays = SelectMultipleField('Days on which the alarm should be played', )
choices=[('1', 'Monday'), end = EasyDateTimeField(
('2', 'Tuesday'), "End date and time",
('3', 'Wednesday'), validators=[validators.optional()],
('4', 'Thursday'), description="After this, no alarm will ring. "
('5', 'Friday'), "Expressed as YYYY-MM-DDTHH:MM:SS. If omitted, "
('6', 'Saturday'), "the alarm will always ring",
('7', 'Sunday')], )
default=list('1234567'), weekdays = SelectMultipleField(
validators=[validators.required()], "Days on which the alarm should be played",
description='The alarm will ring only on ' choices=[
'selected weekdays') ("1", "Monday"),
submit = SubmitField('Submit') ("2", "Tuesday"),
("3", "Wednesday"),
("4", "Thursday"),
("5", "Friday"),
("6", "Saturday"),
("7", "Sunday"),
],
default=list("1234567"),
validators=[validators.required()],
description="The alarm will ring only on " "selected weekdays",
)
submit = SubmitField("Submit")
def populate_from_timespec(self, timespec): def populate_from_timespec(self, timespec):
if 'nick' in timespec: if "nick" in timespec:
self.nick.data = timespec['nick'] self.nick.data = timespec["nick"]
if 'start' in timespec: if "start" in timespec:
self.start.data = datetime.fromtimestamp(timespec['start']) self.start.data = datetime.fromtimestamp(timespec["start"])
if 'end' in timespec: if "end" in timespec:
self.end.data = datetime.fromtimestamp(timespec['end']) self.end.data = datetime.fromtimestamp(timespec["end"])
if 'weekdays' in timespec: if "weekdays" in timespec:
self.weekdays.data = timespec['weekdays'] self.weekdays.data = timespec["weekdays"]
else: else:
self.weekdays.data = list('1234567') self.weekdays.data = list("1234567")
self.interval.data = timespec['interval'] self.interval.data = timespec["interval"]
def validate_interval(self, field): def validate_interval(self, field):
try: try:
int(field.data) int(field.data)
except ValueError: except ValueError:
if timeparse(field.data) is None: if timeparse(field.data) is None:
raise ValidationError("interval must either be a number " raise ValidationError(
"(in seconds) or a human-readable " "interval must either be a number "
"string like '1h2m' or '1d12h'") "(in seconds) or a human-readable "
"string like '1h2m' or '1d12h'"
)
def frequencyalarm_receive(form): def frequencyalarm_receive(form):
obj = { obj = {
'kind': 'frequency', "kind": "frequency",
'nick': form.nick.data, "nick": form.nick.data,
'interval': form.interval.data, "interval": form.interval.data,
'weekdays': form.weekdays.data, "weekdays": form.weekdays.data,
} }
if form.start.data: if form.start.data:
obj['start'] = int(form.start.data.strftime('%s')) obj["start"] = int(form.start.data.strftime("%s"))
else: else:
obj['start'] = 0 obj["start"] = 0
if form.end.data: if form.end.data:
obj['end'] = int(form.end.data.strftime('%s')) obj["end"] = int(form.end.data.strftime("%s"))
return obj return obj

View file

@ -1,6 +1,6 @@
''' """
main module to read and get informations about alarms main module to read and get informations about alarms
''' """
from __future__ import print_function from __future__ import print_function
import sys import sys
from datetime import datetime from datetime import datetime
@ -8,31 +8,48 @@ import argparse
import json import json
from .entrypoints_utils import get_one_entrypoint from .entrypoints_utils import get_one_entrypoint
from logging import getLogger from logging import getLogger
log = getLogger('timegen')
log = getLogger("timegen")
def get_timegenerator(kind): def get_timegenerator(kind):
'''Messes with entrypoints to return an timegenerator function''' """Messes with entrypoints to return an timegenerator function"""
return get_one_entrypoint('larigira.timegenerators', kind) return get_one_entrypoint("larigira.timegenerators", kind)
def get_parser(): def get_parser():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate "ring times" from a timespec') description='Generate "ring times" from a timespec'
parser.add_argument('timespec', metavar='TIMESPEC', type=str, nargs=1, )
help='filename for timespec, formatted in json') parser.add_argument(
parser.add_argument('--now', metavar='NOW', type=int, nargs=1, "timespec",
default=None, metavar="TIMESPEC",
help='Set a different "time", in unix epoch') type=str,
parser.add_argument('--howmany', metavar='N', type=int, nargs=1, nargs=1,
default=[1], help="filename for timespec, formatted in json",
help='Set a different "time", in unix epoch') )
parser.add_argument(
"--now",
metavar="NOW",
type=int,
nargs=1,
default=None,
help='Set a different "time", in unix epoch',
)
parser.add_argument(
"--howmany",
metavar="N",
type=int,
nargs=1,
default=[1],
help='Set a different "time", in unix epoch',
)
return parser return parser
def read_spec(fname): def read_spec(fname):
try: try:
if fname == '-': if fname == "-":
return json.load(sys.stdin) return json.load(sys.stdin)
with open(fname) as buf: with open(fname) as buf:
return json.load(buf) return json.load(buf)
@ -42,12 +59,12 @@ def read_spec(fname):
def check_spec(spec): def check_spec(spec):
if 'kind' not in spec: if "kind" not in spec:
yield "Missing field 'kind'" yield "Missing field 'kind'"
def timegenerate(spec, now=None, howmany=1): def timegenerate(spec, now=None, howmany=1):
Alarm = get_timegenerator(spec['kind']) Alarm = get_timegenerator(spec["kind"])
generator = Alarm(spec) generator = Alarm(spec)
if now is not None: if now is not None:
if type(now) is not datetime: if type(now) is not datetime:
@ -58,14 +75,14 @@ def timegenerate(spec, now=None, howmany=1):
def main(): def main():
'''Main function for the "larigira-timegen" executable''' """Main function for the "larigira-timegen" executable"""
args = get_parser().parse_args() args = get_parser().parse_args()
spec = read_spec(args.timespec[0]) spec = read_spec(args.timespec[0])
errors = tuple(check_spec(spec)) errors = tuple(check_spec(spec))
if errors: if errors:
log.error("Errors in timespec") log.error("Errors in timespec")
for err in errors: for err in errors:
sys.stderr.write('Error: {}\n'.format(err)) sys.stderr.write("Error: {}\n".format(err))
sys.exit(1) sys.exit(1)
now = None if args.now is None else args.now.pop() now = None if args.now is None else args.now.pop()
howmany = None if args.howmany is None else args.howmany.pop() howmany = None if args.howmany is None else args.howmany.pop()

View file

@ -1,6 +1,7 @@
from __future__ import print_function from __future__ import print_function
import logging import logging
log = logging.getLogger('time-every')
log = logging.getLogger("time-every")
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytimeparse.timeparse import timeparse from pytimeparse.timeparse import timeparse
@ -17,21 +18,21 @@ class Alarm(object):
pass pass
def next_ring(self, current_time=None): def next_ring(self, current_time=None):
'''if current_time is None, it is now() """if current_time is None, it is now()
returns the next time it will ring; or None if it will not anymore returns the next time it will ring; or None if it will not anymore
''' """
raise NotImplementedError() raise NotImplementedError()
def has_ring(self, time=None): def has_ring(self, time=None):
'''returns True IFF the alarm will ring exactly at ``time``''' """returns True IFF the alarm will ring exactly at ``time``"""
raise NotImplementedError() raise NotImplementedError()
def all_rings(self, current_time=None): def all_rings(self, current_time=None):
''' """
all future rings all future rings
this, of course, is an iterator (they could be infinite) this, of course, is an iterator (they could be infinite)
''' """
ring = self.next_ring(current_time) ring = self.next_ring(current_time)
while ring is not None: while ring is not None:
yield ring yield ring
@ -39,17 +40,18 @@ class Alarm(object):
class SingleAlarm(Alarm): class SingleAlarm(Alarm):
''' """
rings a single time rings a single time
''' """
description = 'Only once, at a specified date and time'
description = "Only once, at a specified date and time"
def __init__(self, obj): def __init__(self, obj):
super().__init__() super().__init__()
self.dt = getdate(obj['timestamp']) self.dt = getdate(obj["timestamp"])
def next_ring(self, current_time=None): def next_ring(self, current_time=None):
'''if current_time is None, it is now()''' """if current_time is None, it is now()"""
if current_time is None: if current_time is None:
current_time = datetime.now() current_time = datetime.now()
if current_time >= self.dt: if current_time >= self.dt:
@ -63,29 +65,28 @@ class SingleAlarm(Alarm):
class FrequencyAlarm(Alarm): class FrequencyAlarm(Alarm):
''' """
rings on {t | exists a k integer >= 0 s.t. t = start+k*t, start<t<end} rings on {t | exists a k integer >= 0 s.t. t = start+k*t, start<t<end}
''' """
description = 'Events at a specified frequency. Example: every 30minutes'
description = "Events at a specified frequency. Example: every 30minutes"
def __init__(self, obj): def __init__(self, obj):
self.start = getdate(obj['start']) self.start = getdate(obj["start"])
try: try:
self.interval = int(obj['interval']) self.interval = int(obj["interval"])
except ValueError: except ValueError:
self.interval = timeparse(obj['interval']) self.interval = timeparse(obj["interval"])
assert type(self.interval) is int assert type(self.interval) is int
self.end = getdate(obj['end']) if 'end' in obj else None self.end = getdate(obj["end"]) if "end" in obj else None
self.weekdays = [int(x) for x in obj['weekdays']] if \ self.weekdays = [int(x) for x in obj["weekdays"]] if "weekdays" in obj else None
'weekdays' in obj else None
if self.weekdays is not None: if self.weekdays is not None:
for weekday in self.weekdays: for weekday in self.weekdays:
if not 1 <= weekday <= 7: if not 1 <= weekday <= 7:
raise ValueError('Not a valid weekday: {}' raise ValueError("Not a valid weekday: {}".format(weekday))
.format(weekday))
def next_ring(self, current_time=None): def next_ring(self, current_time=None):
'''if current_time is None, it is now()''' """if current_time is None, it is now()"""
if current_time is None: if current_time is None:
current_time = datetime.now() current_time = datetime.now()
if self.end is not None and current_time > self.end: if self.end is not None and current_time > self.end:
@ -100,22 +101,22 @@ class FrequencyAlarm(Alarm):
# fact, it is necessary to retry until a valid event/weekday is # fact, it is necessary to retry until a valid event/weekday is
# found. a "while True" might have been more elegant (and maybe # found. a "while True" might have been more elegant (and maybe
# fast), but this gives a clear upper bound to the cycle. # fast), but this gives a clear upper bound to the cycle.
for _ in range(max(60*60*24*7 // self.interval, 1)): for _ in range(max(60 * 60 * 24 * 7 // self.interval, 1)):
n_interval = ( n_interval = (
(current_time - self.start).total_seconds() // self.interval (current_time - self.start).total_seconds() // self.interval
) + 1 ) + 1
ring = self.start + timedelta(seconds=self.interval * n_interval) ring = self.start + timedelta(seconds=self.interval * n_interval)
if ring == current_time: if ring == current_time:
ring += timedelta(seconds=self.interval) ring += timedelta(seconds=self.interval)
if self.end is not None and ring > self.end: if self.end is not None and ring > self.end:
return None return None
if self.weekdays is not None \ if self.weekdays is not None and ring.isoweekday() not in self.weekdays:
and ring.isoweekday() not in self.weekdays:
current_time = ring current_time = ring
continue continue
return ring return ring
log.warning("Can't find a valid time for event %s; " log.warning(
"something went wrong", str(self)) "Can't find a valid time for event %s; " "something went wrong", str(self)
)
return None return None
def has_ring(self, current_time=None): def has_ring(self, current_time=None):
@ -124,11 +125,9 @@ class FrequencyAlarm(Alarm):
if not self.start >= current_time >= self.end: if not self.start >= current_time >= self.end:
return False return False
n_interval = (current_time - self.start).total_seconds() // \ n_interval = (current_time - self.start).total_seconds() // self.interval
self.interval expected_time = self.start + timedelta(seconds=self.interval * n_interval)
expected_time = self.start + \
timedelta(seconds=self.interval * n_interval)
return expected_time == current_time return expected_time == current_time
def __str__(self): def __str__(self):
return 'FrequencyAlarm(every %ds)' % self.interval return "FrequencyAlarm(every %ds)" % self.interval

View file

@ -1,9 +1,9 @@
''' """
This component will look for files to be removed. There are some assumptions: This component will look for files to be removed. There are some assumptions:
* Only files in $TMPDIR are removed. Please remember that larigira has its * Only files in $TMPDIR are removed. Please remember that larigira has its
own specific TMPDIR own specific TMPDIR
* MPD URIs are parsed, and only file:/// is supported * MPD URIs are parsed, and only file:/// is supported
''' """
import os import os
from os.path import normpath from os.path import normpath
import logging import logging
@ -11,13 +11,14 @@ import mpd
def old_commonpath(directories): def old_commonpath(directories):
if any(p for p in directories if p.startswith('/')) and \ if any(p for p in directories if p.startswith("/")) and any(
any(p for p in directories if not p.startswith('/')): p for p in directories if not p.startswith("/")
):
raise ValueError("Can't mix absolute and relative paths") raise ValueError("Can't mix absolute and relative paths")
norm_paths = [normpath(p) + os.path.sep for p in directories] norm_paths = [normpath(p) + os.path.sep for p in directories]
ret = os.path.dirname(os.path.commonprefix(norm_paths)) ret = os.path.dirname(os.path.commonprefix(norm_paths))
if len(ret) > 0 and ret == '/' * len(ret): if len(ret) > 0 and ret == "/" * len(ret):
return '/' return "/"
return ret return ret
@ -35,35 +36,39 @@ class UnusedCleaner:
def _get_mpd(self): def _get_mpd(self):
mpd_client = mpd.MPDClient(use_unicode=True) mpd_client = mpd.MPDClient(use_unicode=True)
mpd_client.connect(self.conf['MPD_HOST'], self.conf['MPD_PORT']) mpd_client.connect(self.conf["MPD_HOST"], self.conf["MPD_PORT"])
return mpd_client return mpd_client
def watch(self, uri): def watch(self, uri):
''' """
adds fpath to the list of "watched" file adds fpath to the list of "watched" file
as soon as it leaves the mpc playlist, it is removed as soon as it leaves the mpc playlist, it is removed
''' """
if not uri.startswith('file:///'): if not uri.startswith("file:///"):
return # not a file URI return # not a file URI
fpath = uri[len('file://'):] fpath = uri[len("file://") :]
if 'TMPDIR' in self.conf and self.conf['TMPDIR'] \ if (
and commonpath([self.conf['TMPDIR'], fpath]) != \ "TMPDIR" in self.conf
normpath(self.conf['TMPDIR']): and self.conf["TMPDIR"]
self.log.info('Not watching file %s: not in TMPDIR', fpath) and commonpath([self.conf["TMPDIR"], fpath])
!= normpath(self.conf["TMPDIR"])
):
self.log.info("Not watching file %s: not in TMPDIR", fpath)
return return
if not os.path.exists(fpath): if not os.path.exists(fpath):
self.log.warning('a path that does not exist is being monitored') self.log.warning("a path that does not exist is being monitored")
self.waiting_removal_files.add(fpath) self.waiting_removal_files.add(fpath)
def check_playlist(self): def check_playlist(self):
'''check playlist + internal watchlist to see what can be removed''' """check playlist + internal watchlist to see what can be removed"""
mpdc = self._get_mpd() mpdc = self._get_mpd()
files_in_playlist = {song['file'] for song in mpdc.playlistid() files_in_playlist = {
if song['file'].startswith('/')} song["file"] for song in mpdc.playlistid() if song["file"].startswith("/")
}
for fpath in self.waiting_removal_files - files_in_playlist: for fpath in self.waiting_removal_files - files_in_playlist:
# we can remove it! # we can remove it!
self.log.debug('removing unused: %s', fpath) self.log.debug("removing unused: %s", fpath)
self.waiting_removal_files.remove(fpath) self.waiting_removal_files.remove(fpath)
if os.path.exists(fpath): if os.path.exists(fpath):
os.unlink(fpath) os.unlink(fpath)