diff --git a/bin/ffx.py b/bin/ffx.py index 7977bcc..9165f52 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -1,12 +1,11 @@ #! /usr/bin/python3 -import os, sys, subprocess, json, click, time, re +import os, click, time, logging from ffx.file_properties import FileProperties from ffx.ffx_app import FfxApp from ffx.ffx_controller import FfxController -from ffx.show_controller import ShowController from ffx.tmdb_controller import TmdbController from ffx.database import databaseContext @@ -32,11 +31,47 @@ VERSION='0.1.3' @click.group() @click.pass_context @click.option('--database-file', type=str, default='', help='Path to database file') -def ffx(ctx, database_file): +@click.option('-v', '--verbose', type=int, default=0, help='Set verbosity of output') +@click.option("--dry-run", is_flag=True, default=False) +def ffx(ctx, database_file, verbose, dry_run): """FFX""" ctx.obj = {} ctx.obj['database'] = databaseContext(databasePath=database_file) + ctx.obj['dry_run'] = dry_run + ctx.obj['verbosity'] = verbose + + # Critical 50 + # Error 40 + # Warning 30 + # Info 20 + # Debug 10 + fileLogVerbosity = max(40 - verbose * 10, 10) + consoleLogVerbosity = max(20 - verbose * 10, 10) + + homeDir = os.path.expanduser("~") + ffxLogDir = os.path.join(homeDir, '.local', 'var', 'log') + if not os.path.exists(ffxLogDir): + os.makedirs(ffxLogDir) + ffxLogFilePath = os.path.join(ffxLogDir, 'ffx.log') + + ctx.obj['logger'] = logging.getLogger('FFX') + ctx.obj['logger'].setLevel(logging.DEBUG) + + ffxFileHandler = logging.FileHandler(ffxLogFilePath) + ffxFileHandler.setLevel(fileLogVerbosity) + ffxConsoleHandler = logging.StreamHandler() + ffxConsoleHandler.setLevel(consoleLogVerbosity) + + fileFormatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ffxFileHandler.setFormatter(fileFormatter) + consoleFormatter = logging.Formatter( + '%(message)s') + ffxConsoleHandler.setFormatter(consoleFormatter) + + ctx.obj['logger'].addHandler(ffxConsoleHandler) + ctx.obj['logger'].addHandler(ffxFileHandler) # Define a subcommand @@ -52,8 +87,6 @@ def help(): click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]") - - @ffx.command() @click.pass_context @click.argument('filename', nargs=1) @@ -107,16 +140,15 @@ def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix, @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') @click.option("-o", "--output-directory", type=str, default='') @click.option("-s", "--subtitles-only", is_flag=True, default=False) -@click.option("--dry-run", is_flag=True, default=False) def unmux(ctx, paths, label, output_directory, - subtitles_only, - dry_run): + subtitles_only): existingSourcePaths = [p for p in paths if os.path.isfile(p)] - click.echo(f"\nUnmuxing {len(existingSourcePaths)} files") + if ctx.obj['verbosity'] > 0: + click.echo(f"\nUnmuxing {len(existingSourcePaths)} files") for sourcePath in existingSourcePaths: @@ -134,10 +166,12 @@ def unmux(ctx, targetIndicator = f"_S{season}E{episode}" if label and season != -1 and episode != -1 else '' if label and not targetIndicator: - click.echo(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized") + if ctx.obj['verbosity'] > 0: + click.echo(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized") continue else: - click.echo(f"\nUnmuxing file {fp.getFilename()}\n") + if ctx.obj['verbosity'] > 0: + click.echo(f"\nUnmuxing file {fp.getFilename()}\n") for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors(): @@ -149,15 +183,19 @@ def unmux(ctx, unmuxSequence = getUnmuxSequence(trackDescriptor, sourcePath, targetPrefix, targetDirectory = output_directory) if unmuxSequence: - if not dry_run: - click.echo(f"Executing unmuxing sequence: {' '.join(unmuxSequence)}") + if not ctx.obj['dry_run']: + if ctx.obj['verbosity'] > 0: + click.echo(f"Executing unmuxing sequence: {' '.join(unmuxSequence)}") out, err, rc = executeProcess(unmuxSequence) if rc: - click.echo(f"Unmuxing of stream {trackDescriptor.getIndex()} failed with error ({rc}) {err}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Unmuxing of stream {trackDescriptor.getIndex()} failed with error ({rc}) {err}") else: - click.echo(f"Skipping stream with unknown codec {trackDescriptor.getCodec()}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Skipping stream with unknown codec {trackDescriptor.getCodec()}") except Exception as ex: - click.echo(f"Skipping File {sourcePath} ({ex})") + if ctx.obj['verbosity'] > 0: + click.echo(f"Skipping File {sourcePath} ({ex})") @ffx.command() @@ -215,9 +253,6 @@ def shows(ctx): @click.option("-j", "--no-jellyfin", is_flag=True, default=False) @click.option("-np", "--no-pattern", is_flag=True, default=False) -@click.option("--dry-run", is_flag=True, default=False) - - def convert(ctx, paths, label, @@ -246,8 +281,7 @@ def convert(ctx, denoise, no_tmdb, no_jellyfin, - no_pattern, - dry_run): + no_pattern): """Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin Files found under PATHS will be converted according to parameters. @@ -258,8 +292,6 @@ def convert(ctx, startTime = time.perf_counter() context = ctx.obj - - context['dry_run'] = dry_run context['video_encoder'] = VideoEncoder.fromLabel(video_encoder) @@ -277,16 +309,18 @@ def convert(ctx, qualityTokens = quality.split(',') q_list = [q for q in qualityTokens if q.isnumeric()] - click.echo(f"Qualities: {q_list}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Qualities: {q_list}") context['bitrates'] = {} context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k" context['bitrates']['ac3'] = str(ac3_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k" context['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k" - click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}") - click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}") - click.echo(f"DTS bitrate: {context['bitrates']['dts']}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}") + click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}") + click.echo(f"DTS bitrate: {context['bitrates']['dts']}") # Process crop parameters @@ -296,13 +330,15 @@ def convert(ctx, if cTokens and len(cTokens) == 2: context['crop_start'] = int(cTokens[0]) context['crop_length'] = int(cTokens[1]) - click.echo(f"Crop start={context['crop_start']} length={context['crop_length']}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Crop start={context['crop_start']} length={context['crop_length']}") tc = TmdbController() if context['use_tmdb'] else None existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS] - click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs") + if ctx.obj['verbosity'] > 0: + click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs") jobIndex = 0 for sourcePath in existingSourcePaths: @@ -315,7 +351,8 @@ def convert(ctx, sourceFileBasename = '.'.join(sourcePathTokens[:-1]) sourceFilenameExtension = sourcePathTokens[-1] - click.echo(f"\nProcessing file {sourcePath}") + if ctx.obj['verbosity'] > 0: + click.echo(f"\nProcessing file {sourcePath}") mediaFileProperties = FileProperties(context, sourceFilename) @@ -324,7 +361,8 @@ def convert(ctx, #HINT: This is None if the filename did not match anything in database currentPattern = mediaFileProperties.getPattern() if context['use_pattern'] else None - click.echo(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}") # fileBasename = '' @@ -385,9 +423,9 @@ def convert(ctx, # Case pattern matching - targetMediaDescriptor = currentPattern.getMediaDescriptor() + targetMediaDescriptor = currentPattern.getMediaDescriptor(ctx.obj) - currentShowDescriptor = currentPattern.getShowDescriptor() + currentShowDescriptor = currentPattern.getShowDescriptor(ctx.obj) if context['use_tmdb']: @@ -422,7 +460,8 @@ def convert(ctx, # click.echo(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}") # raise click.Abort - click.echo(f"Input mapping tokens (2nd pass): {targetMediaDescriptor.getInputMappingTokens()}") + if ctx.obj['verbosity'] > 0: + click.echo(f"Input mapping tokens (2nd pass): {targetMediaDescriptor.getInputMappingTokens()}") fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor) @@ -435,14 +474,16 @@ def convert(ctx, # audioTokens = fc.generateAudioEncodingTokens() # click.echo(f"Audio Tokens: {audioTokens}") - click.echo(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}") - + if ctx.obj['verbosity'] > 0: + click.echo(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}") - click.echo(f"fileBasename={sourceFileBasename}") + if ctx.obj['verbosity'] > 0: + click.echo(f"fileBasename={sourceFileBasename}") for q in q_list: - click.echo(f"\nRunning job {jobIndex} file={sourcePath} q={q}") + if ctx.obj['verbosity'] > 0: + click.echo(f"\nRunning job {jobIndex} file={sourcePath} q={q}") jobIndex += 1 extra = ['ffx'] if sourceFilenameExtension == FfxController.DEFAULT_FILE_EXTENSION else [] @@ -464,7 +505,8 @@ def convert(ctx, #TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True) endTime = time.perf_counter() - click.echo(f"\nDONE\nTime elapsed {endTime - startTime}") + if ctx.obj['verbosity'] > 0: + click.echo(f"\nDONE\nTime elapsed {endTime - startTime}") if __name__ == '__main__': diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index e4f4fec..70fb886 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -404,7 +404,7 @@ class FfxController(): FfxController.DEFAULT_FILE_FORMAT, FfxController.DEFAULT_FILE_EXTENSION) - click.echo(f"Command: {' '.join(commandSequence)}") + # click.echo(f"Command: {' '.join(commandSequence)}") if not self.__context['dry_run']: executeProcess(commandSequence) @@ -421,7 +421,7 @@ class FfxController(): commandSequence1 += FfxController.NULL_TOKENS - click.echo(f"Command 1: {' '.join(commandSequence1)}") + # click.echo(f"Command 1: {' '.join(commandSequence1)}") if os.path.exists(FfxController.TEMP_FILE_NAME): os.remove(FfxController.TEMP_FILE_NAME) @@ -449,7 +449,7 @@ class FfxController(): FfxController.DEFAULT_FILE_FORMAT, FfxController.DEFAULT_FILE_EXTENSION) - click.echo(f"Command 2: {' '.join(commandSequence2)}") + # click.echo(f"Command 2: {' '.join(commandSequence2)}") if not self.__context['dry_run']: out, err, rc = executeProcess(commandSequence2) diff --git a/bin/ffx/file_properties.py b/bin/ffx/file_properties.py index 99a9afa..9397388 100644 --- a/bin/ffx/file_properties.py +++ b/bin/ffx/file_properties.py @@ -23,6 +23,8 @@ class FileProperties(): self.context = context + self.__logger = context['logger'] + # Separate basedir, basename and extension for current source file self.__sourcePath = sourcePath @@ -167,7 +169,7 @@ class FileProperties(): def getMediaDescriptor(self): - return MediaDescriptor.fromFfprobe(self.getFormatData(), self.getStreamData()) + return MediaDescriptor.fromFfprobe(self.context, self.getFormatData(), self.getStreamData()) def getShowId(self) -> int: @@ -233,6 +235,6 @@ class FileProperties(): targetFilename = '_'.join(targetFilenameTokens) - click.echo(f"Target filename: {targetFilename}") + self.__logger.debug(f"assembleTargetFileBasename(): Target filename: {targetFilename}") return targetFilename diff --git a/bin/ffx/helper.py b/bin/ffx/helper.py index b89b4b2..e4774a8 100644 --- a/bin/ffx/helper.py +++ b/bin/ffx/helper.py @@ -32,7 +32,6 @@ def dictDiff(a : dict, b : dict): def dictCache(element: dict, cache: list = []): for index in range(len(cache)): diff = dictDiff(cache[index], element) - click.echo(f"dictCache() element={element} index={index} cached={cache[index]} diff={diff}") if not diff: return index, cache cache.append(element) diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index cd3dd1e..7dcf9c9 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -1,6 +1,4 @@ -import os -import re -import click +import os, re, click, logging from typing import List, Self @@ -36,6 +34,17 @@ class MediaDescriptor: def __init__(self, **kwargs): + if MediaDescriptor.CONTEXT_KEY in kwargs.keys(): + if type(kwargs[MediaDescriptor.CONTEXT_KEY]) is not dict: + raise TypeError( + f"MediaDescriptor.__init__(): Argument {MediaDescriptor.CONTEXT_KEY} is required to be of type dict" + ) + self.__context = kwargs[MediaDescriptor.CONTEXT_KEY] + self.__logger = self.__context['logger'] + else: + self.__context = {} + self.__logger = logging.getLogger('FFX').addHandler(logging.NullHandler()) + if MediaDescriptor.TAGS_KEY in kwargs.keys(): if type(kwargs[MediaDescriptor.TAGS_KEY]) is not dict: raise TypeError( @@ -149,10 +158,12 @@ class MediaDescriptor: @classmethod - def fromFfprobe(cls, formatData, streamData): + def fromFfprobe(cls, context, formatData, streamData): kwargs = {} + kwargs[MediaDescriptor.CONTEXT_KEY] = context + if MediaDescriptor.FFPROBE_TAGS_KEY in formatData.keys(): kwargs[MediaDescriptor.TAGS_KEY] = formatData[ MediaDescriptor.FFPROBE_TAGS_KEY @@ -242,16 +253,13 @@ class MediaDescriptor: def compare(self, vsMediaDescriptor: Self): if not isinstance(vsMediaDescriptor, self.__class__): - raise click.ClickException( - f"MediaDescriptor.compare(): Argument is required to be of type {self.__class__}" - ) + errorMessage = f"MediaDescriptor.compare(): Argument is required to be of type {self.__class__}" + self.__logger.error(errorMessage) + # raise click.ClickException(errorMessage) + click.Abort() vsTags = vsMediaDescriptor.getTags() tags = self.getTags() - - # tags ist leer - # click.echo(f"tags={tags} vsTags={vsTags}") - # raise click.Abort # HINT: Some tags differ per file, for example creation_time, so these are removed before diff for emt in MediaDescriptor.EXCLUDED_MEDIA_TAGS: @@ -342,7 +350,7 @@ class MediaDescriptor: def getInputMappingTokens(self, use_sub_index: bool = True, only_video: bool = False): - """?: Tracks must be reordered for source index order""" + """Tracks must be reordered for source index order""" # reorderedTrackDescriptors = self.getReorderedTrackDescriptors() inputMappingTokens = [] @@ -405,22 +413,25 @@ class MediaDescriptor: subtitleFileDescriptors.append(subtitleFileDescriptor) - click.echo(f"Available subtitle files {subtitleFileDescriptors}\n") + self.__logger.debug(f"searchSubtitleFiles(): Available subtitle files {subtitleFileDescriptors}") return subtitleFileDescriptors def importSubtitles(self, searchDirectory, prefix, season: int = -1, episode: int = -1): - click.echo(f"Season: {season} Episode: {episode}") + # click.echo(f"Season: {season} Episode: {episode}") + self.__logger.debug(f"importSubtitles(): Season: {season} Episode: {episode}") availableFileSubtitleDescriptors = self.searchSubtitleFiles(searchDirectory, prefix) - click.echo(f"availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}") + # click.echo(f"availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}") + self.__logger.debug(f"importSubtitles(): availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}") subtitleTracks = self.getSubtitleTracks() - click.echo(f"subtitleTracks: {[s.getIndex() for s in subtitleTracks]}") + # click.echo(f"subtitleTracks: {[s.getIndex() for s in subtitleTracks]}") + self.__logger.debug(f"importSubtitles(): subtitleTracks: {[s.getIndex() for s in subtitleTracks]}") # if len(availableFileSubtitleDescriptors) != len(subtitleTracks): # raise click.ClickException(f"MediaDescriptor.importSubtitles(): Number if subtitle files not matching number of subtitle tracks") @@ -438,10 +449,13 @@ class MediaDescriptor: else [] ) - click.echo(f"matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}") + # click.echo(f"matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}") + self.__logger.debug(f"importSubtitles(): matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}") + click.echo(f"importSubtitles(): matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}") for msfd in matchingSubtitleFileDescriptors: matchingSubtitleTrackDescriptor = [s for s in subtitleTracks if s.getIndex() == msfd["index"]] if matchingSubtitleTrackDescriptor: - click.echo(f"Found matching subtitle file {msfd["path"]}\n") + # click.echo(f"Found matching subtitle file {msfd["path"]}\n") + self.__logger.debug(f"importSubtitles(): Found matching subtitle file {msfd["path"]}") matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath(msfd["path"]) diff --git a/bin/ffx/media_details_screen.py b/bin/ffx/media_details_screen.py index 3858c54..b2971b3 100644 --- a/bin/ffx/media_details_screen.py +++ b/bin/ffx/media_details_screen.py @@ -157,7 +157,7 @@ class MediaDetailsScreen(Screen): self.__currentPattern = self.__mediaFileProperties.getPattern() # keine tags vorhanden - self.__targetMediaDescriptor = self.__currentPattern.getMediaDescriptor() if self.__currentPattern is not None else None + self.__targetMediaDescriptor = self.__currentPattern.getMediaDescriptor(self.context) if self.__currentPattern is not None else None # Enumerating differences between media descriptors # from file (=current) vs from stored in database (=target) @@ -452,7 +452,7 @@ class MediaDetailsScreen(Screen): selected_track_data = self.tracksTable.get_row(row_key) kwargs = {} - + kwargs[TrackDescriptor.CONTEXT_KEY] = self.context kwargs[TrackDescriptor.INDEX_KEY] = int(selected_track_data[0]) kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.fromLabel(selected_track_data[1]) kwargs[TrackDescriptor.SUB_INDEX_KEY] = int(selected_track_data[2]) diff --git a/bin/ffx/model/pattern.py b/bin/ffx/model/pattern.py index 2d1adbc..92a7978 100644 --- a/bin/ffx/model/pattern.py +++ b/bin/ffx/model/pattern.py @@ -41,9 +41,9 @@ class Pattern(Base): def getShowId(self): return int(self.show_id) - def getShowDescriptor(self) -> ShowDescriptor: - click.echo(f"self.show {self.show} id={self.show_id}") - return self.show.getDescriptor() + def getShowDescriptor(self, context) -> ShowDescriptor: + # click.echo(f"self.show {self.show} id={self.show_id}") + return self.show.getDescriptor(context) def getId(self): return int(self.id) @@ -55,11 +55,13 @@ class Pattern(Base): return {str(t.key):str(t.value) for t in self.media_tags} - def getMediaDescriptor(self): + def getMediaDescriptor(self, context): kwargs = {} + + kwargs[MediaDescriptor.CONTEXT_KEY] = context + kwargs[MediaDescriptor.TAGS_KEY] = self.getTags() - kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = [] # Set ordered subindices @@ -68,7 +70,7 @@ class Pattern(Base): trackType = track.getType() if not trackType in subIndexCounter.keys(): subIndexCounter[trackType] = 0 - kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(track.getDescriptor(subIndex = subIndexCounter[trackType])) + kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(track.getDescriptor(context, subIndex = subIndexCounter[trackType])) subIndexCounter[trackType] += 1 return MediaDescriptor(**kwargs) diff --git a/bin/ffx/model/show.py b/bin/ffx/model/show.py index eab42e3..7d82e39 100644 --- a/bin/ffx/model/show.py +++ b/bin/ffx/model/show.py @@ -44,10 +44,10 @@ class Show(Base): indicator_episode_digits = Column(Integer, default=ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS) - def getDescriptor(self): + def getDescriptor(self, context): kwargs = {} - + kwargs[ShowDescriptor.CONTEXT_KEY] = context kwargs[ShowDescriptor.ID_KEY] = int(self.id) kwargs[ShowDescriptor.NAME_KEY] = str(self.name) kwargs[ShowDescriptor.YEAR_KEY] = int(self.year) diff --git a/bin/ffx/model/track.py b/bin/ffx/model/track.py index 6af0481..be7dc10 100644 --- a/bin/ffx/model/track.py +++ b/bin/ffx/model/track.py @@ -189,10 +189,12 @@ class Track(Base): return bool(self.disposition_flags & 2**disposition.index()) - def getDescriptor(self, subIndex : int = -1) -> TrackDescriptor: + def getDescriptor(self, context, subIndex : int = -1) -> TrackDescriptor: kwargs = {} + kwargs[TrackDescriptor.CONTEXT_KEY] = context + kwargs[TrackDescriptor.ID_KEY] = self.getId() kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.getPatternId() diff --git a/bin/ffx/pattern_controller.py b/bin/ffx/pattern_controller.py index c77ad7b..47c19cb 100644 --- a/bin/ffx/pattern_controller.py +++ b/bin/ffx/pattern_controller.py @@ -137,14 +137,14 @@ class PatternController(): finally: s.close() - def getMediaDescriptor(self, patternId): + def getMediaDescriptor(self, context, patternId): try: s = self.Session() q = s.query(Pattern).filter(Pattern.id == int(patternId)) if q.count(): - return q.first().getMediaDescriptor() + return q.first().getMediaDescriptor(context) else: return None diff --git a/bin/ffx/pattern_details_screen.py b/bin/ffx/pattern_details_screen.py index f37abb5..0ea2406 100644 --- a/bin/ffx/pattern_details_screen.py +++ b/bin/ffx/pattern_details_screen.py @@ -126,7 +126,7 @@ class PatternDetailsScreen(Screen): for tr in tracks: - td : TrackDescriptor = tr.getDescriptor() + td : TrackDescriptor = tr.getDescriptor(self.context) trackType = td.getType() if not trackType in typeCounter.keys(): @@ -292,7 +292,7 @@ class PatternDetailsScreen(Screen): trackIndex = int(selected_track_data[0]) trackSubIndex = int(selected_track_data[2]) - return self.__tc.getTrack(self.__pattern.getId(), trackIndex).getDescriptor(subIndex=trackSubIndex) + return self.__tc.getTrack(self.__pattern.getId(), trackIndex).getDescriptor(self.context, subIndex=trackSubIndex) else: return None diff --git a/bin/ffx/show_controller.py b/bin/ffx/show_controller.py index 85ff125..a426eaf 100644 --- a/bin/ffx/show_controller.py +++ b/bin/ffx/show_controller.py @@ -20,7 +20,7 @@ class ShowController(): if q.count(): show: Show = q.first() - return show.getDescriptor() + return show.getDescriptor(self.context) except Exception as ex: raise click.ClickException(f"ShowController.getShowDescriptor(): {repr(ex)}") diff --git a/bin/ffx/show_descriptor.py b/bin/ffx/show_descriptor.py index 5108ee6..09e4702 100644 --- a/bin/ffx/show_descriptor.py +++ b/bin/ffx/show_descriptor.py @@ -1,19 +1,10 @@ -import click - -from typing import List, Self - -from ffx.track_type import TrackType -from ffx.track_disposition import TrackDisposition - -from ffx.track_descriptor import TrackDescriptor - -from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY +import logging class ShowDescriptor(): """This class represents the structural content of a media file including streams and metadata""" - # CONTEXT_KEY = 'context' + CONTEXT_KEY = 'context' ID_KEY = 'id' NAME_KEY = 'name' @@ -32,6 +23,17 @@ class ShowDescriptor(): def __init__(self, **kwargs): + if ShowDescriptor.CONTEXT_KEY in kwargs.keys(): + if type(kwargs[ShowDescriptor.CONTEXT_KEY]) is not dict: + raise TypeError( + f"ShowDescriptor.__init__(): Argument {ShowDescriptor.CONTEXT_KEY} is required to be of type dict" + ) + self.__context = kwargs[ShowDescriptor.CONTEXT_KEY] + self.__logger = self.__context['logger'] + else: + self.__context = {} + self.__logger = logging.getLogger('FFX').addHandler(logging.NullHandler()) + if ShowDescriptor.ID_KEY in kwargs.keys(): if type(kwargs[ShowDescriptor.ID_KEY]) is not int: raise TypeError(f"ShowDescriptor.__init__(): Argument {ShowDescriptor.ID_KEY} is required to be of type int") diff --git a/bin/ffx/test/helper.py b/bin/ffx/test/helper.py index e89b7b6..b68cdd4 100644 --- a/bin/ffx/test/helper.py +++ b/bin/ffx/test/helper.py @@ -169,7 +169,7 @@ def createMediaTestFile(mediaDescriptor: MediaDescriptor, if trackType == TrackType.VIDEO: cacheIndex, generatorCache = dictCache({'type': TrackType.VIDEO}, generatorCache) - click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}") + # click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}") if cacheIndex == -1: generatorTokens += ['-f', @@ -192,9 +192,9 @@ def createMediaTestFile(mediaDescriptor: MediaDescriptor, audioLayout = 'stereo' cacheIndex, generatorCache = dictCache({'type': TrackType.AUDIO, 'layout': audioLayout}, generatorCache) - click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}") + # click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}") - click.echo(f"generartorCache index={cacheIndex} len={len(generatorCache)}") + # click.echo(f"generartorCache index={cacheIndex} len={len(generatorCache)}") if cacheIndex == -1: generatorTokens += ['-f', 'lavfi', @@ -214,7 +214,7 @@ def createMediaTestFile(mediaDescriptor: MediaDescriptor, if trackType == TrackType.SUBTITLE: cacheIndex, generatorCache = dictCache({'type': TrackType.SUBTITLE}, generatorCache) - click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}") + # click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}") if cacheIndex == -1: importTokens = ['-i', createVttFile(SHORT_SUBTITLE_SEQUENCE)] @@ -254,10 +254,10 @@ def createMediaTestFile(mediaDescriptor: MediaDescriptor, commandTokens += [outputPath] - print(f"command sequence: {commandTokens}") + # click.echo(f"command sequence: {commandTokens}") out, err, rc = executeProcess(commandTokens) - if rc: - print(f"Creating testfile failed with {rc}: {err}") + #if rc: + # click.echo(f"Creating testfile failed with {rc}: {err}") return outputPath diff --git a/bin/ffx/test/scenario.py b/bin/ffx/test/scenario.py index d77fac1..e9e0dab 100644 --- a/bin/ffx/test/scenario.py +++ b/bin/ffx/test/scenario.py @@ -14,6 +14,9 @@ class Scenario(): os.path.dirname(__file__))), 'ffx.py') + self._logger = context['logger'] + self._reportLogger = context['report_logger'] + @staticmethod def list(): basePath = os.path.dirname(__file__) diff --git a/bin/ffx/test/scenario_1.py b/bin/ffx/test/scenario_1.py index c24b7bf..9475a1d 100644 --- a/bin/ffx/test/scenario_1.py +++ b/bin/ffx/test/scenario_1.py @@ -24,23 +24,27 @@ class Scenario1(Scenario): def run(self): - click.echo(f"Running scenario 1") + self._logger.info(f"Running {self.__class__.__name__}") kwargs = {} + kwargs[TrackDescriptor.CONTEXT_KEY] = self._context kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.VIDEO kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = set([TrackDisposition.DEFAULT]) trackDescriptor1 = TrackDescriptor(**kwargs) kwargs = {} + kwargs[TrackDescriptor.CONTEXT_KEY] = self._context kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.AUDIO kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = set([TrackDisposition.DEFAULT]) trackDescriptor2 = TrackDescriptor(**kwargs) kwargs = {} + kwargs[TrackDescriptor.CONTEXT_KEY] = self._context kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.AUDIO trackDescriptor3 = TrackDescriptor(**kwargs) kwargs = {} + kwargs[MediaDescriptor.CONTEXT_KEY] = self._context kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = [trackDescriptor1, trackDescriptor2, trackDescriptor3] @@ -58,11 +62,14 @@ class Scenario1(Scenario): 'convert', mediaFilePath] - click.echo(f"Scenarion 1 test sequence: {commandSequence}") + self._logger.debug(f"Scenario1.run(): test sequence: {commandSequence}") out, err, rc = executeProcess(commandSequence, directory = self._testDirectory) - click.echo(f"process output: {out}") + if out: + self._logger.debug(f"Scenario1.run(): process output: {out}") + if rc: + self._logger.error(f"Scenario1.run(): process resultet in error {rc}: {err}") # Phase 4: Evaluate results @@ -87,6 +94,9 @@ class Scenario1(Scenario): assert resultMediaTracks[2].getType() == TrackType.AUDIO, f"Stream #2 is not of type audio" assert resultMediaTracks[2].getDispositionFlag(TrackDisposition.DEFAULT), f"Stream #1 has not set default disposition" + self._reportLogger.info('Scenario 1 test passed') + except AssertionError as ae: - click.echo(f"Scenario 1 test failed ({ae})") + # click.echo(f"Scenario 1 test failed ({ae})") + self._reportLogger.error(f"Scenario 1 test failed ({ae})") diff --git a/bin/ffx/track_descriptor.py b/bin/ffx/track_descriptor.py index 4d83f12..6d4e6b4 100644 --- a/bin/ffx/track_descriptor.py +++ b/bin/ffx/track_descriptor.py @@ -1,4 +1,4 @@ -import click +import logging from .iso_language import IsoLanguage from .track_type import TrackType @@ -10,6 +10,8 @@ from .helper import dictDiff, setDiff class TrackDescriptor: + CONTEXT_KEY = "context" + ID_KEY = "id" INDEX_KEY = "index" SOURCE_INDEX_KEY = "source_index" @@ -34,6 +36,17 @@ class TrackDescriptor: def __init__(self, **kwargs): + if TrackDescriptor.CONTEXT_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.CONTEXT_KEY]) is not dict: + raise TypeError( + f"TrackDescriptor.__init__(): Argument {TrackDescriptor.CONTEXT_KEY} is required to be of type dict" + ) + self.__context = kwargs[TrackDescriptor.CONTEXT_KEY] + self.__logger = self.__context['logger'] + else: + self.__context = {} + self.__logger = logging.getLogger('FFX').addHandler(logging.NullHandler()) + if TrackDescriptor.ID_KEY in kwargs.keys(): if type(kwargs[TrackDescriptor.ID_KEY]) is not int: raise TypeError( diff --git a/bin/ffx/track_details_screen.py b/bin/ffx/track_details_screen.py index 5679f93..c87c7ab 100644 --- a/bin/ffx/track_details_screen.py +++ b/bin/ffx/track_details_screen.py @@ -268,6 +268,8 @@ class TrackDetailsScreen(Screen): kwargs = {} + kwargs[TrackDescriptor.CONTEXT_KEY] = self.context + kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__pattern.getId()) kwargs[TrackDescriptor.INDEX_KEY] = self.__index diff --git a/bin/ffx_tests.py b/bin/ffx_tests.py index 77a52f4..0ab0208 100755 --- a/bin/ffx_tests.py +++ b/bin/ffx_tests.py @@ -1,65 +1,74 @@ #! /usr/bin/python3 -import os, sys, subprocess, json, click, time, re, tempfile, math -from datetime import datetime, timedelta +import os, logging, click from ffx.file_properties import FileProperties - -from ffx.ffx_app import FfxApp from ffx.ffx_controller import FfxController -from ffx.show_controller import ShowController -from ffx.tmdb_controller import TmdbController from ffx.database import databaseContext -from ffx.track_descriptor import TrackDescriptor -from ffx.track_type import TrackType -from ffx.video_encoder import VideoEncoder -from ffx.track_disposition import TrackDisposition - -from ffx.process import executeProcess - from ffx.test.helper import createMediaTestFile from ffx.test.scenario import Scenario -VERSION='0.1.0' - -# 0.1.1 -# Bugfixes, TMBD identify shows -# 0.1.2 -# Bugfixes -# 0.1.3 -# Subtitle file imports - @click.group() @click.pass_context -def ffx(ctx): +@click.option('-v', '--verbose', type=int, default=0, help='Set verbosity of output') +@click.option("--dry-run", is_flag=True, default=False) +def ffx(ctx, verbose, dry_run): """FFX""" ctx.obj = {} ctx.obj['database'] = databaseContext(databasePath=None) - - -# Define a subcommand -@ffx.command() -def version(): - click.echo(VERSION) - - -# Another subcommand -@ffx.command() -def help(): - click.echo(f"ffx tests {VERSION}\n") - click.echo(f"Usage: ffx_test ...") - - -# @ffx.command() -# def show(): -# for i in Scenario().list(): -# click.echo(i) - + ctx.obj['dry_run'] = dry_run + + ctx.obj['verbosity'] = verbose + + # Critical 50 + # Error 40 + # Warning 30 + # Info 20 + # Debug 10 + fileLogVerbosity = max(40 - verbose * 10, 10) + consoleLogVerbosity = max(20 - verbose * 10, 10) + + homeDir = os.path.expanduser("~") + ffxLogDir = os.path.join(homeDir, '.local', 'var', 'log') + if not os.path.exists(ffxLogDir): + os.makedirs(ffxLogDir) + ffxLogFilePath = os.path.join(ffxLogDir, 'ffx.tests.log') + + ctx.obj['logger'] = logging.getLogger('FFX Tests') + ctx.obj['logger'].setLevel(logging.DEBUG) + + ctx.obj['report_logger'] = logging.getLogger('FFX Test Result') + ctx.obj['report_logger'].setLevel(logging.INFO) + + ffxFileHandler = logging.FileHandler(ffxLogFilePath) + ffxFileHandler.setLevel(fileLogVerbosity) + ffxConsoleHandler = logging.StreamHandler() + ffxConsoleHandler.setLevel(consoleLogVerbosity) + + os.unlink('ffx_test_report.log') + ffxTestReportFileHandler = logging.FileHandler('ffx_test_report.log') + + fileFormatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ffxFileHandler.setFormatter(fileFormatter) + consoleFormatter = logging.Formatter( + '%(message)s') + ffxConsoleHandler.setFormatter(consoleFormatter) + reportFormatter = logging.Formatter( + '%(message)s') + ffxTestReportFileHandler.setFormatter(reportFormatter) + + ctx.obj['logger'].addHandler(ffxConsoleHandler) + ctx.obj['logger'].addHandler(ffxFileHandler) + + ctx.obj['report_logger'].addHandler(ffxConsoleHandler) + ctx.obj['report_logger'].addHandler(ffxTestReportFileHandler) + # Another subcommand @ffx.command() @@ -67,11 +76,13 @@ def help(): def run(ctx): """Run ffx test sequences""" - for scenarioIdentifier in Scenario().list(): + ctx.obj['logger'].info('Starting FFX test runs') + + for scenarioIdentifier in Scenario.list(): scenario = Scenario.getClassReference(scenarioIdentifier)(ctx.obj) - click.echo(f"Running scenario {scenarioIdentifier}") + ctx.obj['logger'].info(f"Running scenario {scenarioIdentifier}") scenario.run() diff --git a/bin/logg.py b/bin/logg.py new file mode 100755 index 0000000..c9a84a7 --- /dev/null +++ b/bin/logg.py @@ -0,0 +1,52 @@ +#! /usr/bin/python3 + +import logging + +logger = logging.getLogger('FFX') +logger.setLevel(logging.DEBUG) + +testLogger = logging.getLogger('FFX Test') +testLogger.setLevel(logging.DEBUG) + + +# create file handler that logs debug and higher level messages +ffxFileHandler = logging.FileHandler('ffx.log') +ffxFileHandler.setLevel(logging.DEBUG) + +# create file handler that logs debug and higher level messages +ffxTestReportFileHandler = logging.FileHandler('ffx_test_report.log') +ffxTestReportFileHandler.setLevel(logging.DEBUG) + +# create console handler with a higher log level +ffxConsoleHandler = logging.StreamHandler() +#ffxConsoleHandler.setLevel(logging.ERROR) + +# create formatter and add it to the handlers +formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ffxConsoleHandler.setFormatter(formatter) +ffxFileHandler.setFormatter(formatter) + + +# add the handlers to logger +testLogger.addHandler(ffxConsoleHandler) + + +logger.addHandler(ffxConsoleHandler) +logger.addHandler(ffxFileHandler) + + + +logger.debug('debug message') +logger.info('info message') +logger.warning('warn message') +logger.error('error message') +logger.critical('critical message') + + +testLogger.info('TEST: info message') + + + +click / consoleLogger +