Constantin Fürst
2 years ago
9 changed files with 865 additions and 8 deletions
-
6.gitmodules
-
1FFcuesplitter
-
1ffcuesplitter
-
353ffcuesplitter/cuesplitter.py
-
41ffcuesplitter/exceptions.py
-
230ffcuesplitter/ffmpeg.py
-
91ffcuesplitter/ffprobe.py
-
64ffcuesplitter/str_utils.py
-
86ffcuesplitter/utils.py
@ -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 |
@ -1 +0,0 @@ |
|||
FFcuesplitter/ffcuesplitter |
@ -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 <jeanlucperni@gmail.com> |
|||
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 <http://www.gnu.org/licenses/>. |
|||
""" |
|||
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) |
@ -0,0 +1,41 @@ |
|||
""" |
|||
Name: exceptions.py |
|||
Porpose: defines class Exceptions for ffcuesplitter |
|||
Platform: MacOs, Gnu/Linux, FreeBSD |
|||
Writer: jeanslack <jeanlucperni@gmail.com> |
|||
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 <http://www.gnu.org/licenses/>. |
|||
""" |
|||
|
|||
|
|||
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.""" |
@ -0,0 +1,230 @@ |
|||
# -*- coding: UTF-8 -*- |
|||
""" |
|||
Name: ffmpeg.py |
|||
Porpose: builds arguments for FFmpeg processing. |
|||
Compatibility: Python3 |
|||
Platform: all platforms |
|||
Author: Gianluca Pernigotto <jeanlucperni@gmail.com> |
|||
Copyright: (c) 2022/2023 Gianluca Pernigotto <jeanlucperni@gmail.com> |
|||
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 <http://www.gnu.org/licenses/>. |
|||
""" |
|||
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]") |
@ -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 <jeanlucperni@gmail.com> |
|||
Copyright: (c) 2022/2023 Gianluca Pernigotto <jeanlucperni@gmail.com> |
|||
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 <http://www.gnu.org/licenses/>. |
|||
""" |
|||
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) |
@ -0,0 +1,64 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Name: str_utils.py (module) |
|||
Porpose: module for cosmetic output console in ANSI sequences |
|||
Writer: Gianluca Pernigoto <jeanlucperni@gmail.com> |
|||
Copyright: (c) 2022 Gianluca Pernigoto <jeanlucperni@gmail.com> |
|||
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) |
@ -0,0 +1,86 @@ |
|||
""" |
|||
Name: utils.py |
|||
Porpose: utils used by FFcuesplitter |
|||
Platform: MacOs, Gnu/Linux, FreeBSD |
|||
Writer: jeanslack <jeanlucperni@gmail.com> |
|||
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 <http://www.gnu.org/licenses/>. |
|||
""" |
|||
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) |
|||
|
|||
<https://stackoverflow.com/questions/5389507/iterating-over-every- |
|||
two-elements-in-a-list> |
|||
|
|||
""" |
|||
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) |
|||
# ------------------------------------------------------------------------ |
Write
Preview
Loading…
Cancel
Save
Reference in new issue