2018-02-27 18:06:05 +01:00
|
|
|
/*
|
|
|
|
* Strawberry Music Player
|
|
|
|
* This file was part of Clementine.
|
|
|
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
2019-01-26 17:18:26 +01:00
|
|
|
* Copyright 2018-2019, Jonas Kvinge <jonas@jkvinge.net>
|
2018-02-27 18:06:05 +01:00
|
|
|
*
|
|
|
|
* Strawberry is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Strawberry is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
2018-08-09 18:39:44 +02:00
|
|
|
*
|
2018-02-27 18:06:05 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include <functional>
|
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QtGlobal>
|
|
|
|
#include <QThread>
|
|
|
|
#include <QFile>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <QFileInfo>
|
|
|
|
#include <QTimer>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QString>
|
|
|
|
#include <QStringBuilder>
|
2018-02-27 18:06:05 +01:00
|
|
|
#include <QUrl>
|
2019-01-24 19:20:10 +01:00
|
|
|
#include <QStandardPaths>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QtDebug>
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
#include "core/logging.h"
|
2018-12-29 15:37:16 +01:00
|
|
|
#include "core/utilities.h"
|
|
|
|
#include "core/taskmanager.h"
|
|
|
|
#include "core/musicstorage.h"
|
2019-01-24 19:20:10 +01:00
|
|
|
#include "core/tagreaderclient.h"
|
2019-07-07 21:14:24 +02:00
|
|
|
#include "core/song.h"
|
2018-05-01 00:41:33 +02:00
|
|
|
#include "organise.h"
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
|
|
|
# include "transcoder/transcoder.h"
|
|
|
|
#endif
|
2018-05-01 00:41:33 +02:00
|
|
|
|
|
|
|
class OrganiseFormat;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
using std::placeholders::_1;
|
|
|
|
|
|
|
|
const int Organise::kBatchSize = 10;
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
const int Organise::kTranscodeProgressInterval = 500;
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2019-08-07 17:13:40 +02:00
|
|
|
Organise::Organise(TaskManager *task_manager, std::shared_ptr<MusicStorage> destination, const OrganiseFormat &format, bool copy, bool overwrite, bool mark_as_listened, bool albumcover, const NewSongInfoList &songs_info, bool eject_after, const QString &playlist)
|
2018-02-27 18:06:05 +01:00
|
|
|
: thread_(nullptr),
|
|
|
|
task_manager_(task_manager),
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
transcoder_(new Transcoder(this)),
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
destination_(destination),
|
|
|
|
format_(format),
|
|
|
|
copy_(copy),
|
|
|
|
overwrite_(overwrite),
|
|
|
|
mark_as_listened_(mark_as_listened),
|
2019-01-26 17:18:26 +01:00
|
|
|
albumcover_(albumcover),
|
2018-02-27 18:06:05 +01:00
|
|
|
eject_after_(eject_after),
|
|
|
|
task_count_(songs_info.count()),
|
2019-08-07 17:13:40 +02:00
|
|
|
playlist_(playlist),
|
2018-02-27 18:06:05 +01:00
|
|
|
tasks_complete_(0),
|
|
|
|
started_(false),
|
|
|
|
task_id_(0),
|
2019-08-07 17:13:40 +02:00
|
|
|
current_copy_progress_(0){
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
original_thread_ = thread();
|
|
|
|
|
|
|
|
for (const NewSongInfo &song_info : songs_info) {
|
|
|
|
tasks_pending_ << Task(song_info);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-07-19 19:56:37 +02:00
|
|
|
Organise::~Organise() {
|
|
|
|
if (thread_) {
|
|
|
|
thread_->quit();
|
|
|
|
thread_->deleteLater();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
void Organise::Start() {
|
|
|
|
|
|
|
|
if (thread_) return;
|
|
|
|
|
|
|
|
task_id_ = task_manager_->StartTask(tr("Organising files"));
|
|
|
|
task_manager_->SetTaskBlocksCollectionScans(true);
|
|
|
|
|
|
|
|
thread_ = new QThread;
|
|
|
|
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)), SLOT(FileTranscoded(QString, QString, bool)));
|
2019-01-24 19:20:10 +01:00
|
|
|
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
moveToThread(thread_);
|
|
|
|
thread_->start();
|
2019-07-19 19:56:37 +02:00
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Organise::ProcessSomeFiles() {
|
|
|
|
|
|
|
|
if (!started_) {
|
|
|
|
if (!destination_->StartCopy(&supported_filetypes_)) {
|
|
|
|
// Failed to start - mark everything as failed :(
|
|
|
|
for (const Task &task : tasks_pending_) files_with_errors_ << task.song_info_.song_.url().toLocalFile();
|
|
|
|
tasks_pending_.clear();
|
|
|
|
}
|
|
|
|
started_ = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// None left?
|
|
|
|
if (tasks_pending_.isEmpty()) {
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
if (!tasks_transcoding_.isEmpty()) {
|
|
|
|
// Just wait - FileTranscoded will start us off again in a little while
|
|
|
|
qLog(Debug) << "Waiting for transcoding jobs";
|
|
|
|
transcode_progress_timer_.start(kTranscodeProgressInterval, this);
|
|
|
|
return;
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
UpdateProgress();
|
|
|
|
|
|
|
|
destination_->FinishCopy(files_with_errors_.isEmpty());
|
|
|
|
if (eject_after_) destination_->Eject();
|
|
|
|
|
|
|
|
task_manager_->SetTaskFinished(task_id_);
|
|
|
|
|
2019-01-24 19:20:10 +01:00
|
|
|
emit Finished(files_with_errors_, log_);
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
// Move back to the original thread so deleteLater() can get called in the main thread's event loop
|
2018-02-27 18:06:05 +01:00
|
|
|
moveToThread(original_thread_);
|
|
|
|
deleteLater();
|
|
|
|
|
|
|
|
// Stop this thread
|
|
|
|
thread_->quit();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We process files in batches so we can be cancelled part-way through.
|
|
|
|
for (int i = 0; i < kBatchSize; ++i) {
|
|
|
|
SetSongProgress(0);
|
|
|
|
|
|
|
|
if (tasks_pending_.isEmpty()) break;
|
|
|
|
|
|
|
|
Task task = tasks_pending_.takeFirst();
|
|
|
|
qLog(Info) << "Processing" << task.song_info_.song_.url().toLocalFile();
|
|
|
|
|
|
|
|
// Use a Song instead of a tag reader
|
|
|
|
Song song = task.song_info_.song_;
|
|
|
|
if (!song.is_valid()) continue;
|
|
|
|
|
2019-01-24 19:20:10 +01:00
|
|
|
// Get embedded album cover
|
|
|
|
QImage cover = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_info_.song_.url().toLocalFile());
|
|
|
|
if (!cover.isNull()) song.set_image(cover);
|
|
|
|
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
// Maybe this file is one that's been transcoded already?
|
|
|
|
if (!task.transcoded_filename_.isEmpty()) {
|
|
|
|
qLog(Debug) << "This file has already been transcoded";
|
|
|
|
|
|
|
|
// Set the new filetype on the song so the formatter gets it right
|
|
|
|
song.set_filetype(task.new_filetype_);
|
|
|
|
|
|
|
|
// Fiddle the filename extension as well to match the new type
|
|
|
|
song.set_url(QUrl::fromLocalFile(Utilities::FiddleFileExtension(song.basefilename(), task.new_extension_)));
|
|
|
|
song.set_basefilename(Utilities::FiddleFileExtension(song.basefilename(), task.new_extension_));
|
|
|
|
|
|
|
|
// Have to set this to the size of the new file or else funny stuff happens
|
|
|
|
song.set_filesize(QFileInfo(task.transcoded_filename_).size());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Figure out if we need to transcode it
|
|
|
|
Song::FileType dest_type = CheckTranscode(song.filetype());
|
2018-09-08 12:38:02 +02:00
|
|
|
if (dest_type != Song::FileType_Unknown) {
|
2018-02-27 18:06:05 +01:00
|
|
|
// Get the preset
|
|
|
|
TranscoderPreset preset = Transcoder::PresetForFileType(dest_type);
|
|
|
|
qLog(Debug) << "Transcoding with" << preset.name_;
|
|
|
|
|
2019-01-26 14:57:25 +01:00
|
|
|
task.transcoded_filename_ = transcoder_->GetFile(task.song_info_.song_.url().toLocalFile(), preset);
|
2018-02-27 18:06:05 +01:00
|
|
|
task.new_extension_ = preset.extension_;
|
|
|
|
task.new_filetype_ = dest_type;
|
|
|
|
tasks_transcoding_[task.song_info_.song_.url().toLocalFile()] = task;
|
|
|
|
qLog(Debug) << "Transcoding to" << task.transcoded_filename_;
|
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
// Start the transcoding - this will happen in the background and FileTranscoded() will get called when it's done.
|
|
|
|
// At that point the task will get re-added to the pending queue with the new filename.
|
2018-02-27 18:06:05 +01:00
|
|
|
transcoder_->AddJob(task.song_info_.song_.url().toLocalFile(), preset, task.transcoded_filename_);
|
|
|
|
transcoder_->Start();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
MusicStorage::CopyJob job;
|
|
|
|
job.source_ = task.transcoded_filename_.isEmpty() ? task.song_info_.song_.url().toLocalFile() : task.transcoded_filename_;
|
|
|
|
job.destination_ = task.song_info_.new_filename_;
|
|
|
|
job.metadata_ = song;
|
|
|
|
job.overwrite_ = overwrite_;
|
|
|
|
job.mark_as_listened_ = mark_as_listened_;
|
2019-01-26 17:18:26 +01:00
|
|
|
job.albumcover_ = albumcover_;
|
2018-02-27 18:06:05 +01:00
|
|
|
job.remove_original_ = !copy_;
|
2019-08-07 17:13:40 +02:00
|
|
|
job.playlist_ = playlist_;
|
2019-01-26 17:18:26 +01:00
|
|
|
|
2019-07-07 21:14:24 +02:00
|
|
|
if (task.song_info_.song_.art_manual_is_valid() && task.song_info_.song_.art_manual().path() != Song::kManuallyUnsetCover) {
|
2019-07-09 21:43:56 +02:00
|
|
|
if (task.song_info_.song_.art_manual().isLocalFile() && QFile::exists(task.song_info_.song_.art_manual().toLocalFile())) {
|
2019-07-07 21:14:24 +02:00
|
|
|
job.cover_source_ = task.song_info_.song_.art_manual().toLocalFile();
|
|
|
|
}
|
|
|
|
else if (task.song_info_.song_.art_manual().scheme().isEmpty() && QFile::exists(task.song_info_.song_.art_manual().path())) {
|
|
|
|
job.cover_source_ = task.song_info_.song_.art_manual().path();
|
|
|
|
}
|
2019-01-26 17:18:26 +01:00
|
|
|
}
|
2019-07-07 21:14:24 +02:00
|
|
|
else if (task.song_info_.song_.art_automatic_is_valid() && task.song_info_.song_.art_automatic().path() != Song::kEmbeddedCover) {
|
2019-07-09 21:43:56 +02:00
|
|
|
if (task.song_info_.song_.art_automatic().isLocalFile() && QFile::exists(task.song_info_.song_.art_automatic().toLocalFile())) {
|
2019-07-07 21:14:24 +02:00
|
|
|
job.cover_source_ = task.song_info_.song_.art_automatic().toLocalFile();
|
|
|
|
}
|
|
|
|
else if (task.song_info_.song_.art_automatic().scheme().isEmpty() && QFile::exists(task.song_info_.song_.art_automatic().path())) {
|
|
|
|
job.cover_source_ = task.song_info_.song_.art_automatic().path();
|
|
|
|
}
|
2019-01-26 17:18:26 +01:00
|
|
|
}
|
|
|
|
if (!job.cover_source_.isEmpty()) {
|
|
|
|
job.cover_dest_ = QFileInfo(job.destination_).path() + "/" + QFileInfo(job.cover_source_).fileName();
|
|
|
|
}
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
job.progress_ = std::bind(&Organise::SetSongProgress, this, _1, !task.transcoded_filename_.isEmpty());
|
|
|
|
|
|
|
|
if (!destination_->CopyToStorage(job)) {
|
|
|
|
files_with_errors_ << task.song_info_.song_.basefilename();
|
2019-01-24 19:20:10 +01:00
|
|
|
}
|
|
|
|
else {
|
2019-03-25 00:53:12 +01:00
|
|
|
if (job.remove_original_) {
|
|
|
|
// Notify other aspects of system that song has been invalidated
|
|
|
|
QString root = destination_->LocalPath();
|
|
|
|
QFileInfo new_file = QFileInfo(root + "/" + task.song_info_.new_filename_);
|
|
|
|
emit SongPathChanged(song, new_file);
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
if (job.mark_as_listened_) {
|
|
|
|
emit FileCopied(job.metadata_.id());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up the temporary transcoded file
|
|
|
|
if (!task.transcoded_filename_.isEmpty())
|
|
|
|
QFile::remove(task.transcoded_filename_);
|
|
|
|
|
|
|
|
tasks_complete_++;
|
|
|
|
}
|
|
|
|
SetSongProgress(0);
|
|
|
|
|
|
|
|
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
Song::FileType Organise::CheckTranscode(Song::FileType original_type) const {
|
|
|
|
|
2018-09-08 12:38:02 +02:00
|
|
|
if (original_type == Song::FileType_Stream) return Song::FileType_Unknown;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
const MusicStorage::TranscodeMode mode = destination_->GetTranscodeMode();
|
|
|
|
const Song::FileType format = destination_->GetTranscodeFormat();
|
|
|
|
|
|
|
|
switch (mode) {
|
|
|
|
case MusicStorage::Transcode_Never:
|
2018-09-08 12:38:02 +02:00
|
|
|
return Song::FileType_Unknown;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
case MusicStorage::Transcode_Always:
|
2018-09-08 12:38:02 +02:00
|
|
|
if (original_type == format) return Song::FileType_Unknown;
|
2018-02-27 18:06:05 +01:00
|
|
|
return format;
|
|
|
|
|
|
|
|
case MusicStorage::Transcode_Unsupported:
|
2018-09-08 12:38:02 +02:00
|
|
|
if (supported_filetypes_.isEmpty() || supported_filetypes_.contains(original_type)) return Song::FileType_Unknown;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2018-09-08 12:38:02 +02:00
|
|
|
if (format != Song::FileType_Unknown) return format;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
// 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.
|
2018-02-27 18:06:05 +01:00
|
|
|
return Transcoder::PickBestFormat(supported_filetypes_);
|
|
|
|
}
|
2018-09-08 12:38:02 +02:00
|
|
|
return Song::FileType_Unknown;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
void Organise::SetSongProgress(float progress, bool transcoded) {
|
|
|
|
|
|
|
|
const int max = transcoded ? 50 : 100;
|
|
|
|
current_copy_progress_ = (transcoded ? 50 : 0) + qBound(0, static_cast<int>(progress * max), max - 1);
|
|
|
|
UpdateProgress();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Organise::UpdateProgress() {
|
|
|
|
|
|
|
|
const int total = task_count_ * 100;
|
|
|
|
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
// Update transcoding progress
|
|
|
|
QMap<QString, float> transcode_progress = transcoder_->GetProgress();
|
|
|
|
for (const QString &filename : transcode_progress.keys()) {
|
|
|
|
if (!tasks_transcoding_.contains(filename)) continue;
|
|
|
|
tasks_transcoding_[filename].transcode_progress_ = transcode_progress[filename];
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
// Count the progress of all tasks that are in the queue.
|
|
|
|
// Files that need transcoding total 50 for the transcode and 50 for the copy, files that only need to be copied total 100.
|
2018-02-27 18:06:05 +01:00
|
|
|
int progress = tasks_complete_ * 100;
|
|
|
|
|
|
|
|
for (const Task &task : tasks_pending_) {
|
|
|
|
progress += qBound(0, static_cast<int>(task.transcode_progress_ * 50), 50);
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
for (const Task &task : tasks_transcoding_.values()) {
|
|
|
|
progress += qBound(0, static_cast<int>(task.transcode_progress_ * 50), 50);
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
// Add the progress of the track that's currently copying
|
|
|
|
progress += current_copy_progress_;
|
|
|
|
|
|
|
|
task_manager_->SetTaskProgress(task_id_, progress, total);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Organise::FileTranscoded(const QString &input, const QString &output, bool success) {
|
|
|
|
|
2019-09-15 20:27:32 +02:00
|
|
|
Q_UNUSED(output);
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
qLog(Info) << "File finished" << input << success;
|
|
|
|
transcode_progress_timer_.stop();
|
|
|
|
|
|
|
|
Task task = tasks_transcoding_.take(input);
|
|
|
|
if (!success) {
|
|
|
|
files_with_errors_ << input;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
tasks_pending_ << task;
|
|
|
|
}
|
|
|
|
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Organise::timerEvent(QTimerEvent *e) {
|
|
|
|
|
|
|
|
QObject::timerEvent(e);
|
|
|
|
|
2019-01-06 16:48:23 +01:00
|
|
|
#ifdef HAVE_GSTREAMER
|
2018-02-27 18:06:05 +01:00
|
|
|
if (e->timerId() == transcode_progress_timer_.timerId()) {
|
|
|
|
UpdateProgress();
|
|
|
|
}
|
2019-01-06 16:48:23 +01:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-01-24 19:20:10 +01:00
|
|
|
void Organise::LogLine(const QString message) {
|
|
|
|
|
|
|
|
QString date(QDateTime::currentDateTime().toString(Qt::TextDate));
|
|
|
|
log_.append(QString("%1: %2").arg(date, message));
|
|
|
|
|
|
|
|
}
|