formattazione con black
This commit is contained in:
parent
0a7cad0e2d
commit
d19b18eb3c
40 changed files with 949 additions and 811 deletions
2
.flake8
Normal file
2
.flake8
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[flake8]
|
||||||
|
max-line-length=79
|
|
@ -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(';'),
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(";"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
|
||||||
}
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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",)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
150
larigira/mpc.py
150
larigira/mpc.py
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue