Merge pull request #85 from ffplayout/cleanup

Update dev with cleanup branch
This commit is contained in:
jb-alvarado 2021-02-04 13:11:29 +01:00 committed by GitHub
commit edb2b9b29d
12 changed files with 250 additions and 227 deletions

View File

@ -41,7 +41,7 @@ def main():
"""
if stdin_args.mode:
output = locate('ffplayout.output.{}.output'.format(stdin_args.mode))
output = locate(f'ffplayout.output.{stdin_args.mode}.output')
output()
else:
@ -54,7 +54,7 @@ def main():
mode = os.path.splitext(output)[0]
if mode == _playout.mode:
output = locate('ffplayout.output.{}.output'.format(mode))
output = locate(f'ffplayout.output.{mode}.output')
output()

View File

@ -0,0 +1,15 @@
# Custom Configuration
Extend your arguments for using them in your custom extensions.
The file name must have the **argparse_** prefix. The content should look like:
```YAML
short: -v
long: --volume
help: set audio volume
```
At least **short** or **long** have to exist, all other parameters are optional. You can also extend the config, with keys which are exist in **ArgumentParser.add_argument()**.
**Every argument must have its own yaml file!**

View File

@ -0,0 +1,3 @@
short: -v
long: --volume
help: set audio volume

View File

@ -7,3 +7,18 @@ Add your one filters here. They must have the correct file naming:
The file itself should contain only one filter in a function named `def filter(prope):`
Check **v_addtext.py** for example.
In your filter you can also read custom properties from the current program node. That you can use for any usecase you wish, like reading a subtitle file, or a different logo for every clip and so on.
The normal program node looks like:
```JSON
{
"in": 0,
"out": 3600.162,
"duration": 3600.162,
"source": "/dir/input.mp4"
}
```
This you can extend to your needs, and apply this values to your filters.

View File

@ -0,0 +1,10 @@
from ffplayout.utils import get_float, stdin_args
def filter(probe, node=None):
"""
set audio volume
"""
if stdin_args.volume and get_float(stdin_args.volume, False):
return f'volume={stdin_args.volume}'

View File

@ -23,7 +23,8 @@ import re
from glob import glob
from pydoc import locate
from ffplayout.utils import _global, _pre, _text
from ffplayout.utils import (_global, _pre, _text, get_float, is_advertisement,
messenger)
# ------------------------------------------------------------------------------
# building filters,
@ -37,7 +38,7 @@ def text_filter():
if _text.add_text and _text.over_pre:
if _text.fontfile and os.path.isfile(_text.fontfile):
font = ":fontfile='{}'".format(_text.fontfile)
font = f":fontfile='{_text.fontfile}'"
filter_chain = [
"null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}".format(
_text.address.replace(':', '\\:'), font)]
@ -70,12 +71,10 @@ def pad_filter(probe):
_pre.aspect, abs_tol=0.03):
if probe.video[0]['aspect'] < _pre.aspect:
filter_chain.append(
'pad=ih*{}/{}/sar:ih:(ow-iw)/2:(oh-ih)/2'.format(_pre.w,
_pre.h))
f'pad=ih*{_pre.w}/{_pre.h}/sar:ih:(ow-iw)/2:(oh-ih)/2')
elif probe.video[0]['aspect'] > _pre.aspect:
filter_chain.append(
'pad=iw:iw*{}/{}/sar:(ow-iw)/2:(oh-ih)/2'.format(_pre.h,
_pre.w))
f'pad=iw:iw*{_pre.h}/{_pre.w}/sar:(ow-iw)/2:(oh-ih)/2')
return filter_chain
@ -87,7 +86,7 @@ def fps_filter(probe):
filter_chain = []
if probe.video[0]['fps'] != _pre.fps:
filter_chain.append('fps={}'.format(_pre.fps))
filter_chain.append(f'fps={_pre.fps}')
return filter_chain
@ -101,11 +100,11 @@ def scale_filter(probe):
if int(probe.video[0]['width']) != _pre.w or \
int(probe.video[0]['height']) != _pre.h:
filter_chain.append('scale={}:{}'.format(_pre.w, _pre.h))
filter_chain.append(f'scale={_pre.w}:{_pre.h}')
if not math.isclose(probe.video[0]['aspect'],
_pre.aspect, abs_tol=0.03):
filter_chain.append('setdar=dar={}'.format(_pre.aspect))
filter_chain.append(f'setdar=dar={_pre.aspect}')
return filter_chain
@ -117,11 +116,10 @@ def fade_filter(duration, seek, out, track=''):
filter_chain = []
if seek > 0.0:
filter_chain.append('{}fade=in:st=0:d=0.5'.format(track))
filter_chain.append(f'{track}fade=in:st=0:d=0.5')
if out != duration:
filter_chain.append('{}fade=out:st={}:d=1.0'.format(track,
out - seek - 1.0))
filter_chain.append(f'{track}fade=out:st={out - seek - 1.0}:d=1.0')
return filter_chain
@ -139,35 +137,32 @@ def overlay_filter(duration, ad, ad_last, ad_next):
logo_chain = []
if _pre.logo_scale and \
re.match(r'\d+:-?\d+', _pre.logo_scale):
scale_filter = 'scale={},'.format(_pre.logo_scale)
logo_extras = 'format=rgba,{}colorchannelmixer=aa={}'.format(
scale_filter, _pre.logo_opacity)
scale_filter = f'scale={_pre.logo_scale},'
logo_extras = (f'format=rgba,{scale_filter}'
f'colorchannelmixer=aa={_pre.logo_opacity}')
loop = 'loop=loop=-1:size=1:start=0'
logo_chain.append(
'movie={},{},{}'.format(_pre.logo, loop, logo_extras))
logo_chain.append(f'movie={_pre.logo},{loop},{logo_extras}')
if ad_last:
logo_chain.append('fade=in:st=0:d=1.0:alpha=1')
if ad_next:
logo_chain.append('fade=out:st={}:d=1.0:alpha=1'.format(
duration - 1))
logo_chain.append(f'fade=out:st={duration - 1}:d=1.0:alpha=1')
logo_filter = '{}[l];[v][l]{}:shortest=1'.format(
','.join(logo_chain), _pre.logo_filter)
logo_filter = (f'{",".join(logo_chain)}[l];[v][l]'
f'{_pre.logo_filter}:shortest=1')
return logo_filter
def add_audio(probe, duration, msg):
def add_audio(probe, duration):
"""
when clip has no audio we generate an audio line
"""
line = []
if not probe.audio:
msg.warning('Clip "{}" has no audio!'.format(probe.src))
line = [
'aevalsrc=0:channel_layout=2:duration={}:sample_rate={}'.format(
duration, 48000)]
messenger.warning(f'Clip "{probe.src}" has no audio!')
line = [(f'aevalsrc=0:channel_layout=2:duration={duration}:'
f'sample_rate={48000}')]
return line
@ -179,8 +174,8 @@ def add_loudnorm(probe):
loud_filter = []
if probe.audio and _pre.add_loudnorm:
loud_filter = [('loudnorm=I={}:TP={}:LRA={}').format(
_pre.loud_i, _pre.loud_tp, _pre.loud_lra)]
loud_filter = [
f'loudnorm=I={_pre.loud_i}:TP={_pre.loud_tp}:LRA={_pre.loud_lra}']
return loud_filter
@ -193,7 +188,7 @@ def extend_audio(probe, duration):
if probe.audio and 'duration' in probe.audio[0] and \
duration > float(probe.audio[0]['duration']) + 0.1:
pad_filter.append('apad=whole_dur={}'.format(duration))
pad_filter.append(f'apad=whole_dur={duration}')
return pad_filter
@ -203,12 +198,11 @@ def extend_video(probe, duration, target_duration):
check video duration, is it shorter then clip duration - pad it
"""
pad_filter = []
vid_dur = probe.video[0].get('duration')
if 'duration' in probe.video[0] and \
target_duration < duration > float(
probe.video[0]['duration']) + 0.1:
pad_filter.append('tpad=stop_mode=add:stop_duration={}'.format(
duration - float(probe.video[0]['duration'])))
if vid_dur and target_duration < duration > float(vid_dur) + 0.1:
pad_filter.append(
f'tpad=stop_mode=add:stop_duration={duration - float(vid_dur)}')
return pad_filter
@ -217,46 +211,45 @@ def realtime_filter(duration, track=''):
speed_filter = ''
if _pre.realtime:
speed_filter = ',{}realtime=speed=1'.format(track)
speed_filter = f',{track}realtime=speed=1'
if _global.time_delta < 0:
speed = duration / (duration + _global.time_delta)
if speed < 1.1:
speed_filter = ',{}realtime=speed={}'.format(track, speed)
speed_filter = f',{track}realtime=speed={speed}'
return speed_filter
def split_filter(filter_type):
map_node = []
filter_prefix = ''
prefix = ''
_filter = ''
if filter_type == 'a':
filter_prefix = 'a'
prefix = 'a'
if _pre.output_count > 1:
for num in range(_pre.output_count):
map_node.append('[{}out{}]'.format(filter_type, num + 1))
map_node.append(f'[{filter_type}out{num + 1}]')
_filter = ',{}split={}{}'.format(filter_prefix, _pre.output_count,
''.join(map_node))
_filter = f',{prefix}split={_pre.output_count}{"".join(map_node)}'
else:
_filter = '[{}out1]'.format(filter_type)
_filter = f'[{filter_type}out1]'
return _filter
def custom_filter(probe, type):
def custom_filter(probe, type, node):
filter_dir = os.path.dirname(os.path.abspath(__file__))
filters = []
for filter in glob(os.path.join(filter_dir, f'{type}_*')):
filter = os.path.splitext(os.path.basename(filter))[0]
filter_func = locate(f'ffplayout.filters.{filter}.filter')
link = filter_func(probe)
link = filter_func(probe, node)
if link is not None:
filters.append(link)
@ -264,10 +257,17 @@ def custom_filter(probe, type):
return filters
def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
def build_filtergraph(node, node_last, node_next, seek, probe):
"""
build final filter graph, with video and audio chain
"""
duration = get_float(node['duration'], 20)
out = get_float(node['out'], duration)
ad = is_advertisement(node)
ad_last = is_advertisement(node_last)
ad_next = is_advertisement(node_next)
video_chain = []
audio_chain = []
@ -275,7 +275,7 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
seek = 0
if probe.video[0]:
custom_v_filter = custom_filter(probe, 'v')
custom_v_filter = custom_filter(probe, 'v', node)
video_chain += text_filter()
video_chain += deinterlace_filter(probe)
video_chain += pad_filter(probe)
@ -286,10 +286,10 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
video_chain += custom_v_filter
video_chain += fade_filter(duration, seek, out)
audio_chain += add_audio(probe, out - seek, msg)
audio_chain += add_audio(probe, out - seek)
if not audio_chain:
custom_a_filter = custom_filter(probe, 'a')
custom_a_filter = custom_filter(probe, 'a', node)
audio_chain.append('[0:a]anull')
audio_chain += add_loudnorm(probe)
@ -299,7 +299,7 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
audio_chain += fade_filter(duration, seek, out, 'a')
if video_chain:
video_filter = '{}[v]'.format(','.join(video_chain))
video_filter = f'{",".join(video_chain)}[v]'
else:
video_filter = 'null[v]'
@ -308,15 +308,14 @@ def build_filtergraph(duration, seek, out, ad, ad_last, ad_next, probe, msg):
v_split = split_filter('v')
video_map = ['-map', '[vout1]']
video_filter = [
'-filter_complex', '[0:v]{};{}{}{}'.format(
video_filter, logo_filter, v_speed, v_split)]
'-filter_complex',
f'[0:v]{video_filter};{logo_filter}{v_speed}{v_split}']
a_speed = realtime_filter(out - seek, 'a')
a_split = split_filter('a')
audio_map = ['-map', '[aout1]']
audio_filter = [
'-filter_complex', '{}{}{}'.format(','.join(audio_chain),
a_speed, a_split)]
'-filter_complex', f'{",".join(audio_chain)}{a_speed}{a_split}']
if probe.video[0]:
return video_filter + audio_filter + video_map + audio_map

View File

@ -4,7 +4,7 @@ import re
from ffplayout.utils import _text
def filter(probe):
def filter(probe, node=None):
"""
extract title from file name and overlay it
"""

View File

@ -52,7 +52,7 @@ class MediaStore:
def fill(self):
for ext in _storage.extensions:
self.store.extend(
glob.glob(os.path.join(self.folder, '**', '*{}'.format(ext)),
glob.glob(os.path.join(self.folder, '**', f'*{ext}'),
recursive=True))
if _storage.shuffle:
@ -84,7 +84,7 @@ class MediaWatcher:
def __init__(self, media):
self._media = media
self.extensions = ['*{}'.format(ext) for ext in _storage.extensions]
self.extensions = [f'*{ext}' for ext in _storage.extensions]
self.event_handler = PatternMatchingEventHandler(
patterns=self.extensions)
@ -107,14 +107,14 @@ class MediaWatcher:
self._media.add(event.src_path)
messenger.info('Add file to media list: "{}"'.format(event.src_path))
messenger.info(f'Add file to media list: "{event.src_path}"')
def on_moved(self, event):
self._media.remove(event.src_path)
self._media.add(event.dest_path)
messenger.info('Move file from "{}" to "{}"'.format(event.src_path,
event.dest_path))
messenger.info(
f'Move file from "{event.src_path}" to "{event.dest_path}"')
if _current.clip == event.src_path:
_ff.decoder.terminate()
@ -122,8 +122,7 @@ class MediaWatcher:
def on_deleted(self, event):
self._media.remove(event.src_path)
messenger.info(
'Remove file from media list: "{}"'.format(event.src_path))
messenger.info(f'Remove file from media list: "{event.src_path}"')
if _current.clip == event.src_path:
_ff.decoder.terminate()

View File

@ -21,16 +21,15 @@ def output():
ff_pre_settings = [
'-pix_fmt', 'yuv420p', '-r', str(_pre.fps),
'-c:v', 'mpeg2video', '-intra',
'-b:v', '{}k'.format(_pre.v_bitrate),
'-minrate', '{}k'.format(_pre.v_bitrate),
'-maxrate', '{}k'.format(_pre.v_bitrate),
'-bufsize', '{}k'.format(_pre.v_bufsize)
'-b:v', f'{_pre.v_bitrate}k',
'-minrate', f'{_pre.v_bitrate}k',
'-maxrate', f'{_pre.v_bitrate}k',
'-bufsize', f'{_pre.v_bufsize}k'
] + pre_audio_codec() + ['-f', 'mpegts', '-']
if _text.add_text and not _text.over_pre:
messenger.info('Using drawtext node, listening on address: {}'.format(
_text.address
))
messenger.info(
f'Using drawtext node, listening on address: {_text.address}')
overlay = [
'-vf',
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
@ -38,9 +37,13 @@ def output():
]
try:
_ff.encoder = Popen([
enc_cmd = [
'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0'
] + overlay, stderr=PIPE, stdin=PIPE, stdout=None)
] + overlay
messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"')
_ff.encoder = Popen(enc_cmd, stderr=PIPE, stdin=PIPE, stdout=None)
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.encoder.stderr, False))
@ -58,20 +61,21 @@ def output():
try:
for src_cmd in get_source.next():
messenger.debug('src_cmd: "{}"'.format(src_cmd))
if src_cmd[0] == '-i':
current_file = src_cmd[1]
else:
current_file = src_cmd[3]
_current.clip = current_file
messenger.info('Play: "{}"'.format(current_file))
messenger.info(f'Play: {current_file}')
with Popen([
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
'-nostats'] + src_cmd + ff_pre_settings,
stdout=PIPE, stderr=PIPE) as _ff.decoder:
dec_cmd = ['ffmpeg', '-v', _log.ff_level.lower(),
'-hide_banner', '-nostats'
] + src_cmd + ff_pre_settings
messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"')
with Popen(dec_cmd, stdout=PIPE, stderr=PIPE) as _ff.decoder:
dec_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.decoder.stderr, True))
dec_err_thread.daemon = True

View File

@ -23,16 +23,15 @@ def output():
ff_pre_settings = [
'-pix_fmt', 'yuv420p', '-r', str(_pre.fps),
'-c:v', 'mpeg2video', '-intra',
'-b:v', '{}k'.format(_pre.v_bitrate),
'-minrate', '{}k'.format(_pre.v_bitrate),
'-maxrate', '{}k'.format(_pre.v_bitrate),
'-bufsize', '{}k'.format(_pre.v_bufsize)
'-b:v', f'{_pre.v_bitrate}k',
'-minrate', f'{_pre.v_bitrate}k',
'-maxrate', f'{_pre.v_bitrate}k',
'-bufsize', f'{_pre.v_bufsize}k'
] + pre_audio_codec() + ['-f', 'mpegts', '-']
if _text.add_text and not _text.over_pre:
messenger.info('Using drawtext node, listening on address: {}'.format(
_text.address
))
messenger.info(
f'Using drawtext node, listening on address: {_text.address}')
overlay = [
'-vf',
"null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format(
@ -40,15 +39,18 @@ def output():
]
try:
_ff.encoder = Popen([
enc_cmd = [
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
'-nostats', '-re', '-thread_queue_size', '256', '-i', 'pipe:0'
] + overlay + [
'-metadata', 'service_name=' + _playout.name,
'-metadata', 'service_provider=' + _playout.provider,
'-metadata', 'year={}'.format(year)
] + _playout.ffmpeg_param + _playout.stream_output,
stdin=PIPE, stderr=PIPE)
'-metadata', f'year={year}'
] + _playout.ffmpeg_param + _playout.stream_output
messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"')
_ff.encoder = Popen(enc_cmd, stdin=PIPE, stderr=PIPE)
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.encoder.stderr, False))
@ -66,20 +68,21 @@ def output():
try:
for src_cmd in get_source.next():
messenger.debug('src_cmd: "{}"'.format(src_cmd))
if src_cmd[0] == '-i':
current_file = src_cmd[1]
else:
current_file = src_cmd[3]
_current.clip = current_file
messenger.info('Play: "{}"'.format(current_file))
messenger.info(f'Play: {current_file}')
with Popen([
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
'-nostats'] + src_cmd + ff_pre_settings,
stdout=PIPE, stderr=PIPE) as _ff.decoder:
dec_cmd = ['ffmpeg', '-v', _log.ff_level.lower(),
'-hide_banner', '-nostats'
] + src_cmd + ff_pre_settings
messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"')
with Popen(dec_cmd, stdout=PIPE, stderr=PIPE) as _ff.decoder:
dec_err_thread = Thread(target=ffmpeg_stderr_reader,
args=(_ff.decoder.stderr, True))
dec_err_thread.daemon = True

View File

@ -25,7 +25,7 @@ from urllib import request
from .filters.default import build_filtergraph
from .utils import (MediaProbe, _playlist, gen_filler, get_date, get_delta,
get_time, is_float, messenger, stdin_args, timed_source,
get_float, get_time, messenger, stdin_args, timed_source,
valid_json, validate_thread)
@ -58,6 +58,9 @@ class GetSourceFromPlaylist:
self.last = False
self.list_date = get_date(True)
self.node = None
self.node_last = None
self.node_next = None
self.src = None
self.begin = 0
self.seek = 0
@ -107,61 +110,26 @@ class GetSourceFromPlaylist:
else:
self.clip_nodes = None
def get_clip_in_out(self, node):
if is_float(node["in"]):
self.seek = node["in"]
else:
self.seek = 0
if is_float(node["duration"]):
self.duration = node["duration"]
else:
self.duration = 20
if is_float(node["out"]):
self.out = node["out"]
else:
self.out = self.duration
def get_input(self):
self.src_cmd, self.seek, self.out, self.next_playlist = timed_source(
self.probe, self.src, self.begin, self.duration,
self.seek, self.out, self.first, self.last
)
def get_category(self, index, node):
if 'category' in node:
def last_and_next_node(self, index):
if index - 1 >= 0:
last_category = self.clip_nodes[
"program"][index - 1]["category"]
self.node_last = self.clip_nodes['program'][index - 1]
else:
last_category = 'noad'
self.node_last = None
if index + 2 <= len(self.clip_nodes["program"]):
next_category = self.clip_nodes[
"program"][index + 1]["category"]
if index + 2 <= len(self.clip_nodes['program']):
self.node_next = self.clip_nodes['program'][index + 1]
else:
next_category = 'noad'
if node["category"] == 'advertisement':
self.ad = True
else:
self.ad = False
if last_category == 'advertisement':
self.ad_last = True
else:
self.ad_last = False
if next_category == 'advertisement':
self.ad_next = True
else:
self.ad_next = False
self.node_next = None
def set_filtergraph(self):
self.filtergraph = build_filtergraph(
self.duration, self.seek, self.out, self.ad, self.ad_last,
self.ad_next, self.probe, messenger)
self.node, self.node_last, self.node_next, self.seek, self.probe)
def check_for_next_playlist(self):
if not self.next_playlist:
@ -207,13 +175,13 @@ class GetSourceFromPlaylist:
self.last = False
def peperation_task(self, index, node):
def peperation_task(self, index):
# call functions in order to prepare source and filter
self.src = node["source"]
self.src = self.node['source']
self.probe.load(self.src)
self.get_input()
self.get_category(index, node)
self.last_and_next_node(index)
self.set_filtergraph()
self.check_for_next_playlist()
@ -223,30 +191,32 @@ class GetSourceFromPlaylist:
if self.clip_nodes is None:
self.eof_handling(
'No valid playlist:\n{}'.format(self.json_file), True, 30)
f'No valid playlist:\n{self.json_file}', True, 30)
yield self.src_cmd + self.filtergraph
continue
self.begin = self.init_time
# loop through all clips in playlist and get correct clip in time
for index, node in enumerate(self.clip_nodes["program"]):
self.get_clip_in_out(node)
for index, self.node in enumerate(self.clip_nodes['program']):
self.seek = get_float(self.node['in'], 0)
self.duration = get_float(self.node['duration'], 20)
self.out = get_float(self.node['out'], self.duration)
# first time we end up here
if self.first and \
self.last_time < self.begin + self.out - self.seek:
self.peperation_task(index, node)
self.peperation_task(index)
self.first = False
break
elif self.last_time < self.begin:
if index + 1 == len(self.clip_nodes["program"]):
if index + 1 == len(self.clip_nodes['program']):
self.last = True
else:
self.last = False
self.peperation_task(index, node)
self.peperation_task(index)
break
self.begin += self.out - self.seek

View File

@ -32,6 +32,7 @@ from datetime import date, datetime, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
from glob import glob
from logging.handlers import TimedRotatingFileHandler
from shutil import which
from subprocess import STDOUT, CalledProcessError, check_output
@ -40,13 +41,15 @@ from types import SimpleNamespace
import yaml
# path to user define configs
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'config')
# ------------------------------------------------------------------------------
# argument parsing
# ------------------------------------------------------------------------------
stdin_parser = ArgumentParser(
description='python and ffmpeg based playout',
epilog="don't use parameters if you want to use this settings from config")
stdin_parser = ArgumentParser(description='python and ffmpeg based playout')
stdin_parser.add_argument(
'-c', '--config', help='file path to ffplayout.conf'
@ -82,6 +85,19 @@ stdin_parser.add_argument(
help='set length in "hh:mm:ss", "none" for no length check'
)
# read dynamical new arguments
for arg_file in glob(os.path.join(CONFIG_PATH, 'argparse_*')):
with open(arg_file, 'r') as _file:
config = yaml.safe_load(_file)
short = config.pop('short') if config.get('short') else None
long = config.pop('long') if config.get('long') else None
stdin_parser.add_argument(
*filter(None, [short, long]),
**config
)
stdin_args = stdin_parser.parse_args()
@ -270,7 +286,7 @@ class CustomFormatter(logging.Formatter):
}
def format_message(self, msg):
if '"' in msg and '[' in msg:
if '"' in msg:
msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg)
elif '[decoder]' in msg:
msg = re.sub(r'(\[decoder\])', self.reset + r'\1', msg)
@ -377,7 +393,7 @@ class Mailer:
message['To'] = _mail.recip
message['Subject'] = _mail.subject
message['Date'] = formatdate(localtime=True)
message.attach(MIMEText('{} {}'.format(self.time, msg), 'plain'))
message.attach(MIMEText(f'{self.time} {msg}', 'plain'))
text = message.as_string()
try:
@ -463,7 +479,7 @@ def is_in_system(name):
Check whether name is on PATH and marked as executable
"""
if which(name) is None:
messenger.error('{} is not found on system'.format(name))
messenger.error(f'{name} is not found on system')
sys.exit(1)
@ -483,7 +499,7 @@ def ffmpeg_libs():
info = check_output(cmd, stderr=STDOUT).decode('UTF-8')
except CalledProcessError as err:
messenger.error('ffmpeg - libs could not be readed!\n'
'Processing is not possible. Error:\n{}'.format(err))
f'Processing is not possible. Error:\n{err}')
sys.exit(1)
for line in info.split('\n'):
@ -550,8 +566,7 @@ class MediaProbe:
try:
info = json.loads(check_output(cmd).decode('UTF-8'))
except CalledProcessError as err:
messenger.error('MediaProbe error in: "{}"\n {}'.format(self.src,
err))
messenger.error(f'MediaProbe error in: "{self.src}"\n{err}')
self.audio.append(None)
self.video.append(None)
@ -564,12 +579,12 @@ class MediaProbe:
self.audio.append(stream)
if stream['codec_type'] == 'video':
if 'display_aspect_ratio' not in stream:
stream['aspect'] = float(
stream['width']) / float(stream['height'])
else:
if stream.get('display_aspect_ratio'):
w, h = stream['display_aspect_ratio'].split(':')
stream['aspect'] = float(w) / float(h)
else:
stream['aspect'] = float(
stream['width']) / float(stream['height'])
a, b = stream['r_frame_rate'].split('/')
stream['fps'] = float(a) / float(b)
@ -628,14 +643,11 @@ def ffmpeg_stderr_reader(std_errors, decoder):
try:
for line in std_errors:
if _log.ff_level == 'INFO':
logger.info('{}{}'.format(
prefix, line.decode("utf-8").rstrip()))
logger.info(f'{prefix}{line.decode("utf-8").rstrip()}')
elif _log.ff_level == 'WARNING':
logger.warning('{}{}'.format(
prefix, line.decode("utf-8").rstrip()))
logger.warning(f'{prefix}{line.decode("utf-8").rstrip()}')
else:
logger.error('{}{}'.format(
prefix, line.decode("utf-8").rstrip()))
logger.error(f'{prefix}{line.decode("utf-8").rstrip()}')
except ValueError:
pass
@ -648,32 +660,28 @@ def get_date(seek_day):
"""
d = date.today()
if seek_day and get_time('full_sec') < _playlist.start:
yesterday = d - timedelta(1)
return yesterday.strftime('%Y-%m-%d')
return (d - timedelta(1)).strftime('%Y-%m-%d')
else:
if _playlist.start == 0 and \
4 > 86400.0 - get_time('full_sec') > 0:
return (d + timedelta(1)).strftime('%Y-%m-%d')
return d.strftime('%Y-%m-%d')
def is_float(value):
def get_float(value, default=False):
"""
test if value is float
"""
try:
float(value)
return True
return float(value)
except (ValueError, TypeError):
return False
return default
def is_int(value):
"""
test if value is int
"""
try:
int(value)
def is_advertisement(node):
if node and node.get('category') == 'advertisement':
return True
except ValueError:
return False
def valid_json(file):
@ -684,7 +692,7 @@ def valid_json(file):
json_object = json.load(file)
return json_object
except ValueError:
messenger.error("Playlist {} is not JSON conform".format(file))
messenger.error(f'Playlist {file} is not JSON conform')
return None
@ -699,8 +707,8 @@ def check_sync(delta):
if _general.stop and abs(delta) > _general.threshold:
messenger.error(
'Sync tolerance value exceeded with {0:.2f} seconds,\n'
'program terminated!'.format(delta))
f'Sync tolerance value exceeded with {delta:.2f} seconds,\n'
'program terminated!')
terminate_processes()
sys.exit(1)
@ -712,11 +720,9 @@ def check_length(total_play_time):
if _playlist.length and total_play_time < _playlist.length - 5 \
and not stdin_args.loop:
messenger.error(
'Playlist ({}) is not long enough!\n'
'Total play time is: {}, target length is: {}'.format(
get_date(True),
timedelta(seconds=total_play_time),
timedelta(seconds=_playlist.length))
f'Playlist ({get_date(True)}) is not long enough!\n'
f'Total play time is: {timedelta(seconds=total_play_time)}, '
f'target length is: {timedelta(seconds=_playlist.length)}'
)
@ -735,29 +741,35 @@ def validate_thread(clip_nodes):
source = node["source"]
probe.load(source)
missing = []
_in = get_float(node.get('in'), 0)
_out = get_float(node.get('out'), 0)
duration = get_float(node.get('duration'), 0)
if probe.is_remote:
if not probe.video[0]:
missing.append('Stream not exist: "{}"'.format(source))
missing.append(f'Stream not exist: "{source}"')
elif not os.path.isfile(source):
missing.append('File not exist: "{}"'.format(source))
missing.append(f'File not exist: "{source}"')
if is_float(node["in"]) and is_float(node["out"]):
counter += node["out"] - node["in"]
else:
missing.append('Missing Value in: "{}"'.format(node))
if not node.get('in') == 0 and not _in:
missing.append(f'No in Value in: "{node}"')
if not is_float(node["duration"]):
missing.append('No duration Value!')
if not node.get('out') and not _out:
missing.append(f'No out Value in: "{node}"')
if not node.get('duration') and not duration:
missing.append(f'No duration Value in: "{node}"')
counter += _out - _in
line = '\n'.join(missing)
if line:
error += line + '\nIn line: {}\n\n'.format(node)
error += line + f'\nIn line: {node}\n\n'
if error:
messenger.error(
'Validation error, check JSON playlist, '
'values are missing:\n{}'.format(error)
f'values are missing:\n{error}'
)
check_length(counter)
@ -790,9 +802,8 @@ def set_length(duration, seek, out):
def loop_input(source, src_duration, target_duration):
# loop filles n times
loop_count = math.ceil(target_duration / src_duration)
messenger.info(
'Loop "{0}" {1} times, total duration: {2:.2f}'.format(
source, loop_count, target_duration))
messenger.info(f'Loop "{source}" {loop_count} times, '
f'total duration: {target_duration:.2f}')
return ['-stream_loop', str(loop_count),
'-i', source, '-t', str(target_duration)]
@ -806,11 +817,9 @@ def gen_dummy(duration):
# noise = 'noise=alls=50:allf=t+u,hue=s=0'
return [
'-f', 'lavfi', '-i',
'color=c={}:s={}x{}:d={}:r={},format=pix_fmts=yuv420p'.format(
color, _pre.w, _pre.h, duration, _pre.fps
),
'-f', 'lavfi', '-i', 'anoisesrc=d={}:c=pink:r=48000:a=0.05'.format(
duration)
f'color=c={color}:s={_pre.w}x{_pre.h}:d={duration}:r={_pre.fps},'
'format=pix_fmts=yuv420p',
'-f', 'lavfi', '-i', f'anoisesrc=d={duration}:c=pink:r=48000:a=0.05'
]
@ -822,12 +831,12 @@ def gen_filler(duration):
probe.load(_storage.filler)
if probe.format:
if 'duration' in probe.format:
if probe.format.get('duration'):
filler_duration = float(probe.format['duration'])
if filler_duration > duration:
# cut filler
messenger.info(
'Generate filler with {0:.2f} seconds'.format(duration))
f'Generate filler with {duration:.2f} seconds')
return probe, ['-i', _storage.filler] + set_length(
filler_duration, 0, duration)
else:
@ -854,13 +863,13 @@ def src_or_dummy(probe, src, dur, seek, out):
if probe.is_remote and probe.video[0]:
if seek > 0.0:
messenger.warning(
'Seek in live source "{}" not supported!'.format(src))
f'Seek in live source "{src}" not supported!')
return ['-i', src] + set_length(86400.0, seek, out)
elif src and os.path.isfile(src):
if out > dur:
if seek > 0.0:
messenger.warning(
'Seek in looped source "{}" not supported!'.format(src))
f'Seek in looped source "{src}" not supported!')
return ['-i', src] + set_length(dur, seek, out - seek)
else:
# FIXME: when list starts with looped clip,
@ -869,7 +878,7 @@ def src_or_dummy(probe, src, dur, seek, out):
else:
return seek_in(seek) + ['-i', src] + set_length(dur, seek, out)
else:
messenger.error('Clip/URL not exist:\n{}'.format(src))
messenger.error(f'Clip/URL not exist:\n{src}')
return gen_dummy(out - seek)
@ -938,30 +947,27 @@ def handle_list_end(probe, new_length, src, begin, dur, seek, out):
if new_out > dur:
new_out = dur
else:
messenger.info(
'We are over time, new length is: {0:.2f}'.format(new_length))
messenger.info(f'We are over time, new length is: {new_length:.2f}')
missing_secs = abs(new_length - (dur - seek))
if dur > new_length > 1.5 and dur - seek >= new_length:
src_cmd = src_or_dummy(probe, src, dur, seek, new_out)
elif dur > new_length > 0.0:
messenger.info(
'Last clip less then 1.5 second long, skip:\n{}'.format(src))
messenger.info(f'Last clip less then 1.5 second long, skip:\n{src}')
src_cmd = None
if missing_secs > 2:
new_playlist = False
messenger.error(
'Reach playlist end,\n{0:.2f} seconds needed.'.format(
missing_secs))
f'Reach playlist end,\n{missing_secs:.2f} seconds needed.')
else:
new_out = out
new_playlist = False
src_cmd = src_or_dummy(probe, src, dur, seek, out)
messenger.error(
'Playlist is not long enough:'
'\n{0:.2f} seconds needed.'.format(missing_secs))
f'Playlist is not long enough:\n{missing_secs:.2f} seconds needed.'
)
return src_cmd, seek, new_out, new_playlist
@ -981,14 +987,14 @@ def timed_source(probe, src, begin, dur, seek, out, first, last):
return src_or_dummy(probe, src, dur, _seek, _out), \
_seek, _out, new_list
else:
messenger.warning('Clip less then a second, skip:\n{}'.format(src))
messenger.warning(f'Clip less then a second, skip:\n{src}')
return None, 0, 0, True
else:
if not stdin_args.loop and _playlist.length:
check_sync(current_delta)
messenger.debug('current_delta: {:f}'.format(current_delta))
messenger.debug('total_delta: {:f}'.format(total_delta))
messenger.debug(f'current_delta: {current_delta:f}')
messenger.debug(f'total_delta: {total_delta:f}')
if (total_delta > out - seek and not last) \
or stdin_args.loop or not _playlist.length:
@ -996,8 +1002,7 @@ def timed_source(probe, src, begin, dur, seek, out, first, last):
return src_or_dummy(probe, src, dur, seek, out), seek, out, False
elif total_delta <= 0:
messenger.info(
'Start time is over playtime, skip clip:\n{}'.format(src))
messenger.info(f'Start time is over playtime, skip clip:\n{src}')
return None, 0, 0, True
elif total_delta < out - seek or last: