You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
8.3 KiB
230 lines
8.3 KiB
# -*- 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]")
|