diff --git a/bin/ffx.py b/bin/ffx.py index 88fc807..f20f89b 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -15,8 +15,7 @@ DEFAULT_QUALITY = 23 DEFAULT_AV1_PRESET = 5 -DEFAULT_LABEL='output' -DEFAULT_FILE_SUFFIX = 'webm' +DEFAULT_FILE_EXTENSION = 'webm' DEFAULT_STEREO_BANDWIDTH = "128" DEFAULT_AC3_BANDWIDTH = "256" @@ -50,9 +49,8 @@ STREAM_LAYOUT_5_1 = '5.1(side)' STREAM_LAYOUT_STEREO = 'stereo' STREAM_LAYOUT_6CH = '6ch' -SEASON_EPISODE_INDICATOR_MATCH = '([sS][0-9]+)([eE][0-9]+)' -SEASON_INDICATOR_MATCH = '([sS][0-9]+)' -EPISODE_INDICATOR_MATCH = '([eE][0-9]+)' +SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)' +EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)' class DashboardScreen(Screen): @@ -129,7 +127,7 @@ def executeProcess(commandSequence): output, error = process.communicate() - return output + return output.decode('utf-8'), error.decode('utf-8') @@ -142,10 +140,13 @@ def executeProcess(commandSequence): def getStreamDescriptor(filename): - ffprobeOutput = executeProcess(["ffprobe", - "-show_streams", - "-of", "json", - filename]) + ffprobeOutput, ffprobeError = executeProcess(["ffprobe", + "-show_streams", + "-of", "json", + filename]) + + if 'Invalid data found when processing input' in ffprobeError: + return None streamData = json.loads(ffprobeOutput)['streams'] @@ -249,16 +250,16 @@ def generateOutputTokens(f, suffix, q=None): # if presetTokens: # preset = int(presetTokens[0].split('=')[1]) -# cropStart = '' -# cropLength = '' +# ctx.obj['crop_start'] = '' +# ctx.obj['crop_length'] = '' # cropTokens = [c for c in sys.argv if c.startswith('crop')] # if cropTokens: # if '=' in cropTokens[0]: # cropString = cropTokens[0].split('=')[1] -# cropStart, cropLength = cropString.split(',') +# ctx.obj['crop_start'], ctx.obj['crop_length'] = cropString.split(',') # else: -# cropStart = 60 -# cropLength = 180 +# ctx.obj['crop_start'] = 60 +# ctx.obj['crop_length'] = 180 # # denoiseTokens = [d for d in sys.argv if d.startswith('denoise')] # @@ -336,7 +337,10 @@ def help(): @click.argument('filename', nargs=1) @ffx.command() def streams(filename): - for d in getStreamDescriptor(filename): + sd = getStreamDescriptor(filename) + if sd is None: + raise click.ClickException('This file does not contain any audiovisual data') + for d in sd: click.echo(f"{d['codec']}{' (' + str(d['channels']) + ')' if d['type'] == 'audio' else ''}") @@ -345,7 +349,7 @@ def streams(filename): @click.pass_context @click.argument('paths', nargs=-1) -@click.option('-l', '--label', type=str, default=DEFAULT_LABEL, help='Label to be used as filename prefix') +@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') @click.option('-v', '--video-encoder', type=str, default=DEFAULT_VIDEO_ENCODER, help='Target video encoder (vp9 or av1) default: vp9') @@ -418,9 +422,9 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a click.echo(f"\nRunning {len(paths) * len(q_list)} jobs") - + job_index = 0 + se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH) - s_match = re.compile(SEASON_INDICATOR_MATCH) e_match = re.compile(EPISODE_INDICATOR_MATCH) @@ -442,97 +446,146 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a sourceFileBasename = sourceFilename sourceFilenameExtension = '' + #click.echo(f"dir={sourceDirectory} base={sourceFileBasename} ext={sourceFilenameExtension}") click.echo(f"\nProcessing file {sourcePath}") + season_digits = 2 + episode_digits = 2 + index_digits = 3 se_result = se_match.search(sourceFilename) - s_result = s_match.search(sourceFilename) e_result = e_match.search(sourceFilename) + season = -1 + episode = -1 + file_index = 0 + + if se_result is not None: + season = int(se_result.group(1)) + episode = int(se_result.group(2)) + elif e_result is not None: + episode = int(e_result.group(1)) + else: + file_index += 1 + + print(f"season={season} episode={episode} file={file_index}") + + + targetFilenameTokens = [] - #streamDescriptor = getStreamDescriptor(sourcePath) + targetFilenameExtension = DEFAULT_FILE_EXTENSION - #commandTokens = COMMAND_TOKENS + [sourcePath] + if label: + targetFilenameTokens = [label] - #for q in q_list: + if season > -1 and episode > -1: + targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"] + elif episode > -1: + targetFilenameTokens += [f"E{episode:0{episode_digits}d}"] + else: + targetFilenameTokens += [f"{file_index:0{index_digits}d}"] - #click.echo(f"\nRunning job q={q}") + else: + targetFilenameTokens = [sourceFileBasename] - #mappingVideoTokens = ['-map', 'v:0'] - #mappingTokens = mappingVideoTokens.copy() - #audioTokens = [] + # In case source and target filenames are the same add an extension to distinct output from input + if sourceFilenameExtension == targetFilenameExtension: + targetFilenameTokens += ['ffx'] - #audioIndex = 0 - #for audioStreamDescriptor in streamDescriptor: - - #if audioStreamDescriptor['type'] == STREAM_TYPE_AUDIO: - #mappingTokens += ['-map', f"a:{audioIndex}"] - #audioTokens += generateAudioTokens(ctx.obj, audioIndex, audioStreamDescriptor['layout']) - #audioIndex += 1 + targetFilename = '_'.join(targetFilenameTokens) + '.' + targetFilenameExtension + click.echo(f"target filename: {targetFilename}") - #for s in range(len([d for d in streamDescriptor if d['type'] == STREAM_TYPE_SUBTITLE])): - #mappingTokens += ['-map', f"s:{s}"] + streamDescriptor = getStreamDescriptor(sourcePath) + if streamDescriptor is None: + click.echo(f"File with path {sourcePath} does not contain any audiovisual data, skipping ...") + continue + commandTokens = COMMAND_TOKENS + [sourcePath] - #if video_encoder == 'av1': - #commandSequence = commandTokens + mappingTokens + audioTokens + generateAV1Tokens(q, preset) + audioTokens + for q in q_list: - #if clear_metadata: - #commandSequence += generateClearTokens(streamDescriptor) + click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}") + job_index += 1 - #if performCrop: - #commandSequence += generateCropTokens(cropStart, cropLength) - - #commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_SUFFIX, q) + mappingVideoTokens = ['-map', 'v:0'] + mappingTokens = mappingVideoTokens.copy() + audioTokens = [] - #click.echo(f"Command: {' '.join(commandSequence)}") + audioIndex = 0 + for audioStreamDescriptor in streamDescriptor: + + if audioStreamDescriptor['type'] == STREAM_TYPE_AUDIO: - #executeProcess(commandSequence) + mappingTokens += ['-map', f"a:{audioIndex}"] + audioTokens += generateAudioTokens(ctx.obj, audioIndex, audioStreamDescriptor['layout']) + audioIndex += 1 - #if video_encoder == 'vp9': + for s in range(len([d for d in streamDescriptor if d['type'] == STREAM_TYPE_SUBTITLE])): + mappingTokens += ['-map', f"s:{s}"] - #commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q) - #if performCrop: - # commandSequence1 += generateCropTokens(cropStart, cropLength) - #commandSequence1 += NULL_TOKENS + if video_encoder == 'av1': - #click.echo(f"Command 1: {' '.join(commandSequence1)}") + commandSequence = commandTokens + mappingTokens + audioTokens + generateAV1Tokens(q, preset) + audioTokens - #if os.path.exists(TEMP_FILE_NAME): - # os.remove(TEMP_FILE_NAME) + if clear_metadata: + commandSequence += generateClearTokens(streamDescriptor) - #executeProcess(commandSequence1) + if ctx.obj['perform_crop']: + commandSequence += generateCropTokens(ctx.obj['crop_start'], ctx.obj['crop_length']) + + commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_EXTENSION, q) + click.echo(f"Command: {' '.join(commandSequence)}") - #commandSequence2 = commandTokens + mappingTokens + # executeProcess(commandSequence) - #if denoise: - # commandSequence2 += generateDenoiseTokens() - #commandSequence2 += generateVP9Pass2Tokens(q) + audioTokens + if video_encoder == 'vp9': - #if clear_metadata: - # commandSequence2 += generateClearTokens(streamDescriptor) + commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q) - #if performCrop: - # commandSequence2 += generateCropTokens(cropStart, cropLength) + if ctx.obj['perform_crop']: + commandSequence1 += generateCropTokens(ctx.obj['crop_start'], ctx.obj['crop_length']) - #commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_SUFFIX, q) + commandSequence1 += NULL_TOKENS - #click.echo(f"Command 2: {' '.join(commandSequence2)}") - - #executeProcess(commandSequence2) + click.echo(f"Command 1: {' '.join(commandSequence1)}") + + if os.path.exists(TEMP_FILE_NAME): + os.remove(TEMP_FILE_NAME) + + # executeProcess(commandSequence1) + + + commandSequence2 = commandTokens + mappingTokens + + if denoise: + commandSequence2 += generateDenoiseTokens() + + commandSequence2 += generateVP9Pass2Tokens(q) + audioTokens + + if clear_metadata: + commandSequence2 += generateClearTokens(streamDescriptor) + + if ctx.obj['perform_crop']: + commandSequence2 += generateCropTokens(ctx.obj['crop_start'], ctx.obj['crop_length']) + + commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_EXTENSION, q) + + click.echo(f"Command 2: {' '.join(commandSequence2)}") + + # executeProcess(commandSequence2) #app = ModesApp(ctx.obj)