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