improved compatibility with latest version of Yfitops
This commit is contained in:
parent
a6199d0fdd
commit
a5936281d7
|
@ -4,6 +4,8 @@
|
||||||
#include <c++utilities/io/inifile.h>
|
#include <c++utilities/io/inifile.h>
|
||||||
#include <c++utilities/conversion/stringconversion.h>
|
#include <c++utilities/conversion/stringconversion.h>
|
||||||
|
|
||||||
|
#include <QStringBuilder>
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
|
@ -23,12 +25,14 @@ FfmpegLauncher::FfmpegLauncher(PlayerWatcher &watcher, QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
m_watcher(watcher),
|
m_watcher(watcher),
|
||||||
m_sink(QStringLiteral("default")),
|
m_sink(QStringLiteral("default")),
|
||||||
|
m_inputOptions(),
|
||||||
m_options(),
|
m_options(),
|
||||||
m_targetDir(QStringLiteral(".")),
|
m_targetDir(QStringLiteral(".")),
|
||||||
m_targetExtension(QStringLiteral(".m4a")),
|
m_targetExtension(QStringLiteral(".m4a")),
|
||||||
m_ffmpeg(new QProcess(this))
|
m_ffmpeg(new QProcess(this))
|
||||||
{
|
{
|
||||||
connect(&watcher, &PlayerWatcher::nextSong, this, &FfmpegLauncher::nextSong);
|
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, &QProcess::started, this, &FfmpegLauncher::ffmpegStarted);
|
||||||
connect(m_ffmpeg, static_cast<void(QProcess::*)(QProcess::ProcessError)>(&QProcess::error), this, &FfmpegLauncher::ffmpegError);
|
connect(m_ffmpeg, static_cast<void(QProcess::*)(QProcess::ProcessError)>(&QProcess::error), this, &FfmpegLauncher::ffmpegError);
|
||||||
connect(m_ffmpeg, static_cast<void(QProcess::*)(int)>(&QProcess::finished), this, &FfmpegLauncher::ffmpegFinished);
|
connect(m_ffmpeg, static_cast<void(QProcess::*)(int)>(&QProcess::finished), this, &FfmpegLauncher::ffmpegFinished);
|
||||||
|
@ -46,112 +50,145 @@ void addMetaData(QStringList &args, const QString &field, const QString &value)
|
||||||
|
|
||||||
void FfmpegLauncher::nextSong()
|
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
|
// 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 : m_watcher.artist(), m_watcher.artist().isEmpty() ? miscCategory : 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 ios_base::failure &) {
|
||||||
|
cerr << "Warning: Can't parse info.ini because an IO error occured." << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// determine target name/path
|
||||||
|
QString targetName(QStringLiteral("%3%1%2").arg(m_watcher.title().isEmpty() ? unknownTitle : 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) {
|
if(m_ffmpeg->state() != QProcess::NotRunning) {
|
||||||
m_ffmpeg->terminate();
|
m_ffmpeg->terminate();
|
||||||
m_ffmpeg->waitForFinished(250);
|
m_ffmpeg->waitForFinished(10000);
|
||||||
if(m_ffmpeg->state() != QProcess::NotRunning) {
|
if(m_ffmpeg->state() != QProcess::NotRunning) {
|
||||||
m_ffmpeg->kill();
|
m_ffmpeg->kill();
|
||||||
m_ffmpeg->waitForFinished(250);
|
m_ffmpeg->waitForFinished(5000);
|
||||||
if(m_ffmpeg->state() != QProcess::NotRunning) {
|
if(m_ffmpeg->state() != QProcess::NotRunning) {
|
||||||
throw runtime_error("Unable to terminate/kill ffmpeg process.");
|
throw runtime_error("Unable to terminate/kill ffmpeg process.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(m_watcher.isPlaying()) {
|
|
||||||
// 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 : m_watcher.artist(), m_watcher.artist().isEmpty() ? miscCategory : 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;
|
|
||||||
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(" - "));
|
|
||||||
}
|
|
||||||
// determine additional info
|
|
||||||
// - from a file called info.ini in the album directory
|
|
||||||
// - currently only track length is supported (used to get rid of advertisements at the end)
|
|
||||||
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);
|
|
||||||
// read length scope, only possible if track number known because the track number is used for mapping
|
|
||||||
if(m_watcher.trackNumber()) {
|
|
||||||
for(auto &scope : infoIni.data()) {
|
|
||||||
if(scope.first == "length") {
|
|
||||||
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 info.ini." << endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(const ios_base::failure &) {
|
|
||||||
cerr << "Warning: Can't parse info.ini because an IO error occured." << endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// determine target name/path
|
|
||||||
QString targetName(QStringLiteral("%3%1%2").arg(m_watcher.title().isEmpty() ? unknownTitle : 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 << QStringLiteral("-i");
|
|
||||||
args << m_sink;
|
|
||||||
// set length
|
|
||||||
if(!length.isEmpty()) {
|
|
||||||
args << "-t";
|
|
||||||
args << 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"), m_watcher.genre());
|
|
||||||
addMetaData(args, QStringLiteral("year"), m_watcher.year());
|
|
||||||
if(m_watcher.trackNumber()) {
|
|
||||||
addMetaData(args, QStringLiteral("track"), QString::number(m_watcher.trackNumber()));
|
|
||||||
}
|
|
||||||
if(m_watcher.diskNumber()) {
|
|
||||||
addMetaData(args, QStringLiteral("disk"), QString::number(m_watcher.diskNumber()));
|
|
||||||
}
|
|
||||||
// set output file
|
|
||||||
args << targetPath;
|
|
||||||
m_ffmpeg->setArguments(args);
|
|
||||||
// start process
|
|
||||||
m_ffmpeg->start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FfmpegLauncher::ffmpegStarted()
|
void FfmpegLauncher::ffmpegStarted()
|
||||||
|
|
|
@ -16,13 +16,15 @@ public:
|
||||||
explicit FfmpegLauncher(PlayerWatcher &watcher, QObject *parent = nullptr);
|
explicit FfmpegLauncher(PlayerWatcher &watcher, QObject *parent = nullptr);
|
||||||
|
|
||||||
void setSink(const QString &sinkName);
|
void setSink(const QString &sinkName);
|
||||||
void setFfmpegBinary(const QString &path);
|
void setFFmpegInputOptions(const QString &options);
|
||||||
void setFfmpegOptions(const QString &options);
|
void setFFmpegBinary(const QString &path);
|
||||||
|
void setFFmpegOptions(const QString &options);
|
||||||
void setTargetDir(const QString &path);
|
void setTargetDir(const QString &path);
|
||||||
void setTargetExtension(const QString &extension);
|
void setTargetExtension(const QString &extension);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void nextSong();
|
void nextSong();
|
||||||
|
void stopFfmpeg();
|
||||||
void ffmpegStarted();
|
void ffmpegStarted();
|
||||||
void ffmpegError();
|
void ffmpegError();
|
||||||
void ffmpegFinished(int exitCode);
|
void ffmpegFinished(int exitCode);
|
||||||
|
@ -30,6 +32,7 @@ private slots:
|
||||||
private:
|
private:
|
||||||
PlayerWatcher &m_watcher;
|
PlayerWatcher &m_watcher;
|
||||||
QString m_sink;
|
QString m_sink;
|
||||||
|
QStringList m_inputOptions;
|
||||||
QStringList m_options;
|
QStringList m_options;
|
||||||
QDir m_targetDir;
|
QDir m_targetDir;
|
||||||
QString m_targetExtension;
|
QString m_targetExtension;
|
||||||
|
@ -41,12 +44,17 @@ inline void FfmpegLauncher::setSink(const QString &sinkName)
|
||||||
m_sink = sinkName;
|
m_sink = sinkName;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void FfmpegLauncher::setFfmpegBinary(const QString &path)
|
inline void FfmpegLauncher::setFFmpegInputOptions(const QString &options)
|
||||||
|
{
|
||||||
|
m_inputOptions = options.split(QChar(' '), QString::SkipEmptyParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void FfmpegLauncher::setFFmpegBinary(const QString &path)
|
||||||
{
|
{
|
||||||
m_ffmpeg->setProgram(path);
|
m_ffmpeg->setProgram(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void FfmpegLauncher::setFfmpegOptions(const QString &options)
|
inline void FfmpegLauncher::setFFmpegOptions(const QString &options)
|
||||||
{
|
{
|
||||||
m_options = options.split(QChar(' '), QString::SkipEmptyParts);
|
m_options = options.split(QChar(' '), QString::SkipEmptyParts);
|
||||||
}
|
}
|
||||||
|
|
18
main.cpp
18
main.cpp
|
@ -28,6 +28,9 @@ int main(int argc, char *argv[])
|
||||||
sinkArg.setValueNames({"sink"});
|
sinkArg.setValueNames({"sink"});
|
||||||
sinkArg.setRequiredValueCount(1);
|
sinkArg.setRequiredValueCount(1);
|
||||||
sinkArg.setCombinable(true);
|
sinkArg.setCombinable(true);
|
||||||
|
Argument ffmpegInputOptions("ffmpeg-input-options", "i", "specifies options for the ffmpeg input device");
|
||||||
|
ffmpegInputOptions.setRequiredValueCount(1);
|
||||||
|
ffmpegInputOptions.setCombinable(true);
|
||||||
Argument targetDirArg("target-dir", "t", "specifies the target directory");
|
Argument targetDirArg("target-dir", "t", "specifies the target directory");
|
||||||
targetDirArg.setValueNames({"path"});
|
targetDirArg.setValueNames({"path"});
|
||||||
targetDirArg.setRequiredValueCount(1);
|
targetDirArg.setRequiredValueCount(1);
|
||||||
|
@ -36,6 +39,8 @@ int main(int argc, char *argv[])
|
||||||
targetExtArg.setValueNames({"extension"});
|
targetExtArg.setValueNames({"extension"});
|
||||||
targetExtArg.setRequiredValueCount(1);
|
targetExtArg.setRequiredValueCount(1);
|
||||||
targetExtArg.setCombinable(true);
|
targetExtArg.setCombinable(true);
|
||||||
|
Argument ignorePlaybackStatusArg("ignore-playback-status", string(), "ignores the playback status (does not call PlaybackStatus())");
|
||||||
|
ignorePlaybackStatusArg.setCombinable(true);
|
||||||
Argument ffmpegBinArg("ffmpeg-bin", "f", "specifies the path to the ffmpeg binary");
|
Argument ffmpegBinArg("ffmpeg-bin", "f", "specifies the path to the ffmpeg binary");
|
||||||
ffmpegBinArg.setValueNames({"path"});
|
ffmpegBinArg.setValueNames({"path"});
|
||||||
ffmpegBinArg.setRequiredValueCount(1);
|
ffmpegBinArg.setRequiredValueCount(1);
|
||||||
|
@ -44,7 +49,7 @@ int main(int argc, char *argv[])
|
||||||
ffmpegOptions.setValueNames({"options"});
|
ffmpegOptions.setValueNames({"options"});
|
||||||
ffmpegOptions.setRequiredValueCount(1);
|
ffmpegOptions.setRequiredValueCount(1);
|
||||||
ffmpegOptions.setCombinable(true);
|
ffmpegOptions.setCombinable(true);
|
||||||
recordArg.setSecondaryArguments({&applicationArg, &sinkArg, &targetDirArg, &targetExtArg, &ffmpegBinArg, &ffmpegOptions});
|
recordArg.setSecondaryArguments({&applicationArg, &sinkArg, &ffmpegInputOptions, &targetDirArg, &targetExtArg, &ignorePlaybackStatusArg, &ffmpegBinArg, &ffmpegOptions});
|
||||||
parser.setMainArguments({&recordArg, &helpArg});
|
parser.setMainArguments({&recordArg, &helpArg});
|
||||||
parser.setIgnoreUnknownArguments(false);
|
parser.setIgnoreUnknownArguments(false);
|
||||||
// parse command line arguments
|
// parse command line arguments
|
||||||
|
@ -60,17 +65,20 @@ int main(int argc, char *argv[])
|
||||||
cerr << "Watching MPRIS service of the specified application \"" << applicationArg.values().front() << "\" ..." << endl;
|
cerr << "Watching MPRIS service of the specified application \"" << applicationArg.values().front() << "\" ..." << endl;
|
||||||
// create app loop, player watcher and ffmpeg launcher
|
// create app loop, player watcher and ffmpeg launcher
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
PlayerWatcher watcher(QString::fromLocal8Bit(applicationArg.values().front().data()));
|
PlayerWatcher watcher(QString::fromLocal8Bit(applicationArg.values().front().data()), ignorePlaybackStatusArg.isPresent());
|
||||||
FfmpegLauncher ffmpeg(watcher);
|
FfmpegLauncher ffmpeg(watcher);
|
||||||
// pass specified args to ffmpeg launcher
|
// pass specified args to ffmpeg launcher
|
||||||
if(sinkArg.isPresent()) {
|
if(sinkArg.isPresent()) {
|
||||||
ffmpeg.setSink(QString::fromLocal8Bit(sinkArg.values().front().data()));
|
ffmpeg.setSink(QString::fromLocal8Bit(sinkArg.values().front().data()));
|
||||||
}
|
}
|
||||||
|
if(ffmpegInputOptions.isPresent()) {
|
||||||
|
ffmpeg.setFFmpegInputOptions(QString::fromLocal8Bit(ffmpegInputOptions.values().front().data()));
|
||||||
|
}
|
||||||
if(ffmpegBinArg.isPresent()) {
|
if(ffmpegBinArg.isPresent()) {
|
||||||
ffmpeg.setFfmpegBinary(QString::fromLocal8Bit(ffmpegBinArg.values().front().data()));
|
ffmpeg.setFFmpegBinary(QString::fromLocal8Bit(ffmpegBinArg.values().front().data()));
|
||||||
}
|
}
|
||||||
if(ffmpegOptions.isPresent()) {
|
if(ffmpegOptions.isPresent()) {
|
||||||
ffmpeg.setFfmpegOptions(QString::fromLocal8Bit(ffmpegOptions.values().front().data()));
|
ffmpeg.setFFmpegOptions(QString::fromLocal8Bit(ffmpegOptions.values().front().data()));
|
||||||
}
|
}
|
||||||
if(targetDirArg.isPresent()) {
|
if(targetDirArg.isPresent()) {
|
||||||
ffmpeg.setTargetDir(QString::fromLocal8Bit(targetDirArg.values().front().data()));
|
ffmpeg.setTargetDir(QString::fromLocal8Bit(targetDirArg.values().front().data()));
|
||||||
|
@ -80,7 +88,7 @@ int main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
// enter app loop
|
// enter app loop
|
||||||
return app.exec();
|
return app.exec();
|
||||||
} else {
|
} else if(!helpArg.isPresent()) {
|
||||||
cerr << "No operation specified." << endl;
|
cerr << "No operation specified." << endl;
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
using namespace ChronoUtilities;
|
||||||
|
|
||||||
namespace DBusSoundRecorder {
|
namespace DBusSoundRecorder {
|
||||||
|
|
||||||
|
@ -18,18 +19,30 @@ inline ostream &operator <<(ostream &stream, const QString &str)
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerWatcher::PlayerWatcher(const QString &appName, QObject *parent) :
|
PlayerWatcher::PlayerWatcher(const QString &appName, bool ignorePlaybackStatus, QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
m_service(QStringLiteral("org.mpris.MediaPlayer2.%1").arg(appName)),
|
m_mediaPlayerInterfaceName(QStringLiteral("org.mpris.MediaPlayer2.%1").arg(appName)),
|
||||||
m_serviceWatcher(new QDBusServiceWatcher(m_service, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this)),
|
m_mediaPlayerServiceWatcher(new QDBusServiceWatcher(m_mediaPlayerInterfaceName, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this)),
|
||||||
m_propertiesInterface(new OrgFreedesktopDBusPropertiesInterface(m_service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this)),
|
m_propertiesInterface(new OrgFreedesktopDBusPropertiesInterface(m_mediaPlayerInterfaceName, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this)),
|
||||||
m_playerInterface(new OrgMprisMediaPlayer2PlayerInterface(m_service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this)),
|
m_playerInterface(new OrgMprisMediaPlayer2PlayerInterface(m_mediaPlayerInterfaceName, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this)),
|
||||||
m_isPlaying(false),
|
m_isPlaying(false),
|
||||||
|
m_isAd(false),
|
||||||
m_trackNumber(0),
|
m_trackNumber(0),
|
||||||
m_diskNumber(0)
|
m_diskNumber(0),
|
||||||
|
m_silent(false),
|
||||||
|
m_ignorePlaybackStatus(ignorePlaybackStatus)
|
||||||
{
|
{
|
||||||
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &PlayerWatcher::serviceOwnerChanged);
|
if(!connect(m_mediaPlayerServiceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &PlayerWatcher::serviceOwnerChanged)) {
|
||||||
connect(m_propertiesInterface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &PlayerWatcher::propertiesChanged);
|
cout << "Warning: Unable to connect \"serviceOwnerChanged\" signal of service watcher." << endl;
|
||||||
|
}
|
||||||
|
// The code below does not work anymore with the newest version of Spotify.
|
||||||
|
//if(!connect(m_propertiesInterface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &PlayerWatcher::propertiesChanged)) {
|
||||||
|
// cout << "Warning: Unable to connect \"PropertiesChanged\" signal of properties interface." << endl;
|
||||||
|
//}
|
||||||
|
// However, the following works:
|
||||||
|
if(!QDBusConnection::sessionBus().connect(m_mediaPlayerInterfaceName, QStringLiteral("/org/mpris/MediaPlayer2"), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged"), this, SLOT(propertiesChanged()))) {
|
||||||
|
cout << "Warning: Unable to connect \"PropertiesChanged\" signal of properties interface." << endl;
|
||||||
|
}
|
||||||
propertiesChanged();
|
propertiesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +79,22 @@ void PlayerWatcher::serviceOwnerChanged(const QString &service, const QString &o
|
||||||
void PlayerWatcher::propertiesChanged()
|
void PlayerWatcher::propertiesChanged()
|
||||||
{
|
{
|
||||||
// get meta data
|
// get meta data
|
||||||
if(!m_playerInterface->playbackStatus().compare(QLatin1String("playing"), Qt::CaseInsensitive)) {
|
QVariantMap metadata = m_playerInterface->metadata();
|
||||||
|
m_isAd = metadata.value(QStringLiteral("mpris:trackid")).toString().startsWith(QLatin1String("spotify:ad"));
|
||||||
|
QString title = metadata.value(QStringLiteral("xesam:title")).toString();
|
||||||
|
QString album = metadata.value(QStringLiteral("xesam:album")).toString();
|
||||||
|
QString artist = metadata.value(QStringLiteral("xesam:artist")).toString();
|
||||||
|
bool isPlaying;
|
||||||
|
if(m_ignorePlaybackStatus) {
|
||||||
|
// determine playback status by checking whether there is a song title
|
||||||
|
isPlaying = !title.isEmpty();
|
||||||
|
} else {
|
||||||
|
isPlaying = !m_playerInterface->playbackStatus().compare(QLatin1String("playing"), Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
if(isPlaying) {
|
||||||
if(!m_isPlaying) {
|
if(!m_isPlaying) {
|
||||||
m_isPlaying = true;
|
|
||||||
cerr << "Playback started" << endl;
|
cerr << "Playback started" << endl;
|
||||||
}
|
}
|
||||||
QVariantMap metadata = m_playerInterface->metadata();
|
|
||||||
QString title = metadata.value(QStringLiteral("xesam:title")).toString();
|
|
||||||
QString album = metadata.value(QStringLiteral("xesam:album")).toString();
|
|
||||||
QString artist = metadata.value(QStringLiteral("xesam:artist")).toString();
|
|
||||||
// use title, album and artist to identify song
|
// use title, album and artist to identify song
|
||||||
if(m_title != title || m_album != album || m_artist != artist) {
|
if(m_title != title || m_album != album || m_artist != artist) {
|
||||||
// next song playing
|
// next song playing
|
||||||
|
@ -92,23 +112,37 @@ void PlayerWatcher::propertiesChanged()
|
||||||
if(!m_diskNumber) {
|
if(!m_diskNumber) {
|
||||||
m_diskNumber = metadata.value(QStringLiteral("xesam:discNumber")).toUInt();
|
m_diskNumber = metadata.value(QStringLiteral("xesam:discNumber")).toUInt();
|
||||||
}
|
}
|
||||||
m_length = metadata.value(QStringLiteral("xesam:length")).toULongLong();
|
m_length = TimeSpan(metadata.value(QStringLiteral("mpris:length")).toULongLong() * 10);
|
||||||
// notify
|
// notify
|
||||||
cerr << "Next song: " << m_title << endl;
|
cerr << "Next song: " << m_title << endl;
|
||||||
if(!m_isPlaying) {
|
if(!m_isPlaying && !m_silent) {
|
||||||
|
m_isPlaying = true;
|
||||||
emit playbackStarted();
|
emit playbackStarted();
|
||||||
}
|
}
|
||||||
emit nextSong();
|
if(!m_silent) {
|
||||||
|
m_isPlaying = true;
|
||||||
|
emit nextSong();
|
||||||
|
}
|
||||||
} else if(!m_isPlaying) {
|
} else if(!m_isPlaying) {
|
||||||
emit playbackStarted();
|
if(!m_silent) {
|
||||||
|
m_isPlaying = true;
|
||||||
|
emit playbackStarted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if(m_isPlaying) {
|
} else if(m_isPlaying) {
|
||||||
m_isPlaying = false;
|
m_isPlaying = false;
|
||||||
cerr << "Playback stopped" << endl;
|
cerr << "Playback stopped" << endl;
|
||||||
emit playbackStopped();
|
if(!m_silent) {
|
||||||
|
emit playbackStopped();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerWatcher::notificationReceived()
|
||||||
|
{
|
||||||
|
cout << "It works!" << endl;
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerWatcher::seeked(qlonglong pos)
|
void PlayerWatcher::seeked(qlonglong pos)
|
||||||
{
|
{
|
||||||
cerr << "Seeked: " << pos << endl;
|
cerr << "Seeked: " << pos << endl;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#ifndef PLAYERWATCHER_H
|
#ifndef PLAYERWATCHER_H
|
||||||
#define PLAYERWATCHER_H
|
#define PLAYERWATCHER_H
|
||||||
|
|
||||||
|
#include <c++utilities/chrono/timespan.h>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
QT_FORWARD_DECLARE_CLASS(QDBusServiceWatcher)
|
QT_FORWARD_DECLARE_CLASS(QDBusServiceWatcher)
|
||||||
|
@ -14,7 +16,7 @@ class PlayerWatcher : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit PlayerWatcher(const QString &appName, QObject *parent = nullptr);
|
explicit PlayerWatcher(const QString &appName, bool ignorePlaybackStatus = false, QObject *parent = nullptr);
|
||||||
|
|
||||||
void play();
|
void play();
|
||||||
void stop();
|
void stop();
|
||||||
|
@ -22,6 +24,8 @@ public:
|
||||||
void playPause();
|
void playPause();
|
||||||
|
|
||||||
bool isPlaying() const;
|
bool isPlaying() const;
|
||||||
|
bool isPlaybackStatusIgnored() const;
|
||||||
|
bool isAd() const;
|
||||||
const QString &title() const;
|
const QString &title() const;
|
||||||
const QString &album() const;
|
const QString &album() const;
|
||||||
const QString &artist() const;
|
const QString &artist() const;
|
||||||
|
@ -29,7 +33,8 @@ public:
|
||||||
const QString &genre() const;
|
const QString &genre() const;
|
||||||
unsigned int trackNumber() const;
|
unsigned int trackNumber() const;
|
||||||
unsigned int diskNumber() const;
|
unsigned int diskNumber() const;
|
||||||
unsigned long long length() const;
|
ChronoUtilities::TimeSpan length() const;
|
||||||
|
void setSilent(bool silent);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void nextSong();
|
void nextSong();
|
||||||
|
@ -39,14 +44,18 @@ signals:
|
||||||
private slots:
|
private slots:
|
||||||
void serviceOwnerChanged(const QString &service, const QString &oldOwner, const QString &newOwner);
|
void serviceOwnerChanged(const QString &service, const QString &oldOwner, const QString &newOwner);
|
||||||
void propertiesChanged();
|
void propertiesChanged();
|
||||||
|
void notificationReceived();
|
||||||
void seeked(qlonglong pos);
|
void seeked(qlonglong pos);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_service;
|
QString m_mediaPlayerInterfaceName;
|
||||||
QDBusServiceWatcher *m_serviceWatcher;
|
QDBusServiceWatcher *m_mediaPlayerServiceWatcher;
|
||||||
|
QString m_notifyInterfaceName;
|
||||||
|
QDBusServiceWatcher *m_notifyServiceWatcher;
|
||||||
OrgFreedesktopDBusPropertiesInterface *m_propertiesInterface;
|
OrgFreedesktopDBusPropertiesInterface *m_propertiesInterface;
|
||||||
OrgMprisMediaPlayer2PlayerInterface *m_playerInterface;
|
OrgMprisMediaPlayer2PlayerInterface *m_playerInterface;
|
||||||
bool m_isPlaying;
|
bool m_isPlaying;
|
||||||
|
bool m_isAd;
|
||||||
QString m_title;
|
QString m_title;
|
||||||
QString m_album;
|
QString m_album;
|
||||||
QString m_artist;
|
QString m_artist;
|
||||||
|
@ -54,7 +63,9 @@ private:
|
||||||
QString m_genre;
|
QString m_genre;
|
||||||
unsigned int m_trackNumber;
|
unsigned int m_trackNumber;
|
||||||
unsigned int m_diskNumber;
|
unsigned int m_diskNumber;
|
||||||
unsigned long long m_length;
|
ChronoUtilities::TimeSpan m_length;
|
||||||
|
bool m_silent;
|
||||||
|
bool m_ignorePlaybackStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool PlayerWatcher::isPlaying() const
|
inline bool PlayerWatcher::isPlaying() const
|
||||||
|
@ -62,6 +73,16 @@ inline bool PlayerWatcher::isPlaying() const
|
||||||
return m_isPlaying;
|
return m_isPlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool PlayerWatcher::isPlaybackStatusIgnored() const
|
||||||
|
{
|
||||||
|
return m_ignorePlaybackStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool PlayerWatcher::isAd() const
|
||||||
|
{
|
||||||
|
return m_isAd;
|
||||||
|
}
|
||||||
|
|
||||||
inline const QString &PlayerWatcher::title() const
|
inline const QString &PlayerWatcher::title() const
|
||||||
{
|
{
|
||||||
return m_title;
|
return m_title;
|
||||||
|
@ -97,11 +118,16 @@ inline unsigned int PlayerWatcher::diskNumber() const
|
||||||
return m_diskNumber;
|
return m_diskNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline unsigned long long PlayerWatcher::length() const
|
inline ChronoUtilities::TimeSpan PlayerWatcher::length() const
|
||||||
{
|
{
|
||||||
return m_length;
|
return m_length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void PlayerWatcher::setSilent(bool silent)
|
||||||
|
{
|
||||||
|
m_silent = silent;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // PLAYERWATCHER_H
|
#endif // PLAYERWATCHER_H
|
||||||
|
|
Loading…
Reference in New Issue