dbus-soundrecorder/ffmpeglauncher.cpp

246 lines
9.8 KiB
C++

#include "ffmpeglauncher.h"
#include "playerwatcher.h"
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/inifile.h>
#include <QStringBuilder>
#include <fstream>
#include <iostream>
using namespace std;
using namespace CppUtilities;
namespace DBusSoundRecorder {
inline ostream &operator<<(ostream &stream, const QString &str)
{
stream << str.toLocal8Bit().data();
return stream;
}
inline QString validFileName(const QString &text)
{
QString copy(text);
copy.replace(QChar('\\'), QLatin1String(" - "))
.replace(QChar('/'), QLatin1String(" - "))
.replace(QChar('\n'), QString())
.replace(QChar('\r'), QString())
.replace(QChar('\f'), QString())
.replace(QChar('<'), QString())
.replace(QChar('>'), QString())
.replace(QChar('?'), QString())
.replace(QChar('*'), QString())
.replace(QChar('!'), QString())
.replace(QChar('|'), QString())
.replace(QLatin1String(": "), QLatin1String(" - "))
.replace(QChar(':'), QChar('-'));
return copy;
}
FfmpegLauncher::FfmpegLauncher(PlayerWatcher &watcher, QObject *parent)
: QObject(parent)
, m_watcher(watcher)
, m_sink(QStringLiteral("default"))
, m_inputOptions()
, m_options()
, m_targetDir(QStringLiteral("."))
, m_targetExtension(QStringLiteral(".m4a"))
, m_ffmpeg(new QProcess(this))
{
connect(&watcher, &PlayerWatcher::nextSong, this, &FfmpegLauncher::nextSong);
connect(&watcher, &PlayerWatcher::playbackStopped, this, &FfmpegLauncher::stopFfmpeg);
connect(m_ffmpeg, &QProcess::started, this, &FfmpegLauncher::ffmpegStarted);
connect(m_ffmpeg,
#if QT_VERSION < QT_VERSION_CHECK(5, 6, 0) || (QT_DEPRECATED_SINCE(5, 6) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
static_cast<void (QProcess::*)(QProcess::ProcessError)>(
#endif
&QProcess::error
#if QT_VERSION < QT_VERSION_CHECK(5, 6, 0) || (QT_DEPRECATED_SINCE(5, 6) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
)
#endif
,
this, &FfmpegLauncher::ffmpegError);
connect(m_ffmpeg, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &FfmpegLauncher::ffmpegFinished);
m_ffmpeg->setProgram(QStringLiteral("ffmpeg"));
m_ffmpeg->setProcessChannelMode(QProcess::ForwardedChannels);
}
void addMetaData(QStringList &args, const QString &field, const QString &value)
{
if (!value.isEmpty()) {
args << QStringLiteral("-metadata");
args << QStringLiteral("%1=%2").arg(field, value);
}
}
void FfmpegLauncher::nextSong()
{
// skip ads
if (m_watcher.isAd()) {
return;
}
// pause player until ffmpeg has been started
m_watcher.setSilent(true);
if (m_watcher.isPlaying()) {
m_watcher.pause();
}
// terminate/kill the current process
stopFfmpeg();
// determine output file, create target directory
static const QString miscCategory(QStringLiteral("misc"));
static const QString unknownTitle(QStringLiteral("unknown track"));
const auto targetDirPath = QStringLiteral("%1/%2").arg(m_watcher.artist().isEmpty() ? miscCategory : validFileName(m_watcher.artist()),
m_watcher.artist().isEmpty() ? miscCategory : validFileName(m_watcher.album()));
if (!m_targetDir.mkpath(targetDirPath)) {
cerr << "Error: Can not create target directory: " << targetDirPath << endl;
return;
}
QDir targetDir(m_targetDir);
targetDir.cd(targetDirPath);
// determine track number
QString number, length, year, genre, totalTracks, totalDisks;
if (m_watcher.trackNumber()) {
if (m_watcher.diskNumber()) {
number = QStringLiteral("%2-%1").arg(m_watcher.trackNumber(), 2, 10, QLatin1Char('0')).arg(m_watcher.diskNumber());
} else {
number = QStringLiteral("%1").arg(m_watcher.trackNumber(), 2, 10, QLatin1Char('0'));
}
}
if (!number.isEmpty()) {
number.append(QStringLiteral(" - "));
}
// read additional meta info
// - from an INI file called info.ini in the album directory (must be created before recording)
// - track lengths might be specified for each track in the [length] section (useful to get rid of advertisements at the end)
// - year, genre, total_tracks and total_disks might be specified in the [general] section
if (targetDir.exists(QStringLiteral("info.ini"))) {
fstream infoFile;
infoFile.exceptions(ios_base::badbit | ios_base::failbit);
try {
infoFile.open((targetDir.path() + QStringLiteral("/info.ini")).toLocal8Bit().data(), ios_base::in);
IniFile infoIni;
infoIni.parse(infoFile);
for (auto &scope : infoIni.data()) {
if (scope.first == "length") {
if (m_watcher.trackNumber()) {
// reading length scope is only possible if track number known because the track number is used for mapping
for (const auto &entry : scope.second) {
try {
if (stringToNumber<unsigned int>(entry.first) == m_watcher.trackNumber()) {
// length entry for this track
length = QString::fromLocal8Bit(entry.second.data());
break;
}
} catch (const ConversionException &) {
cerr << "Warning: Ignoring non-numeric key \"" << entry.first << "\" in [length] section of info.ini." << endl;
}
}
}
} else if (scope.first == "general") {
for (const auto &entry : scope.second) {
if (entry.first == "year") {
year = QString::fromLocal8Bit(entry.second.data());
} else if (entry.first == "genre") {
genre = QString::fromLocal8Bit(entry.second.data());
} else if (entry.first == "total_tracks") {
totalTracks = QString::fromLocal8Bit(entry.second.data());
} else if (entry.first == "total_disks") {
totalDisks = QString::fromLocal8Bit(entry.second.data());
} else {
cerr << "Warning: Ignoring unknown property \"" << entry.first << "\" in [general] section of info.ini." << endl;
}
}
} else {
cerr << "Warning: Ignoring unknown section [" << scope.first << "] in info.ini." << endl;
}
}
} catch (const std::ios_base::failure &failure) {
cerr << "Warning: Can't parse info.ini because an IO error occurred: " << failure.what() << endl;
}
}
// determine target name/path
QString targetName(
QStringLiteral("%3%1%2").arg(m_watcher.title().isEmpty() ? unknownTitle : validFileName(m_watcher.title()), m_targetExtension, number));
unsigned int count = 1;
while (targetDir.exists(targetName)) {
++count;
targetName
= QStringLiteral("%3%1 (%4)%2").arg(m_watcher.title().isEmpty() ? unknownTitle : m_watcher.title(), m_targetExtension, number).arg(count);
}
auto targetPath = targetDir.absoluteFilePath(targetName);
// set input device
QStringList args;
args << QStringLiteral("-f");
args << QStringLiteral("pulse");
args << m_inputOptions;
args << QStringLiteral("-i");
args << m_sink;
// set length if specified in info.ini
if (!length.isEmpty() || !m_watcher.length().isNull()) {
args << "-t";
args << (length.isEmpty() ? QString::number(m_watcher.length().totalSeconds()) : length);
}
// set additional options
args << m_options;
// set meta data
addMetaData(args, QStringLiteral("title"), m_watcher.title());
addMetaData(args, QStringLiteral("album"), m_watcher.album());
addMetaData(args, QStringLiteral("artist"), m_watcher.artist());
addMetaData(args, QStringLiteral("genre"), genre.isEmpty() ? m_watcher.genre() : genre);
addMetaData(args, QStringLiteral("year"), year.isEmpty() ? m_watcher.year() : year);
if (m_watcher.trackNumber()) {
addMetaData(args, QStringLiteral("track"),
totalTracks.isEmpty() ? QString::number(m_watcher.trackNumber()) : QString::number(m_watcher.trackNumber()) % QChar('/') % totalTracks);
}
if (m_watcher.diskNumber()) {
addMetaData(args, QStringLiteral("disk"),
totalDisks.isEmpty() ? QString::number(m_watcher.diskNumber()) : QString::number(m_watcher.diskNumber()) % QChar('/') % totalDisks);
}
// set output file
args << targetPath;
m_ffmpeg->setArguments(args);
// start process
m_ffmpeg->start();
// resume player
m_watcher.play();
m_watcher.setSilent(false);
}
void FfmpegLauncher::stopFfmpeg()
{
if (m_ffmpeg->state() != QProcess::NotRunning) {
m_ffmpeg->terminate();
m_ffmpeg->waitForFinished(10000);
if (m_ffmpeg->state() != QProcess::NotRunning) {
m_ffmpeg->kill();
m_ffmpeg->waitForFinished(5000);
if (m_ffmpeg->state() != QProcess::NotRunning) {
throw runtime_error("Unable to terminate/kill ffmpeg process.");
}
}
}
}
void FfmpegLauncher::ffmpegStarted()
{
cerr << "Started ffmpeg: ";
cerr << m_ffmpeg->program();
for (const auto &arg : m_ffmpeg->arguments()) {
cerr << ' ' << arg;
}
cerr << endl;
}
void FfmpegLauncher::ffmpegError()
{
cerr << "Failed to start ffmpeg: " << m_ffmpeg->errorString() << '\n';
}
void FfmpegLauncher::ffmpegFinished(int exitCode)
{
cerr << "FFmpeg finished with exit code " << exitCode << '\n';
}
} // namespace DBusSoundRecorder