1 /*
2 * Strawberry Music Player
3 * This file was part of Clementine.
4 * Copyright 2010, David Sansome <me@davidsansome.com>
5 * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
6 *
7 * Strawberry 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 * Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22 #include <functional>
23 #include <chrono>
24
25 #include <QtGlobal>
26 #include <QThread>
27 #include <QFile>
28 #include <QFileInfo>
29 #include <QTimer>
30 #include <QDateTime>
31 #include <QList>
32 #include <QVector>
33 #include <QString>
34 #include <QUrl>
35 #include <QImage>
36 #include <QtDebug>
37
38 #include "core/logging.h"
39 #include "core/utilities.h"
40 #include "core/taskmanager.h"
41 #include "core/musicstorage.h"
42 #include "core/tagreaderclient.h"
43 #include "core/song.h"
44 #include "organize.h"
45 #ifdef HAVE_GSTREAMER
46 # include "transcoder/transcoder.h"
47 #endif
48
49 using namespace std::chrono_literals;
50
51 class OrganizeFormat;
52
53 const int Organize::kBatchSize = 10;
54 #ifdef HAVE_GSTREAMER
55 const int Organize::kTranscodeProgressInterval = 500;
56 #endif
57
Organize(TaskManager * task_manager,std::shared_ptr<MusicStorage> destination,const OrganizeFormat & format,const bool copy,const bool overwrite,const bool mark_as_listened,const bool albumcover,const NewSongInfoList & songs_info,const bool eject_after,const QString & playlist,QObject * parent)58 Organize::Organize(TaskManager *task_manager, std::shared_ptr<MusicStorage> destination, const OrganizeFormat &format, const bool copy, const bool overwrite, const bool mark_as_listened, const bool albumcover, const NewSongInfoList &songs_info, const bool eject_after, const QString &playlist, QObject *parent)
59 : QObject(parent),
60 thread_(nullptr),
61 task_manager_(task_manager),
62 #ifdef HAVE_GSTREAMER
63 transcoder_(new Transcoder(this)),
64 #endif
65 process_files_timer_(new QTimer(this)),
66 destination_(destination),
67 format_(format),
68 copy_(copy),
69 overwrite_(overwrite),
70 mark_as_listened_(mark_as_listened),
71 albumcover_(albumcover),
72 eject_after_(eject_after),
73 task_count_(songs_info.count()),
74 playlist_(playlist),
75 tasks_complete_(0),
76 started_(false),
77 task_id_(0),
78 current_copy_progress_(0),
79 finished_(false) {
80
81 original_thread_ = thread();
82
83 process_files_timer_->setSingleShot(true);
84 process_files_timer_->setInterval(100ms);
85 QObject::connect(process_files_timer_, &QTimer::timeout, this, &Organize::ProcessSomeFiles);
86
87 tasks_pending_.reserve(songs_info.count());
88 for (const NewSongInfo &song_info : songs_info) {
89 tasks_pending_ << Task(song_info);
90 }
91
92 }
93
~Organize()94 Organize::~Organize() {
95
96 if (thread_) {
97 thread_->quit();
98 thread_->deleteLater();
99 }
100
101 }
102
Start()103 void Organize::Start() {
104
105 if (thread_) return;
106
107 task_id_ = task_manager_->StartTask(tr("Organizing files"));
108 task_manager_->SetTaskBlocksCollectionScans(task_id_);
109
110 thread_ = new QThread;
111 QObject::connect(thread_, &QThread::started, this, &Organize::ProcessSomeFiles);
112 #ifdef HAVE_GSTREAMER
113 QObject::connect(transcoder_, &Transcoder::JobComplete, this, &Organize::FileTranscoded);
114 QObject::connect(transcoder_, &Transcoder::LogLine, this, &Organize::LogLine);
115 #endif
116
117 moveToThread(thread_);
118 thread_->start();
119
120 }
121
ProcessSomeFiles()122 void Organize::ProcessSomeFiles() {
123
124 if (finished_) return;
125
126 if (!started_) {
127 if (!destination_->StartCopy(&supported_filetypes_)) {
128 // Failed to start - mark everything as failed :(
129 for (const Task &task : tasks_pending_) {
130 files_with_errors_ << task.song_info_.song_.url().toLocalFile();
131 }
132 tasks_pending_.clear();
133 }
134 started_ = true;
135 }
136
137 // None left?
138 if (tasks_pending_.isEmpty()) {
139 #ifdef HAVE_GSTREAMER
140 if (!tasks_transcoding_.isEmpty()) {
141 // Just wait - FileTranscoded will start us off again in a little while
142 qLog(Debug) << "Waiting for transcoding jobs";
143 transcode_progress_timer_.start(kTranscodeProgressInterval, this);
144 return;
145 }
146 #endif
147
148 UpdateProgress();
149
150 destination_->FinishCopy(files_with_errors_.isEmpty());
151 if (eject_after_) destination_->Eject();
152
153 task_manager_->SetTaskFinished(task_id_);
154
155 emit Finished(files_with_errors_, log_);
156
157 // Move back to the original thread so deleteLater() can get called in the main thread's event loop
158 moveToThread(original_thread_);
159 deleteLater();
160
161 // Stop this thread
162 thread_->quit();
163 finished_ = true;
164 return;
165 }
166
167 // We process files in batches so we can be cancelled part-way through.
168 for (int i = 0; i < kBatchSize; ++i) {
169 SetSongProgress(0);
170
171 if (tasks_pending_.isEmpty()) break;
172
173 Task task = tasks_pending_.takeFirst();
174 qLog(Info) << "Processing" << task.song_info_.song_.url().toLocalFile();
175
176 // Use a Song instead of a tag reader
177 Song song = task.song_info_.song_;
178 if (!song.is_valid()) continue;
179
180 // Get embedded album cover
181 QImage cover = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(task.song_info_.song_.url().toLocalFile());
182 if (!cover.isNull()) song.set_image(cover);
183
184 #ifdef HAVE_GSTREAMER
185 // Maybe this file is one that's been transcoded already?
186 if (!task.transcoded_filename_.isEmpty()) {
187 qLog(Debug) << "This file has already been transcoded";
188
189 // Set the new filetype on the song so the formatter gets it right
190 song.set_filetype(task.new_filetype_);
191
192 // Fiddle the filename extension as well to match the new type
193 song.set_url(QUrl::fromLocalFile(Utilities::FiddleFileExtension(song.basefilename(), task.new_extension_)));
194 song.set_basefilename(Utilities::FiddleFileExtension(song.basefilename(), task.new_extension_));
195 task.song_info_.new_filename_ = Utilities::FiddleFileExtension(task.song_info_.new_filename_, task.new_extension_);
196
197 // Have to set this to the size of the new file or else funny stuff happens
198 song.set_filesize(QFileInfo(task.transcoded_filename_).size());
199 }
200 else {
201 // Figure out if we need to transcode it
202 Song::FileType dest_type = CheckTranscode(song.filetype());
203 if (dest_type != Song::FileType_Unknown) {
204 // Get the preset
205 TranscoderPreset preset = Transcoder::PresetForFileType(dest_type);
206 qLog(Debug) << "Transcoding with" << preset.name_;
207
208 task.transcoded_filename_ = transcoder_->GetFile(task.song_info_.song_.url().toLocalFile(), preset);
209 task.new_extension_ = preset.extension_;
210 task.new_filetype_ = dest_type;
211 tasks_transcoding_[task.song_info_.song_.url().toLocalFile()] = task;
212 qLog(Debug) << "Transcoding to" << task.transcoded_filename_;
213
214 // Start the transcoding - this will happen in the background and FileTranscoded() will get called when it's done.
215 // At that point the task will get re-added to the pending queue with the new filename.
216 transcoder_->AddJob(task.song_info_.song_.url().toLocalFile(), preset, task.transcoded_filename_);
217 transcoder_->Start();
218 continue;
219 }
220 }
221 #endif
222
223 MusicStorage::CopyJob job;
224 job.source_ = task.transcoded_filename_.isEmpty() ? task.song_info_.song_.url().toLocalFile() : task.transcoded_filename_;
225 job.destination_ = task.song_info_.new_filename_;
226 job.metadata_ = song;
227 job.overwrite_ = overwrite_;
228 job.mark_as_listened_ = mark_as_listened_;
229 job.albumcover_ = albumcover_;
230 job.remove_original_ = !copy_;
231 job.playlist_ = playlist_;
232
233 if (task.song_info_.song_.art_manual_is_valid() && !task.song_info_.song_.has_manually_unset_cover()) {
234 if (task.song_info_.song_.art_manual().isLocalFile() && QFile::exists(task.song_info_.song_.art_manual().toLocalFile())) {
235 job.cover_source_ = task.song_info_.song_.art_manual().toLocalFile();
236 }
237 else if (task.song_info_.song_.art_manual().scheme().isEmpty() && QFile::exists(task.song_info_.song_.art_manual().path())) {
238 job.cover_source_ = task.song_info_.song_.art_manual().path();
239 }
240 }
241 else if (task.song_info_.song_.art_automatic_is_valid() && !task.song_info_.song_.has_embedded_cover()) {
242 if (task.song_info_.song_.art_automatic().isLocalFile() && QFile::exists(task.song_info_.song_.art_automatic().toLocalFile())) {
243 job.cover_source_ = task.song_info_.song_.art_automatic().toLocalFile();
244 }
245 else if (task.song_info_.song_.art_automatic().scheme().isEmpty() && QFile::exists(task.song_info_.song_.art_automatic().path())) {
246 job.cover_source_ = task.song_info_.song_.art_automatic().path();
247 }
248 }
249 if (!job.cover_source_.isEmpty()) {
250 job.cover_dest_ = QFileInfo(job.destination_).path() + "/" + QFileInfo(job.cover_source_).fileName();
251 }
252
253 job.progress_ = std::bind(&Organize::SetSongProgress, this, std::placeholders::_1, !task.transcoded_filename_.isEmpty());
254
255 if (!destination_->CopyToStorage(job)) {
256 files_with_errors_ << task.song_info_.song_.basefilename();
257 }
258 else {
259 if (job.remove_original_) {
260 // Notify other aspects of system that song has been invalidated
261 QString root = destination_->LocalPath();
262 QFileInfo new_file = QFileInfo(root + "/" + task.song_info_.new_filename_);
263 emit SongPathChanged(song, new_file, destination_->collection_directory_id());
264 }
265 if (job.mark_as_listened_) {
266 emit FileCopied(job.metadata_.id());
267 }
268 }
269
270 // Clean up the temporary transcoded file
271 if (!task.transcoded_filename_.isEmpty()) {
272 QFile::remove(task.transcoded_filename_);
273 }
274
275 tasks_complete_++;
276 }
277 SetSongProgress(0);
278
279 if (!process_files_timer_->isActive()) {
280 process_files_timer_->start();
281 }
282
283
284 }
285
286 #ifdef HAVE_GSTREAMER
CheckTranscode(Song::FileType original_type) const287 Song::FileType Organize::CheckTranscode(Song::FileType original_type) const {
288
289 if (original_type == Song::FileType_Stream) return Song::FileType_Unknown;
290
291 const MusicStorage::TranscodeMode mode = destination_->GetTranscodeMode();
292 const Song::FileType format = destination_->GetTranscodeFormat();
293
294 switch (mode) {
295 case MusicStorage::Transcode_Never:
296 return Song::FileType_Unknown;
297
298 case MusicStorage::Transcode_Always:
299 if (original_type == format) return Song::FileType_Unknown;
300 return format;
301
302 case MusicStorage::Transcode_Unsupported:
303 if (supported_filetypes_.isEmpty() || supported_filetypes_.contains(original_type)) return Song::FileType_Unknown;
304
305 if (format != Song::FileType_Unknown) return format;
306
307 // The user hasn't visited the device properties page yet to set a preferred format for the device, so we have to pick the best available one.
308 return Transcoder::PickBestFormat(supported_filetypes_);
309 }
310 return Song::FileType_Unknown;
311
312 }
313 #endif
314
SetSongProgress(float progress,bool transcoded)315 void Organize::SetSongProgress(float progress, bool transcoded) {
316
317 const int max = transcoded ? 50 : 100;
318 current_copy_progress_ = (transcoded ? 50 : 0) + qBound(0, static_cast<int>(progress * static_cast<float>(max)), max - 1);
319 UpdateProgress();
320
321 }
322
UpdateProgress()323 void Organize::UpdateProgress() {
324
325 const int total = task_count_ * 100;
326
327 #ifdef HAVE_GSTREAMER
328 // Update transcoding progress
329 QMap<QString, float> transcode_progress = transcoder_->GetProgress();
330 QStringList filenames = transcode_progress.keys();
331 for (const QString &filename : filenames) {
332 if (!tasks_transcoding_.contains(filename)) continue;
333 tasks_transcoding_[filename].transcode_progress_ = transcode_progress[filename];
334 }
335 #endif
336
337 // Count the progress of all tasks that are in the queue.
338 // Files that need transcoding total 50 for the transcode and 50 for the copy, files that only need to be copied total 100.
339 int progress = tasks_complete_ * 100;
340
341 for (const Task &task : tasks_pending_) {
342 progress += qBound(0, static_cast<int>(task.transcode_progress_ * 50), 50);
343 }
344 #ifdef HAVE_GSTREAMER
345 QList<Task> tasks_transcoding = tasks_transcoding_.values();
346 for (const Task &task : tasks_transcoding) {
347 progress += qBound(0, static_cast<int>(task.transcode_progress_ * 50), 50);
348 }
349 #endif
350
351 // Add the progress of the track that's currently copying
352 progress += current_copy_progress_;
353
354 task_manager_->SetTaskProgress(task_id_, progress, total);
355
356 }
357
FileTranscoded(const QString & input,const QString & output,bool success)358 void Organize::FileTranscoded(const QString &input, const QString &output, bool success) {
359
360 Q_UNUSED(output);
361
362 qLog(Info) << "File finished" << input << success;
363 transcode_progress_timer_.stop();
364
365 Task task = tasks_transcoding_.take(input);
366 if (!success) {
367 files_with_errors_ << input;
368 }
369 else {
370 tasks_pending_ << task;
371 }
372
373 if (!process_files_timer_->isActive()) {
374 process_files_timer_->start();
375 }
376
377 }
378
timerEvent(QTimerEvent * e)379 void Organize::timerEvent(QTimerEvent *e) {
380
381 QObject::timerEvent(e);
382
383 #ifdef HAVE_GSTREAMER
384 if (e->timerId() == transcode_progress_timer_.timerId()) {
385 UpdateProgress();
386 }
387 #endif
388
389 }
390
LogLine(const QString & message)391 void Organize::LogLine(const QString &message) {
392
393 QString date(QDateTime::currentDateTime().toString(Qt::TextDate));
394 log_.append(QString("%1: %2").arg(date, message));
395
396 }
397