Automated music library format conversion with cuesheet detection, tagging support and configurable regex to obtain tags from filenames. Configuration with ini-files to support multiple locations with multiple quality requirements.
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

# -*- 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]")