1 /* Converter.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11 
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "Converter.h"
22 
23 #include "Utils/FileUtils.h"
24 #include "Utils/Logger/Logger.h"
25 #include "Utils/MetaData/MetaDataList.h"
26 #include "Utils/StandardPaths.h"
27 #include "Utils/Utils.h"
28 
29 #include <QStringList>
30 #include <QProcess>
31 #include <QMap>
32 #include <QVariant>
33 
34 using ProcessMap = QList<QStringList>;
35 
36 struct Converter::Private
37 {
38 	QStringList logFiles;
39 	ProcessMap processes;
40 	MetaDataList tracks;
41 	QString targetDirectory;
42 	QMap<int, QProcess*> runningProcesses;
43 
44 	int initialCount;
45 	int currentIndex;
46 	int commandCount;
47 	int errorCount;
48 	int processCount;
49 	int quality;
50 	bool stopped;
51 
PrivateConverter::Private52 	Private(int quality) :
53 		initialCount(0),
54 		currentIndex(0),
55 		commandCount(0),
56 		errorCount(0),
57 		processCount(0),
58 		quality(quality),
59 		stopped(false)
60 	{}
61 };
62 
Converter(int quality,QObject * parent)63 Converter::Converter(int quality, QObject* parent) :
64 	QObject(parent)
65 {
66 	m = Pimpl::make<Private>(quality);
67 
68 }
69 
~Converter()70 Converter::~Converter()
71 {
72 	Util::File::deleteFiles(m->logFiles);
73 }
74 
loggingDirectory() const75 QString Converter::loggingDirectory() const
76 {
77 	return Util::tempPath("encoder-logs");
78 }
79 
targetDirectory() const80 QString Converter::targetDirectory() const
81 {
82 	return m->targetDirectory;
83 }
84 
addMetadata(const MetaDataList & tracks)85 void Converter::addMetadata(const MetaDataList& tracks)
86 {
87 	m->initialCount = tracks.count();
88 	m->tracks.clear();
89 
90 	QStringList formats = supportedInputFormats();
91 	for(const MetaData& md : tracks)
92 	{
93 		const QString filepath(md.filepath());
94 		for(const QString& format : formats)
95 		{
96 			if(filepath.endsWith(format, Qt::CaseInsensitive))
97 			{
98 				m->tracks << md;
99 				break;
100 			}
101 		}
102 	}
103 }
104 
start(int numThreads,const QString & targetDirectoryectory)105 void Converter::start(int numThreads, const QString& targetDirectoryectory)
106 {
107 	m->processCount = numThreads;
108 	m->targetDirectory = targetDirectoryectory;
109 	m->runningProcesses.clear();
110 	m->errorCount = 0;
111 	m->processes.clear();
112 	m->currentIndex = 0;
113 	m->stopped = false;
114 
115 	for(const MetaData& md : m->tracks)
116 	{
117 		m->processes << processEntry(md);
118 	}
119 
120 	m->commandCount = m->tracks.count();
121 
122 	auto processCount = std::min(m->processes.size(), m->processCount);
123 	for(int i = 0; i < processCount; i++)
124 	{
125 		QString process_name = binary();
126 		QStringList arguments = m->processes.takeFirst();
127 
128 		startProcess(process_name, arguments);
129 	}
130 }
131 
stop()132 void Converter::stop()
133 {
134 	m->stopped = true;
135 
136 	const auto processKeys = m->runningProcesses.keys();
137 	for(int key : processKeys)
138 	{
139 		QProcess* p = m->runningProcesses.value(key);
140 		if(p)
141 		{
142 			p->kill();
143 		}
144 	}
145 }
146 
errorCount() const147 int Converter::errorCount() const
148 {
149 	return m->errorCount;
150 }
151 
quality() const152 int Converter::quality() const
153 {
154 	return m->quality;
155 }
156 
fileCount() const157 int Converter::fileCount() const
158 {
159 	return m->tracks.count();
160 }
161 
initialCount() const162 int Converter::initialCount() const
163 {
164 	return m->initialCount;
165 }
166 
isAvailable() const167 bool Converter::isAvailable() const
168 {
169 	return QProcess::startDetached(binary(), {"--version"});
170 }
171 
targetFile(const MetaData & md) const172 QString Converter::targetFile(const MetaData& md) const
173 {
174 	const auto [dirname, filename] = Util::File::splitFilename(md.filepath());
175 
176 	QString target = Util::File::cleanFilename(m->targetDirectory + "/" + filename);
177 	target = target.left(target.lastIndexOf(".")) + "." + extension();
178 
179 	return target;
180 }
181 
startProcess(const QString & command,const QStringList & arguments)182 bool Converter::startProcess(const QString& command, const QStringList& arguments)
183 {
184 	m->currentIndex++;
185 
186 	Util::File::createDir(loggingDirectory());
187 	const auto logFile = QString("%1/encoder_%2_%3.out")
188 		.arg(loggingDirectory())
189 		.arg(binary())
190 		.arg(m->currentIndex);
191 
192 	m->logFiles << logFile;
193 
194 	int id = Util::randomNumber(100, 1000000);
195 
196 	auto* process = new QProcess(this);
197 	process->setStandardOutputFile(logFile);
198 	process->setStandardErrorFile(logFile);
199 	process->setProperty("id", id);
200 	m->runningProcesses.insert(id, process);
201 
202 	connect(process,
203 	        static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
204 	        this,
205 	        &Converter::processFinished);
206 	connect(process, &QProcess::errorOccurred, this, &Converter::errorOccured);
207 
208 	spLog(Log::Debug, this) << "Starting: " << command << " " << arguments.join(" ");
209 	process->start(command, arguments);
210 
211 	return true;
212 }
213 
processFinished(int ret,QProcess::ExitStatus exit_status)214 void Converter::processFinished(int ret, QProcess::ExitStatus exit_status)
215 {
216 	Q_UNUSED(exit_status)
217 
218 	auto* process = static_cast<QProcess*>(sender());
219 
220 	spLog(Log::Debug, this) << "process finished";
221 	if(ret != 0)
222 	{
223 		m->errorCount++;
224 		spLog(Log::Warning, this) << "Encoding process failed with code " << ret << process->program();
225 	}
226 
227 	int id = process->property("id").toInt();
228 	m->runningProcesses.remove(id);
229 
230 	emit sigProgress(100 - (m->processes.size() * 100) / m->commandCount);
231 
232 	if(!m->processes.isEmpty() && !m->stopped)
233 	{
234 		QStringList arguments = m->processes.takeFirst();
235 		startProcess(binary(), arguments);
236 	}
237 
238 	else if(m->runningProcesses.isEmpty())
239 	{
240 		emit sigFinished();
241 	}
242 
243 	else
244 	{
245 		spLog(Log::Warning, this) << "Something strange happened";
246 	}
247 }
248 
errorOccured(QProcess::ProcessError err)249 void Converter::errorOccured(QProcess::ProcessError err)
250 {
251 	auto* p = static_cast<QProcess*>(this->sender());
252 
253 	spLog(Log::Warning, this) << p->program() << ": " << p->arguments().join(", ");
254 	spLog(Log::Warning, this) << "Error: QProcess:ProcessError " << p->errorString();
255 
256 	processFinished(10000 + err, QProcess::ExitStatus::NormalExit);
257 }
258