From 67be3afb3d5e540bf684514d64bf09f3c8a0e246 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 26 Mar 2021 14:43:21 +0100 Subject: [PATCH] add docstrings and clean code with pylint --- __init__.py | 0 ffplayout.py | 4 + ffplayout/filters/a_volume.py | 7 ++ ffplayout/filters/default.py | 66 +++++++++++------ ffplayout/filters/v_drawtext.py | 6 ++ ffplayout/folder.py | 44 +++++++++-- ffplayout/output/desktop.py | 4 + ffplayout/output/hls.py | 4 + ffplayout/output/stream.py | 4 + ffplayout/playlist.py | 36 ++++++--- ffplayout/utils.py | 127 +++++++++++++++++++++++--------- 11 files changed, 226 insertions(+), 76 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ffplayout.py b/ffplayout.py index d737ba33..a118a77f 100755 --- a/ffplayout.py +++ b/ffplayout.py @@ -17,6 +17,10 @@ # ------------------------------------------------------------------------------ +""" +This module is the starting program for running ffplayout engine. +""" + import os from pydoc import locate diff --git a/ffplayout/filters/a_volume.py b/ffplayout/filters/a_volume.py index 82b31e94..287d89ca 100644 --- a/ffplayout/filters/a_volume.py +++ b/ffplayout/filters/a_volume.py @@ -1,6 +1,11 @@ +""" +cunstom audio filter, which get loaded automatically +""" + from ffplayout.utils import get_float, stdin_args +# pylint: disable=unused-argument def filter_link(node): """ set audio volume @@ -8,3 +13,5 @@ def filter_link(node): if stdin_args.volume and get_float(stdin_args.volume, False): return f'volume={stdin_args.volume}' + + return None diff --git a/ffplayout/filters/default.py b/ffplayout/filters/default.py index da5c3bb5..c10281ee 100644 --- a/ffplayout/filters/default.py +++ b/ffplayout/filters/default.py @@ -15,6 +15,11 @@ # ------------------------------------------------------------------------------ +""" +This module prepare all ffmpeg filters. +This is mainly for unify clips to have a unique output. +""" + import math import os import re @@ -31,6 +36,9 @@ from ffplayout.utils import (is_advertisement, lower_third, messenger, pre, def text_filter(): + """ + add drawtext filter for lower thirds messages + """ filter_chain = [] font = '' @@ -122,21 +130,21 @@ def fade_filter(duration, seek, out, track=''): return filter_chain -def overlay_filter(duration, ad, ad_last, ad_next): +def overlay_filter(duration, advertisement, ad_last, ad_next): """ overlay logo: when is an ad don't overlay, when ad is comming next fade logo out, when clip before was an ad fade logo in """ logo_filter = '[v]null' - scale_filter = '' + scale = '' - if pre.add_logo and os.path.isfile(pre.logo) and not ad: + if pre.add_logo and os.path.isfile(pre.logo) and not advertisement: logo_chain = [] if pre.logo_scale and \ re.match(r'\d+:-?\d+', pre.logo_scale): - scale_filter = f'scale={pre.logo_scale},' - logo_extras = (f'format=rgba,{scale_filter}' + scale = f'scale={pre.logo_scale},' + logo_extras = (f'format=rgba,{scale}' f'colorchannelmixer=aa={pre.logo_opacity}') loop = 'loop=loop=-1:size=1:start=0' logo_chain.append(f'movie={pre.logo},{loop},{logo_extras}') @@ -182,30 +190,33 @@ def extend_audio(probe, duration): """ check audio duration, is it shorter then clip duration - pad it """ - pad_filter = [] + pad = [] if probe.audio and 'duration' in probe.audio[0] and \ duration > float(probe.audio[0]['duration']) + 0.1: - pad_filter.append(f'apad=whole_dur={duration}') + pad.append(f'apad=whole_dur={duration}') - return pad_filter + return pad def extend_video(probe, duration, target_duration): """ check video duration, is it shorter then clip duration - pad it """ - pad_filter = [] + pad = [] vid_dur = probe.video[0].get('duration') if vid_dur and target_duration < duration > float(vid_dur) + 0.1: - pad_filter.append( + pad.append( f'tpad=stop_mode=add:stop_duration={duration - float(vid_dur)}') - return pad_filter + return pad def realtime_filter(duration, track=''): + """ + this realtime filter is important for HLS output to stay in sync + """ speed_filter = '' if pre.realtime: @@ -221,6 +232,10 @@ def realtime_filter(duration, track=''): def split_filter(filter_type): + """ + this filter splits the media input in multiple outputs, + to be able to have differnt streaming/HLS outputs + """ map_node = [] prefix = '' _filter = '' @@ -241,6 +256,9 @@ def split_filter(filter_type): def custom_filter(filter_type, node): + """ + read custom filters from filters folder + """ filter_dir = os.path.dirname(os.path.abspath(__file__)) filters = [] @@ -260,7 +278,7 @@ def build_filtergraph(node, node_last, node_next): build final filter graph, with video and audio chain """ - ad = is_advertisement(node) + advertisement = is_advertisement(node) ad_last = is_advertisement(node_last) ad_next = is_advertisement(node_next) @@ -277,12 +295,12 @@ def build_filtergraph(node, node_last, node_next): if probe and probe.video[0]: custom_v_filter = custom_filter('v', node) - video_chain += text_filter() - video_chain += deinterlace_filter(probe) - video_chain += pad_filter(probe) - video_chain += fps_filter(probe) - video_chain += scale_filter(probe) - video_chain += extend_video(probe, duration, out - seek) + video_chain += text_filter() \ + + deinterlace_filter(probe) \ + + pad_filter(probe) \ + + fps_filter(probe) \ + + scale_filter(probe) \ + + extend_video(probe, duration, out - seek) if custom_v_filter: video_chain += custom_v_filter video_chain += fade_filter(duration, seek, out) @@ -292,9 +310,9 @@ def build_filtergraph(node, node_last, node_next): if not audio_chain: custom_a_filter = custom_filter('a', node) - audio_chain.append('[0:a]anull') - audio_chain += add_loudnorm(probe) - audio_chain += extend_audio(probe, out - seek) + audio_chain += ['[0:a]anull'] \ + + add_loudnorm(probe) \ + + extend_audio(probe, out - seek) if custom_a_filter: audio_chain += custom_a_filter audio_chain += fade_filter(duration, seek, out, 'a') @@ -304,7 +322,7 @@ def build_filtergraph(node, node_last, node_next): else: video_filter = 'null[v]' - logo_filter = overlay_filter(out - seek, ad, ad_last, ad_next) + logo_filter = overlay_filter(out - seek, advertisement, ad_last, ad_next) v_speed = realtime_filter(out - seek) v_split = split_filter('v') video_map = ['-map', '[vout1]'] @@ -320,5 +338,5 @@ def build_filtergraph(node, node_last, node_next): if probe and probe.video[0]: return video_filter + audio_filter + video_map + audio_map - else: - return video_filter + video_map + ['-map', '1:a'] + + return video_filter + video_map + ['-map', '1:a'] diff --git a/ffplayout/filters/v_drawtext.py b/ffplayout/filters/v_drawtext.py index 28e27ea2..bd9a933e 100644 --- a/ffplayout/filters/v_drawtext.py +++ b/ffplayout/filters/v_drawtext.py @@ -1,3 +1,7 @@ +""" +cunstom video filter, which get loaded automatically +""" + import os import re @@ -19,3 +23,5 @@ def filter_link(node): if lower_third.text_from_filename: escape = title.replace("'", "'\\\\\\''").replace("%", "\\\\\\%") return f"drawtext=text='{escape}':{lower_third.style}{font}" + + return None diff --git a/ffplayout/folder.py b/ffplayout/folder.py index f04cdd81..0bf62a7d 100644 --- a/ffplayout/folder.py +++ b/ffplayout/folder.py @@ -15,6 +15,10 @@ # ------------------------------------------------------------------------------ +""" +This module handles folder reading. It monitor file adding, deleting or moving +""" + import glob import os import random @@ -49,31 +53,47 @@ class MediaStore: self.fill() def fill(self): + """ + fill media list + """ for ext in storage.extensions: self.store.extend( glob.glob(os.path.join(self.folder, '**', f'*{ext}'), recursive=True)) def sort_or_radomize(self): + """ + sort or randomize file list + """ if storage.shuffle: self.rand() else: self.sort() def add(self, file): + """ + add new file to media list + """ self.store.append(file) self.sort_or_radomize() def remove(self, file): + """ + remove file from media list + """ self.store.remove(file) self.sort_or_radomize() def sort(self): - # sort list for sorted playing + """ + sort list for sorted playing + """ self.store = sorted(self.store) def rand(self): - # randomize list for playing + """ + randomize list for playing + """ random.shuffle(self.store) @@ -100,7 +120,9 @@ class MediaWatcher: self.observer.start() def on_created(self, event): - # add file to media list only if it is completely copied + """ + add file to media list only if it is completely copied + """ file_size = -1 while file_size != os.path.getsize(event.src_path): file_size = os.path.getsize(event.src_path) @@ -111,6 +133,9 @@ class MediaWatcher: messenger.info(f'Add file to media list: "{event.src_path}"') def on_moved(self, event): + """ + operation when file on storage are moved + """ self._media.remove(event.src_path) self._media.add(event.dest_path) @@ -121,6 +146,9 @@ class MediaWatcher: ff_proc.decoder.terminate() def on_deleted(self, event): + """ + operation when file on storage are deleted + """ self._media.remove(event.src_path) messenger.info(f'Remove file from media list: "{event.src_path}"') @@ -129,6 +157,9 @@ class MediaWatcher: ff_proc.decoder.terminate() def stop(self): + """ + stop monitoring storage + """ self.observer.stop() self.observer.join() @@ -150,6 +181,9 @@ class GetSourceFromFolder: self.node_next = None def next(self): + """ + generator for getting always a new file + """ while True: while self.index < len(self._media.store): if self.node_next: @@ -188,5 +222,5 @@ class GetSourceFromFolder: yield self.node self.index += 1 self.node_last = deepcopy(self.node) - else: - self.index = 0 + + self.index = 0 diff --git a/ffplayout/output/desktop.py b/ffplayout/output/desktop.py index 9659fffc..756c8202 100644 --- a/ffplayout/output/desktop.py +++ b/ffplayout/output/desktop.py @@ -15,6 +15,10 @@ # ------------------------------------------------------------------------------ +""" +This module plays the compressed output directly on the desktop. +""" + import os from subprocess import PIPE, Popen from threading import Thread diff --git a/ffplayout/output/hls.py b/ffplayout/output/hls.py index 2de35793..7874027c 100644 --- a/ffplayout/output/hls.py +++ b/ffplayout/output/hls.py @@ -15,6 +15,10 @@ # ------------------------------------------------------------------------------ +""" +This module write the files compression directly to a hls (m3u8) playlist. +""" + import os import re from glob import iglob diff --git a/ffplayout/output/stream.py b/ffplayout/output/stream.py index d20fd12d..455ef130 100644 --- a/ffplayout/output/stream.py +++ b/ffplayout/output/stream.py @@ -15,6 +15,10 @@ # ------------------------------------------------------------------------------ +""" +This module streams the files out to a remote target. +""" + import os from subprocess import PIPE, Popen from threading import Thread diff --git a/ffplayout/playlist.py b/ffplayout/playlist.py index 5694d70b..1df56fb2 100644 --- a/ffplayout/playlist.py +++ b/ffplayout/playlist.py @@ -15,6 +15,12 @@ # ------------------------------------------------------------------------------ +""" +This module handles playlists, it can be aware of time syncing. +Empty, missing or any other playlist related failure should be compensate. +Missing clips will be replaced by a dummy clip. +""" + import os import socket import time @@ -50,11 +56,10 @@ def handle_list_init(node): node['out'] = out node['seek'] = seek return src_or_dummy(node) - else: - messenger.warning( - f'Clip less then a second, skip:\n{node["source"]}') - return None + messenger.warning(f'Clip less then a second, skip:\n{node["source"]}') + + return None def handle_list_end(duration, node): @@ -191,6 +196,11 @@ def validate_thread(clip_nodes, list_date): class PlaylistReader: + """ + Class which read playlists, it checks if playlist got modified, + when yes it reads the file new, when not it used the cached one + """ + def __init__(self, list_date, last_mod_time): self.list_date = list_date self.last_mod_time = last_mod_time @@ -198,13 +208,16 @@ class PlaylistReader: self.error = False def read(self): + """ + read and process playlist + """ self.nodes = {'program': []} self.error = False if stdin_args.playlist: json_file = stdin_args.playlist else: - year, month, day = self.list_date.split('-') + year, month, _ = self.list_date.split('-') json_file = os.path.join(playlist.path, year, month, f'{self.list_date}.json') @@ -231,8 +244,8 @@ class PlaylistReader: # check last modification time from playlist mod_time = os.path.getmtime(json_file) if mod_time > self.last_mod_time: - with open(json_file, 'r', encoding='utf-8') as f: - self.nodes = valid_json(f) + with open(json_file, 'r', encoding='utf-8') as playlist_file: + self.nodes = valid_json(playlist_file) self.last_mod_time = mod_time messenger.info('Open: ' + json_file) @@ -252,6 +265,7 @@ class GetSourceFromPlaylist: def __init__(self): self.prev_date = get_date(True) self.list_start = playlist.start + self.last_time = 0 self.first = True self.last = False self.clip_nodes = [] @@ -316,13 +330,13 @@ class GetSourceFromPlaylist: if self.last: seek = self.node['seek'] if self.node['seek'] > 0 else 0 - delta, total_delta = get_delta(begin) + delta, _ = get_delta(begin) delta += seek + 1 next_start = begin - playlist.start + out + delta else: - delta, total_delta = get_delta(begin) + delta, _ = get_delta(begin) next_start = begin - playlist.start + sync_op.threshold + delta if playlist.length and next_start >= playlist.length: @@ -456,8 +470,8 @@ class GetSourceFromPlaylist: # when we reach playlist end, stop script messenger.info('Playlist reached end!') return None - else: - self.eof_handling(begin) + + self.eof_handling(begin) if self.node: yield self.node diff --git a/ffplayout/utils.py b/ffplayout/utils.py index dfbe477e..0b1a6db6 100644 --- a/ffplayout/utils.py +++ b/ffplayout/utils.py @@ -15,6 +15,10 @@ # ------------------------------------------------------------------------------ +""" +This module contains default variables and helper functions +""" + import json import logging import math @@ -285,6 +289,9 @@ class CustomFormatter(logging.Formatter): } def format_message(self, msg): + """ + match strings with regex and add different color tags to it + """ if '"' in msg: msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg) elif '[decoder]' in msg: @@ -302,6 +309,9 @@ class CustomFormatter(logging.Formatter): return msg def format(self, record): + """ + override logging format + """ record.msg = self.format_message(record.getMessage()) log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt) @@ -378,13 +388,19 @@ class Mailer: self.temp_msg = os.path.join(tempfile.gettempdir(), 'ffplayout.txt') def current_time(self): + """ + set sending time + """ self.time = get_time(None) def send_mail(self, msg): + """ + send emails to specified recipients + """ if mail.recip: # write message to temp file for rate limit - with open(self.temp_msg, 'w+') as f: - f.write(msg) + with open(self.temp_msg, 'w+') as msg_file: + msg_file.write(msg) self.current_time() @@ -416,12 +432,14 @@ class Mailer: server.quit() def check_if_new(self, msg): - # send messege only when is new or the rate_limit is pass + """ + send messege only when is new or the rate_limit is pass + """ if os.path.isfile(self.temp_msg): mod_time = os.path.getmtime(self.temp_msg) - with open(self.temp_msg, 'r', encoding='utf-8') as f: - last_msg = f.read() + with open(self.temp_msg, 'r', encoding='utf-8') as msg_file: + last_msg = msg_file.read() if msg != last_msg \ or get_time('stamp') - mod_time > self.rate_limit: @@ -430,14 +448,23 @@ class Mailer: self.send_mail(msg) def info(self, msg): + """ + send emails with level INFO, WARNING and ERROR + """ if self.level in ['INFO']: self.check_if_new(msg) def warning(self, msg): + """ + send emails with level WARNING and ERROR + """ if self.level in ['INFO', 'WARNING']: self.check_if_new(msg) def error(self, msg): + """ + send emails with level ERROR + """ if self.level in ['INFO', 'WARNING', 'ERROR']: self.check_if_new(msg) @@ -451,18 +478,31 @@ class Messenger: def __init__(self): self._mailer = Mailer() + # pylint: disable=no-self-use def debug(self, msg): + """ + log debugging messages + """ playout_logger.debug(msg.replace('\n', ' ')) def info(self, msg): + """ + log and mail info messages + """ playout_logger.info(msg.replace('\n', ' ')) self._mailer.info(msg) def warning(self, msg): + """ + log and mail warning messages + """ playout_logger.warning(msg.replace('\n', ' ')) self._mailer.warning(msg) def error(self, msg): + """ + log and mail error messages + """ playout_logger.error(msg.replace('\n', ' ')) self._mailer.error(msg) @@ -521,6 +561,9 @@ FF_LIBS = ffmpeg_libs() def validate_ffmpeg_libs(): + """ + check if ffmpeg contains some basic libs + """ if 'libx264' not in FF_LIBS['libs']: playout_logger.error('ffmpeg contains no libx264!') if 'libfdk-aac' not in FF_LIBS['libs']: @@ -542,8 +585,18 @@ class MediaProbe: get infos about media file, similare to mediainfo """ - def load(self, file): + def __init__(self): self.remote_source = ['http', 'https', 'ftp', 'smb', 'sftp'] + self.src = None + self.format = None + self.audio = [] + self.video = [] + self.is_remote = False + + def load(self, file): + """ + load media file with ffprobe and get infos out of it + """ self.src = file self.format = None self.audio = [] @@ -582,14 +635,14 @@ class MediaProbe: if stream['codec_type'] == 'video': if stream.get('display_aspect_ratio'): - w, h = stream['display_aspect_ratio'].split(':') - stream['aspect'] = float(w) / float(h) + width, heigth = stream['display_aspect_ratio'].split(':') + stream['aspect'] = float(width) / float(heigth) else: stream['aspect'] = float( stream['width']) / float(stream['height']) - a, b = stream['r_frame_rate'].split('/') - stream['fps'] = float(a) / float(b) + rate, factor = stream['r_frame_rate'].split('/') + stream['fps'] = float(rate) / float(factor) self.video.append(stream) @@ -602,9 +655,10 @@ def handle_sigterm(sig, frame): """ handler for ctrl+c signal """ - raise(SystemExit) + raise SystemExit +# pylint: disable=unused-argument def handle_sighub(sig, frame): """ handling SIGHUB signal for reload configuration @@ -694,14 +748,15 @@ def get_date(seek_day, next_start=0): when seek_day is set: check if playlist date must be from yesterday """ - d = date.today() + date_ = date.today() if seek_day and playlist.start > get_time('full_sec'): - return (d - timedelta(1)).strftime('%Y-%m-%d') - elif playlist.start == 0 and next_start >= 86400: - return (d + timedelta(1)).strftime('%Y-%m-%d') - else: - return d.strftime('%Y-%m-%d') + return (date_ - timedelta(1)).strftime('%Y-%m-%d') + + if playlist.start == 0 and next_start >= 86400: + return (date_ + timedelta(1)).strftime('%Y-%m-%d') + + return date_.strftime('%Y-%m-%d') def get_float(value, default=False): @@ -721,6 +776,8 @@ def is_advertisement(node): if node and node.get('category') == 'advertisement': return True + return False + def valid_json(file): """ @@ -804,35 +861,33 @@ def gen_filler(node): if probe.format: if probe.format.get('duration'): - filler_duration = float(probe.format['duration']) - if filler_duration > duration: + filler_dur = float(probe.format['duration']) + if filler_dur > duration: # cut filler messenger.info( f'Generate filler with {duration:.2f} seconds') node['source'] = storage.filler node['src_cmd'] = ['-i', storage.filler] + set_length( - filler_duration, 0, duration) + filler_dur, 0, duration) return node - else: - # loop file n times - node['src_cmd'] = loop_input(storage.filler, filler_duration, - duration) - return node - else: - messenger.error("Can't get filler length, generate dummy!") - dummy = gen_dummy(duration) - node['source'] = dummy[3] - node['src_cmd'] = dummy + + # loop file n times + node['src_cmd'] = loop_input(storage.filler, filler_dur, duration) return node - else: - # when no filler is set, generate a dummy - messenger.warning('No filler is set!') + messenger.error("Can't get filler length, generate dummy!") dummy = gen_dummy(duration) node['source'] = dummy[3] node['src_cmd'] = dummy return node + # when no filler is set, generate a dummy + messenger.warning('No filler is set!') + dummy = gen_dummy(duration) + node['source'] = dummy[3] + node['src_cmd'] = dummy + return node + def src_or_dummy(node): """ @@ -862,7 +917,7 @@ def src_or_dummy(node): ] + set_length(node['duration'], node['seek'], node['out'] - node['seek']) else: - # FIXME: when list starts with looped clip, + # when list starts with looped clip, # the logo length will be wrong node['src_cmd'] = loop_input(node['source'], node['duration'], node['out']) @@ -884,5 +939,5 @@ def pre_audio_codec(): """ if pre.add_loudnorm: return ['-c:a', 'mp2', '-b:a', '384k', '-ar', '48000', '-ac', '2'] - else: - return ['-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2'] + + return ['-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2']