From 3486ef1f8ee5cacf9959d05f6e982fb3acd8885f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Constantin=20F=C3=BCrst?= Date: Mon, 29 Aug 2022 23:20:00 +0200 Subject: [PATCH] remove gitmodules to make the project smaller and less complex --- .gitmodules | 6 - FFcuesplitter | 1 - ffcuesplitter | 1 - ffcuesplitter/cuesplitter.py | 353 +++++++++++++++++++++++++++++++++++ ffcuesplitter/exceptions.py | 41 ++++ ffcuesplitter/ffmpeg.py | 230 +++++++++++++++++++++++ ffcuesplitter/ffprobe.py | 91 +++++++++ ffcuesplitter/str_utils.py | 64 +++++++ ffcuesplitter/utils.py | 86 +++++++++ 9 files changed, 865 insertions(+), 8 deletions(-) delete mode 100644 .gitmodules delete mode 160000 FFcuesplitter delete mode 120000 ffcuesplitter create mode 100644 ffcuesplitter/cuesplitter.py create mode 100644 ffcuesplitter/exceptions.py create mode 100644 ffcuesplitter/ffmpeg.py create mode 100644 ffcuesplitter/ffprobe.py create mode 100644 ffcuesplitter/str_utils.py create mode 100644 ffcuesplitter/utils.py diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e87d400..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "FFcuesplitter"] - path = FFcuesplitter - url = https://github.com/jeanslack/FFcuesplitter.git -[submodule "deflacue"] - path = deflacue - url = https://github.com/idlesign/deflacue.git diff --git a/FFcuesplitter b/FFcuesplitter deleted file mode 160000 index 6a92928..0000000 --- a/FFcuesplitter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6a92928778b523e5e35be057232ef6f385155802 diff --git a/ffcuesplitter b/ffcuesplitter deleted file mode 120000 index afc4621..0000000 --- a/ffcuesplitter +++ /dev/null @@ -1 +0,0 @@ -FFcuesplitter/ffcuesplitter \ No newline at end of file diff --git a/ffcuesplitter/cuesplitter.py b/ffcuesplitter/cuesplitter.py new file mode 100644 index 0000000..9dc7dcd --- /dev/null +++ b/ffcuesplitter/cuesplitter.py @@ -0,0 +1,353 @@ +""" +First release: January 16 2022 + +Name: cuesplitter.py +Porpose: FFmpeg based audio splitter for Cue sheet files +Platform: MacOs, Gnu/Linux, FreeBSD +Writer: jeanslack +license: GPL3 +Rev: February 06 2022 +Code checker: flake8 and pylint +#################################################################### + +This file is part of FFcuesplitter. + + FFcuesplitter is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FFcuesplitter is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FFcuesplitter. If not, see . +""" +import os +import shutil +import tempfile +import chardet +from deflacue.deflacue import CueParser +from ffcuesplitter.str_utils import msgdebug, msg +from ffcuesplitter.exceptions import (InvalidFileError, + FFCueSplitterError, + ) +from ffcuesplitter.ffprobe import ffprobe +from ffcuesplitter.ffmpeg import FFMpeg + + +class FFCueSplitter(FFMpeg): + """ + This class implements an interface for retrieve the required + data to accurately split audio CD images using FFmpeg. + + Usage: + >>> from ffcuesplitter.cuesplitter import FFCueSplitter + + Splittings: + >>> split = FFCueSplitter('/home/user/my_file.cue') + >>> split.open_cuefile() + >>> split.do_operations() + + Get data tracks: + >>> data = FFCueSplitter('/home/user/other.cue', dry=True) + >>> data.open_cuefile() + >>> data.audiotracks # trackdata + >>> data.cue.meta.data # cd_info + >>> data.ffmpeg_arguments() + + Only processing the track three: + >>> myfile = FFCueSplitter('/home/user/my_file.cue', + progress_meter='tqdm') + >>> f.open_cuefile() + >>> f.kwargs['tempdir'] = '/tmp/mytempdir' + >>> f.ffmpeg_arguments() + >>> f.processing(myfile.arguments[2], myfile.seconds[2]) + >>> f.move_files_to_outputdir() + + For a full meaning of the arguments to pass to the instance, read + the __init__ docstring of this class. + + """ + def __init__(self, + filename, + outputdir=str('.'), + suffix=str('flac'), + overwrite=str('ask'), + ffmpeg_cmd=str('ffmpeg'), + ffmpeg_loglevel=str('info'), + ffprobe_cmd=str('ffprobe'), + ffmpeg_add_params=str(''), + progress_meter=str('standard'), + dry=bool(False) + ): + """ + ------------------ + Arguments meaning: + ------------------ + + filename: + absolute or relative CUE sheet file + outputdir: + absolute or relative pathname to output files + suffix: + output format, one of ("wav", "flac", "mp3", "ogg") . + overwrite: + overwriting options, one of "ask", "never", "always". + ffmpeg_cmd: + an absolute path command of ffmpeg + ffmpeg_loglevel: + one of "error", "warning", "info", "verbose", "debug" . + ffprobe_cmd: + an absolute path command of ffprobe. + ffmpeg_add_params: + additionals parameters of FFmpeg. + progress_meter: + one of 'tqdm', 'standard', default is 'standard'. + dry: + with `True`, perform the dry run with no changes + done to filesystem. + """ + super().__init__() + + self.kwargs = {'filename': os.path.abspath(filename)} + self.kwargs['dirname'] = os.path.dirname(self.kwargs['filename']) + if outputdir == '.': + self.kwargs['outputdir'] = self.kwargs['dirname'] + else: + self.kwargs['outputdir'] = os.path.abspath(outputdir) + self.kwargs['format'] = suffix + self.kwargs['overwrite'] = overwrite + self.kwargs['ffmpeg_cmd'] = ffmpeg_cmd + self.kwargs['ffmpeg_loglevel'] = ffmpeg_loglevel + self.kwargs['ffprobe_cmd'] = ffprobe_cmd + self.kwargs['ffmpeg_add_params'] = ffmpeg_add_params + self.kwargs['progress_meter'] = progress_meter + self.kwargs['dry'] = dry + self.kwargs['logtofile'] = os.path.join(self.kwargs['dirname'], + 'ffcuesplitter.log') + self.kwargs['tempdir'] = '.' + self.audiotracks = None + self.probedata = [] + self.cue_encoding = None # data chardet + self.cue = None + self.testpatch = None # set for test cases only + # ----------------------------------------------------------------# + + def move_files_to_outputdir(self): + """ + All files are processed in a /temp folder. After the split + operation is complete, all tracks are moved from /temp folder + to output folder. Here evaluates what to do if files already + exists on output folder. + + Raises: + FFCueSplitterError + Returns: + None + + """ + outputdir = self.kwargs['outputdir'] + overwr = self.kwargs['overwrite'] + + for track in os.listdir(self.kwargs['tempdir']): + if os.path.exists(os.path.join(outputdir, track)): + if overwr in ('n', 'N', 'y', 'Y', 'ask'): + while True: + msgdebug(warn=f"File already exists: " + f"'{os.path.join(outputdir, track)}'") + overwr = input("Overwrite [Y/n/always/never]? > ") + if overwr in ('Y', 'y', 'n', 'N', 'always', 'never'): + break + msgdebug(err=f"Invalid option '{overwr}'") + continue + if overwr == 'never': + msgdebug(info=("Do not overwrite any files because " + "you specified 'never' option")) + return None + + if overwr in ('y', 'Y', 'always', 'never', 'ask'): + if overwr == 'always': + msgdebug(info=("Overwrite existing file because " + "you specified the 'always' option")) + try: + shutil.move(os.path.join(self.kwargs['tempdir'], track), + os.path.join(outputdir, track)) + + except Exception as error: + raise FFCueSplitterError(error) from error + + return None + # ----------------------------------------------------------------# + + def do_operations(self): + """ + Automates the work in a temporary context using tempfile. + + Raises: + FFCueSplitterError + Returns: + None + """ + with tempfile.TemporaryDirectory(suffix=None, + prefix='ffcuesplitter_', + dir=None) as tmpdir: + self.kwargs['tempdir'] = tmpdir + self.ffmpeg_arguments() + + msgdebug(info=(f"Temporary Target: '{self.kwargs['tempdir']}'")) + count = 0 + msgdebug(info="Extracting audio tracks (type Ctrl+c to stop):") + + for args, secs, title in zip(self.arguments, + self.seconds, + self.audiotracks): + count += 1 + msg(f'\nTRACK {count}/{len(self.audiotracks)} ' + f'>> "{title["TITLE"]}.{self.outsuffix}" ...') + self.processing(args, secs) + + if self.kwargs['dry'] is True: + return + + msg('\n') + msgdebug(info="...done exctracting") + msgdebug(info="Move files to: ", + tail=(f"\033[34m" + f"'{os.path.abspath(self.kwargs['outputdir'])}'" + f"\033[0m")) + try: + os.makedirs(self.kwargs['outputdir'], + mode=0o777, exist_ok=True) + except Exception as error: + raise FFCueSplitterError(error) from error + + self.move_files_to_outputdir() + # ----------------------------------------------------------------# + + def get_track_duration(self, tracks): + """ + Gets total duration of the source audio tracks for chunks + calculation on the progress meter during ffmpeg executions. + Given a total duration calculates the remains duration + for the last track as well. + + This method is called by `cuefile_parser` method, Do not + call this method directly. + + Raises: + FFCueSplitterError + Returns: + tracks (list), all track data taken from the cue file. + """ + if self.testpatch: + probe = {'format': {'duration': 6.000000}} + else: + filename = tracks[0].get('FILE') + cmd = self.kwargs['ffprobe_cmd'] + kwargs = {'loglevel': 'error', 'hide_banner': None} + probe = ffprobe(filename, cmd=cmd, **kwargs) + self.probedata.append(probe) + + time = [] + for idx in enumerate(tracks): + if idx[0] != len(tracks) - 1: # minus last + trk = (tracks[idx[0] + 1]['START'] - + tracks[idx[0]]['START']) / (44100) + time.append(trk) + + if not time: + last = (float(probe['format']['duration']) - + tracks[0]['START'] / 44100) + else: + last = float(probe['format']['duration']) - sum(time) + time.append(last) + for keydur, remain in zip(tracks, time): + keydur['DURATION'] = remain + + return tracks + # ----------------------------------------------------------------# + + def deflacue_object_handler(self): + """ + Handles `deflacue.CueParser` data. + Raises: + FFCueSplitterError: if no source audio file found + Returns: + 'audiotracks' list object + """ + self.audiotracks = [] + cd_info = self.cue.meta.data + + def sanitize(val: str) -> str: + return val.replace('/', '').replace('\\','').replace('"', '') + + tracks = self.cue.tracks + sourcenames = {k: [] for k in [str(x.file.path) for x in tracks]} + + for track in enumerate(tracks): + track_file = track[1].file.path + + if not track_file.exists(): + msgdebug(warn=(f'Source file `{track_file}` is not ' + f'found. Track is skipped.')) + + if str(track_file) in sourcenames: + sourcenames.pop(str(track_file)) + if not sourcenames: + raise FFCueSplitterError('No audio source files ' + 'found!') + continue + + filename = (f"{sanitize(track[1].title)}") + + data = {'FILE': str(track_file), **cd_info, **track[1].data} + data['TITLE'] = filename + data['START'] = track[1].start + + if track[1].end != 0: + data['END'] = track[1].end + + if f"{data['FILE']}" in sourcenames.keys(): + sourcenames[f'{data["FILE"]}'].append(data) + + for val in sourcenames.values(): + self.audiotracks += self.get_track_duration(val) + + return self.audiotracks + # ----------------------------------------------------------------# + + def check_cuefile(self): + """ + Cue file check + """ + filesuffix = os.path.splitext(self.kwargs['filename'])[1] + isfile = os.path.isfile(self.kwargs['filename']) + + if not isfile or filesuffix not in ('.cue', '.CUE'): + raise InvalidFileError(f"Invalid CUE sheet file: " + f"'{self.kwargs['filename']}'") + # ----------------------------------------------------------------# + + def open_cuefile(self, testpatch=None): + """ + Read cue file and start file parsing via deflacue package + """ + if testpatch: + self.testpatch = True + + self.check_cuefile() + curdir = os.getcwd() + os.chdir(self.kwargs['dirname']) + + with open(self.kwargs['filename'], 'rb') as file: + cuebyte = file.read() + self.cue_encoding = chardet.detect(cuebyte) + + parser = CueParser.from_file(self.kwargs['filename'], + encoding=self.cue_encoding['encoding']) + self.cue = parser.run() + self.deflacue_object_handler() + os.chdir(curdir) diff --git a/ffcuesplitter/exceptions.py b/ffcuesplitter/exceptions.py new file mode 100644 index 0000000..4b8079d --- /dev/null +++ b/ffcuesplitter/exceptions.py @@ -0,0 +1,41 @@ +""" +Name: exceptions.py +Porpose: defines class Exceptions for ffcuesplitter +Platform: MacOs, Gnu/Linux, FreeBSD +Writer: jeanslack +license: GPL3 +Rev: February 03 2022 +Code checker: flake8 and pylint +#################################################################### + +This file is part of FFcuesplitter. + + FFcuesplitter is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FFcuesplitter is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FFcuesplitter. If not, see . +""" + + +class FFMpegError(Exception): + """Excepion raised by FFMpeg class""" + + +class FFProbeError(Exception): + """Excepion raised by FFProbe class""" + + +class InvalidFileError(Exception): + """Exception type raised when CUE file is invalid.""" + + +class FFCueSplitterError(Exception): + """Exception raised in all other cases.""" diff --git a/ffcuesplitter/ffmpeg.py b/ffcuesplitter/ffmpeg.py new file mode 100644 index 0000000..26b4c1d --- /dev/null +++ b/ffcuesplitter/ffmpeg.py @@ -0,0 +1,230 @@ +# -*- coding: UTF-8 -*- +""" +Name: ffmpeg.py +Porpose: builds arguments for FFmpeg processing. +Compatibility: Python3 +Platform: all platforms +Author: Gianluca Pernigotto +Copyright: (c) 2022/2023 Gianluca Pernigotto +license: GPL3 +Rev: February 06 2022 +Code checker: flake8, pylint +######################################################## + +This file is part of FFcuesplitter. + + FFcuesplitter is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FFcuesplitter is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FFcuesplitter. If not, see . +""" +import subprocess +import os +import sys +import platform +from tqdm import tqdm +from ffcuesplitter.exceptions import FFMpegError +from ffcuesplitter.str_utils import msg +from ffcuesplitter.utils import Popen + +if not platform.system() == 'Windows': + import shlex + + +class FFMpeg: + """ + FFMpeg is a parent base class interface for FFCueSplitter. + It represents FFmpeg command and arguments with their + sub-processing. + """ + DATACODECS = {'wav': 'pcm_s16le -ar 44100', + 'flac': 'flac -ar 44100', + 'ogg': 'libvorbis -ar 44100', + 'mp3': 'libmp3lame -ar 44100', + } + + def __init__(self, **kwargs): + """ + Constructor + """ + self.kwargs = kwargs + self.audiotracks = kwargs + self.seconds = None + self.arguments = None + self.osplat = platform.system() + self.outsuffix = None + # -------------------------------------------------------------# + + def codec_setup(self, sourcef): + """ + Returns codec arg based on given format + """ + if self.kwargs['format'] == 'copy': + self.outsuffix = os.path.splitext(sourcef)[1].replace('.', '') + codec = '-c copy' + + else: + self.outsuffix = self.kwargs['format'] + codec = f'-c:a {FFMpeg.DATACODECS[self.kwargs["format"]]}' + + return codec, self.outsuffix + # -------------------------------------------------------------# + + def ffmpeg_arguments(self): + """ + Builds `FFmpeg` arguments and calculates time seconds + for each audio track. + + Returns: + dict(arguments:[...], seconds:[...]) + """ + self.arguments = [] + self.seconds = [] + + meters = {'tqdm': '-progress pipe:1 -nostats -nostdin', 'standard': ''} + + for track in self.audiotracks: + codec, suffix = self.codec_setup(track["FILE"]) + metadata = {'ARTIST': track.get('PERFORMER', ''), + 'ALBUM': track.get('ALBUM', ''), + 'TITLE': track.get('TITLE', ''), + 'TRACK': (str(track['TRACK_NUM']) + '/' + + str(len(self.audiotracks))), + 'DISCNUMBER': track.get('DISCNUMBER', ''), + 'GENRE': track.get('GENRE', ''), + 'DATE': track.get('DATE', ''), + 'COMMENT': track.get('COMMENT', ''), + 'DISCID': track.get('DISCID', ''), + } + cmd = f'"{self.kwargs["ffmpeg_cmd"]}" ' + cmd += f' -loglevel {self.kwargs["ffmpeg_loglevel"]}' + cmd += f" {meters[self.kwargs['progress_meter']]}" + fpath = os.path.join(self.kwargs["dirname"], track["FILE"]) + cmd += f' -i "{fpath}"' + cmd += f" -ss {round(track['START'] / 44100, 6)}" # ff to secs + if 'END' in track: + cmd += f" -to {round(track['END'] / 44100, 6)}" # ff to secs + for key, val in metadata.items(): + cmd += f' -metadata {key}="{val}"' + cmd += f' {codec}' + cmd += f" {self.kwargs['ffmpeg_add_params']}" + cmd += ' -y' + num = str(track['TRACK_NUM']).rjust(2, '0') + name = f'{num} - {track["TITLE"]}.{suffix}' + cmd += f' "{os.path.join(self.kwargs["tempdir"], name)}"' + self.arguments.append(cmd) + self.seconds.append(track['DURATION']) + + return {'arguments': self.arguments, 'seconds': self.seconds} + # --------------------------------------------------------------# + + def processing(self, arg, secs): + """ + Redirect to required processing + """ + if self.kwargs['progress_meter'] == 'tqdm': + cmd = arg if self.osplat == 'Windows' else shlex.split(arg) + if self.kwargs['dry'] is True: + msg(cmd) # stdout cmd in dry mode + return + self.processing_with_tqdm_progress(cmd, secs) + + elif self.kwargs['progress_meter'] == 'standard': + cmd = arg if self.osplat == 'Windows' else shlex.split(arg) + if self.kwargs['dry'] is True: + msg(cmd) # stdout cmd in dry mode + return + self.processing_with_standard_progress(cmd) + # --------------------------------------------------------------# + + def processing_with_tqdm_progress(self, cmd, seconds): + """ + FFmpeg sub-processing showing a tqdm progress meter + for each loop. Also writes a log file to the same + destination folder as the .cue file . + + Usage for get seconds elapsed: + progbar = tqdm(total=round(seconds), unit="s", dynamic_ncols=True) + progbar.clear() + previous_s = 0 + + s_processed = round(int(output.split('=')[1]) / 1_000_000) + s_increase = s_processed - previous_s + progbar.update(s_increase) + previous_s = s_processed + + Raises: + FFMpegError + Returns: + None + """ + progbar = tqdm(total=100, + unit="s", + dynamic_ncols=True + ) + progbar.clear() + + try: + with open(self.kwargs['logtofile'], "w", encoding='utf-8') as log: + log.write(f'\nCOMMAND: {cmd}') + with Popen(cmd, + stdout=subprocess.PIPE, + stderr=log, + bufsize=1, + universal_newlines=True) as proc: + + for output in proc.stdout: + if "out_time_ms" in output.strip(): + s_processed = int(output.split('=')[1]) / 1_000_000 + percent = s_processed / seconds * 100 + progbar.update(round(percent) - progbar.n) + + if proc.wait(): # error + progbar.close() + raise FFMpegError(f"ffmpeg FAILED: See log details: " + f"'{self.kwargs['logtofile']}'" + f"\nExit status: {proc.wait()}") + + except (OSError, FileNotFoundError) as excepterr: + progbar.close() + raise FFMpegError(excepterr) from excepterr + + except KeyboardInterrupt: + # proc.kill() + progbar.close() + proc.terminate() + sys.exit("\n[KeyboardInterrupt] FFmpeg process terminated.") + + progbar.close() + # --------------------------------------------------------------# + + def processing_with_standard_progress(self, cmd): + """ + FFmpeg sub-processing with stderr output to console. + The output depending on the ffmpeg loglevel option. + Raises: + FFMpegError + Returns: + None + """ + with open(self.kwargs['logtofile'], "w", encoding='utf-8') as log: + log.write(f'COMMAND: {cmd}') + try: + subprocess.run(cmd, check=True, shell=False) + + except FileNotFoundError as err: + raise FFMpegError(f"{err}") from err + + except subprocess.CalledProcessError as err: + raise FFMpegError(f"ffmpeg FAILED: {err}") from err + + except KeyboardInterrupt: + sys.exit("\n[KeyboardInterrupt]") diff --git a/ffcuesplitter/ffprobe.py b/ffcuesplitter/ffprobe.py new file mode 100644 index 0000000..3d91b70 --- /dev/null +++ b/ffcuesplitter/ffprobe.py @@ -0,0 +1,91 @@ +# -*- coding: UTF-8 -*- +""" +Name: ffprobe.py +Porpose: simple cross-platform wrap for ffprobe +Compatibility: Python3 +Platform: all platforms +Author: Gianluca Pernigotto +Copyright: (c) 2022/2023 Gianluca Pernigotto +license: GPL3 +Rev: Feb.17.2022 +Code checker: flake8, pylint +######################################################## + +This file is part of FFcuesplitter. + + FFcuesplitter is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FFcuesplitter is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FFcuesplitter. If not, see . +""" +import subprocess +import shlex +import platform +import json +from ffcuesplitter.exceptions import FFProbeError +from ffcuesplitter.utils import Popen + + +def from_kwargs_to_args(kwargs): + """ + Helper function to build command line + arguments out of dict. + """ + args = [] + for key in sorted(kwargs.keys()): + val = kwargs[key] + args.append(f'-{key}') + if val is not None: + args.append(f'{val}') + return args + + +def ffprobe(filename, cmd='ffprobe', **kwargs): + """ + Run ffprobe on the specified file and return a + JSON representation of the output. + + Raises: + :class:`ffcuesplitter.FFProbeError`: if ffprobe + returns a non-zero exit code; + `ffcuesplitter.FFProbeError` from `OSError`, + `FileNotFoundError` if a generic error. + + Usage: + ffprobe(filename, + cmd='/usr/bin oi/ffprobe', + loglevel='error', + hide_banner=None, + etc, + ) + """ + args = (f'"{cmd}" -show_format -show_streams -of json ' + f'{" ".join(from_kwargs_to_args(kwargs))} ' + f'"{filename}"' + ) + args = shlex.split(args) if platform.system() != 'Windows' else args + + try: + with Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) as proc: + output, error = proc.communicate() + + if proc.returncode != 0: + raise FFProbeError(f'ffprobe: {error}') + + except (OSError, FileNotFoundError) as excepterr: + raise FFProbeError(excepterr) from excepterr + + else: + return json.loads(output) diff --git a/ffcuesplitter/str_utils.py b/ffcuesplitter/str_utils.py new file mode 100644 index 0000000..759b816 --- /dev/null +++ b/ffcuesplitter/str_utils.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +Name: str_utils.py (module) +Porpose: module for cosmetic output console in ANSI sequences +Writer: Gianluca Pernigoto +Copyright: (c) 2022 Gianluca Pernigoto +license: GPL3 +Rev: Jan 10 2022 +Code checker: flake8, pylint +""" + + +def msgdebug(head='', info=None, warn=None, err=None, tail=''): + """ + Print debug messages: + ``head`` can be used for additionals custom string to use + at the beginning of the string. + ``tail`` can be used for additionals custom string to use + at the end of the string. + ``info`` print in blue color, ``warn`` print in yellow color + ``err`` print in red color. + """ + if info: + print(f"{head}\033[32;1mINFO:\033[0m {info}{tail}") + elif warn: + print(f"{head}\033[33;1mWARNING:\033[0m {warn}{tail}") + elif err: + print(f"{head}\033[31;1mERROR:\033[0m {err}{tail}") + + +def msgcolor(head='', orange=None, green=None, azure=None, tail=''): + """ + Print information messages by explicitly + choosing the name of the color to be displayed: + ``head`` can be used for additionals custom string to use + at the beginning of the string. + ``tail`` can be used for additionals custom string to use + at the end of the string. + """ + if orange: + print(f"{head}\033[33;1m{orange}\033[0m{tail}") + + elif green: + print(f"{head}\033[32;1m{green}\033[0m{tail}") + + elif azure: + print(f"{head}\033[34;1m{azure}\033[0m{tail}") + + +def msgend(done=None, abort=None): + """ + Print status messages at the end of the tasks + """ + if done: + print("\033[1m..Finished!\033[0m\n") + elif abort: + print("\033[1m..Abort!\033[0m\n") + + +def msg(message): + """ + Print any string messages + """ + print(message) diff --git a/ffcuesplitter/utils.py b/ffcuesplitter/utils.py new file mode 100644 index 0000000..e7598aa --- /dev/null +++ b/ffcuesplitter/utils.py @@ -0,0 +1,86 @@ +""" +Name: utils.py +Porpose: utils used by FFcuesplitter +Platform: MacOs, Gnu/Linux, FreeBSD +Writer: jeanslack +license: GPL3 +Rev: January 16 2022 +Code checker: flake8 and pylint +#################################################################### + +This file is part of FFcuesplitter. + + FFcuesplitter is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FFcuesplitter is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FFcuesplitter. If not, see . +""" +import subprocess +import platform +import datetime + + +def pairwise(iterable): + """ + Return a zip object from iterable. + This function is used by run method. + ---- + USE: + + after splitting ffmpeg's progress strings such as: + output = "frame= 1178 fps=155 q=29.0 size= 2072kB time=00:00:39.02 + bitrate= 435.0kbits/s speed=5.15x " + in a list as: + iterable = ['frame', '1178', 'fps', '155', 'q', '29.0', 'size', '2072kB', + 'time', '00:00:39.02', 'bitrate', '435.0kbits/s', speed', + '5.15x'] + for x, y in pairwise(iterable): + (x,y) + + + + """ + itobj = iter(iterable) # list_iterator object + return zip(itobj, itobj) # zip object pairs from list iterable object +# ------------------------------------------------------------------------ + + +def frames_to_seconds(frames): + """ + Converts frames (10407600) to seconds (236.0) and then + converts them to a time format string (0:03:56) using datetime. + """ + secs = frames / 44100 + return str(datetime.timedelta(seconds=secs)) +# ------------------------------------------------------------------------ + + +class Popen(subprocess.Popen): + """ + Inherit subprocess.Popen class to set _startupinfo. + This avoids displaying a console window on MS-Windows + using GUI's . + """ + if platform.system() == 'Windows': + _startupinfo = subprocess.STARTUPINFO() + _startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + _startupinfo = None + + def __init__(self, *args, **kwargs): + """Constructor + """ + super().__init__(*args, **kwargs, startupinfo=self._startupinfo) + + # def communicate_or_kill(self, *args, **kwargs): + # return process_communicate_or_kill(self, *args, **kwargs) +# ------------------------------------------------------------------------