Transcode files when copying them to devices

This commit is contained in:
David Sansome 2010-08-29 19:22:21 +00:00
parent eae74f6ad8
commit 4b381e00fd
13 changed files with 214 additions and 59 deletions

View File

@ -36,6 +36,13 @@ public:
Role_FreeSpace, Role_FreeSpace,
}; };
// Values are saved in the database - don't change
enum TranscodeMode {
Transcode_Always = 1,
Transcode_Never = 2,
Transcode_Unsupported = 3,
};
typedef boost::function<void (float progress)> ProgressFunction; typedef boost::function<void (float progress)> ProgressFunction;
struct CopyJob { struct CopyJob {
@ -53,6 +60,10 @@ public:
virtual QString LocalPath() const { return QString(); } virtual QString LocalPath() const { return QString(); }
virtual TranscodeMode GetTranscodeMode() const { return Transcode_Never; }
virtual Song::FileType GetTranscodeFormat() const { return Song::Type_Unknown; }
virtual QList<Song::FileType> SupportedFiletypes() { return QList<Song::FileType>(); }
virtual void StartCopy() {} virtual void StartCopy() {}
virtual bool CopyToStorage(const CopyJob& job) = 0; virtual bool CopyToStorage(const CopyJob& job) = 0;
virtual void FinishCopy(bool success) {} virtual void FinishCopy(bool success) {}

View File

@ -33,18 +33,24 @@ Organise::Organise(TaskManager* task_manager,
const QStringList& files, bool eject_after) const QStringList& files, bool eject_after)
: thread_(NULL), : thread_(NULL),
task_manager_(task_manager), task_manager_(task_manager),
transcoder_(new Transcoder(this)),
destination_(destination), destination_(destination),
format_(format), format_(format),
copy_(copy), copy_(copy),
overwrite_(overwrite), overwrite_(overwrite),
files_(files),
eject_after_(eject_after), eject_after_(eject_after),
task_count_(files.count()),
transcode_suffix_(1),
started_(false), started_(false),
task_id_(0), task_id_(0),
progress_(0), progress_(0),
song_progress_(0) song_progress_(0)
{ {
original_thread_ = thread(); original_thread_ = thread();
foreach (const QString& filename, files) {
tasks_pending_ << Task(filename);
}
} }
void Organise::Start() { void Organise::Start() {
@ -56,6 +62,7 @@ void Organise::Start() {
thread_ = new QThread; thread_ = new QThread;
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles())); connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
connect(transcoder_, SIGNAL(JobComplete(QString,bool)), SLOT(FileTranscoded(QString,bool)));
moveToThread(thread_); moveToThread(thread_);
thread_->start(); thread_->start();
@ -63,12 +70,21 @@ void Organise::Start() {
void Organise::ProcessSomeFiles() { void Organise::ProcessSomeFiles() {
if (!started_) { if (!started_) {
transcode_temp_name_.open();
supported_filetypes_ = destination_->SupportedFiletypes();
destination_->StartCopy(); destination_->StartCopy();
started_ = true; started_ = true;
} }
// None left? // None left?
if (progress_ >= files_.count()) { if (tasks_pending_.isEmpty()) {
if (!tasks_transcoding_.isEmpty()) {
// Just wait - FileTranscoded will start us off again in a little while
qDebug() << "Waiting for transcoding jobs";
return;
}
UpdateProgress(); UpdateProgress();
destination_->FinishCopy(files_with_errors_.isEmpty()); destination_->FinishCopy(files_with_errors_.isEmpty());
@ -89,34 +105,75 @@ void Organise::ProcessSomeFiles() {
return; return;
} }
QDir dir;
// We process files in batches so we can be cancelled part-way through. // We process files in batches so we can be cancelled part-way through.
for (int i=0 ; i<kBatchSize ; ++i) {
const int n = qMin(files_.count(), progress_ + kBatchSize);
for ( ; progress_<n ; ++progress_) {
SetSongProgress(0); SetSongProgress(0);
const QString filename = files_[progress_]; if (tasks_pending_.isEmpty())
break;
Task task = tasks_pending_.takeFirst();
qDebug() << "Processing" << task.filename_;
// Is it a directory? // Is it a directory?
if (QFileInfo(filename).isDir()) { if (QFileInfo(task.filename_).isDir()) {
QDir dir(filename); QDir dir(task.filename_);
foreach (const QString& entry, dir.entryList( foreach (const QString& entry, dir.entryList(
QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)) { QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)) {
files_ << filename + "/" + entry; tasks_pending_ << Task(task.filename_ + "/" + entry);
task_count_ ++;
} }
continue; continue;
} }
// Read metadata from the file // Read metadata from the file
Song song; Song song;
song.InitFromFile(filename, -1); song.InitFromFile(task.filename_, -1);
if (!song.is_valid()) if (!song.is_valid())
continue; continue;
// Maybe this file is one that's been transcoded already?
if (!task.transcoded_filename_.isEmpty()) {
qDebug() << "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_filename(song.filename().section('.', 0, -2) + "." + task.new_extension_);
song.set_basefilename(song.basefilename().section('.', 0, -2) + "." + 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());
if (dest_type != Song::Type_Unknown) {
// Get the preset
TranscoderPreset preset = Transcoder::PresetForFileType(dest_type);
qDebug() << "Transcoding with" << preset.name_;
// Get a temporary name for the transcoded file
task.transcoded_filename_ = transcode_temp_name_.fileName() + "-" +
QString::number(transcode_suffix_++);
task.new_extension_ = preset.extension_;
task.new_filetype_ = dest_type;
tasks_transcoding_[task.filename_] = task;
qDebug() << "Transcoding to" << task.transcoded_filename_;
// 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.
transcoder_->AddJob(task.filename_, preset, task.transcoded_filename_);
transcoder_->Start();
continue;
}
}
MusicStorage::CopyJob job; MusicStorage::CopyJob job;
job.source_ = filename; job.source_ = task.transcoded_filename_.isEmpty() ?
task.filename_ : task.transcoded_filename_;
job.destination_ = format_.GetFilenameForSong(song); job.destination_ = format_.GetFilenameForSong(song);
job.metadata_ = song; job.metadata_ = song;
job.overwrite_ = overwrite_; job.overwrite_ = overwrite_;
@ -124,14 +181,47 @@ void Organise::ProcessSomeFiles() {
job.progress_ = boost::bind(&Organise::SetSongProgress, this, _1); job.progress_ = boost::bind(&Organise::SetSongProgress, this, _1);
if (!destination_->CopyToStorage(job)) { if (!destination_->CopyToStorage(job)) {
files_with_errors_ << filename; files_with_errors_ << task.filename_;
} }
progress_++;
} }
SetSongProgress(0); SetSongProgress(0);
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles())); QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
} }
Song::FileType Organise::CheckTranscode(Song::FileType original_type) const {
if (original_type == Song::Type_Stream)
return Song::Type_Unknown;
const MusicStorage::TranscodeMode mode = destination_->GetTranscodeMode();
const Song::FileType format = destination_->GetTranscodeFormat();
switch (mode) {
case MusicStorage::Transcode_Never:
return Song::Type_Unknown;
case MusicStorage::Transcode_Always:
if (original_type == format)
return Song::Type_Unknown;
return format;
case MusicStorage::Transcode_Unsupported:
if (supported_filetypes_.isEmpty() || supported_filetypes_.contains(original_type))
return Song::Type_Unknown;
if (format != Song::Type_Unknown)
return format;
// 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.
return Transcoder::PickBestFormat(supported_filetypes_);
}
return Song::Type_Unknown;
}
void Organise::SetSongProgress(float progress) { void Organise::SetSongProgress(float progress) {
song_progress_ = qBound(0, int(progress * 100), 99); song_progress_ = qBound(0, int(progress * 100), 99);
UpdateProgress(); UpdateProgress();
@ -139,6 +229,18 @@ void Organise::SetSongProgress(float progress) {
void Organise::UpdateProgress() { void Organise::UpdateProgress() {
const int progress = progress_ * 100 + song_progress_; const int progress = progress_ * 100 + song_progress_;
const int total = files_.count() * 100; const int total = task_count_ * 100;
task_manager_->SetTaskProgress(task_id_, progress, total); task_manager_->SetTaskProgress(task_id_, progress, total);
} }
void Organise::FileTranscoded(const QString& filename, bool success) {
qDebug() << "File finished" << filename << success;
Task task = tasks_transcoding_.take(filename);
if (!success) {
files_with_errors_ << filename;
} else {
tasks_pending_ << task;
}
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
}

View File

@ -18,10 +18,12 @@
#define ORGANISE_H #define ORGANISE_H
#include <QObject> #include <QObject>
#include <QTemporaryFile>
#include <boost/shared_ptr.hpp> #include <boost/shared_ptr.hpp>
#include "organiseformat.h" #include "organiseformat.h"
#include "transcoder/transcoder.h"
class MusicStorage; class MusicStorage;
class TaskManager; class TaskManager;
@ -44,20 +46,41 @@ signals:
private slots: private slots:
void ProcessSomeFiles(); void ProcessSomeFiles();
void SetSongProgress(float progress); void FileTranscoded(const QString& filename, bool success);
void UpdateProgress();
private: private:
void SetSongProgress(float progress);
void UpdateProgress();
Song::FileType CheckTranscode(Song::FileType original_type) const;
private:
struct Task {
Task(const QString& filename = QString()) : filename_(filename) {}
QString filename_;
QString transcoded_filename_;
QString new_extension_;
Song::FileType new_filetype_;
};
QThread* thread_; QThread* thread_;
QThread* original_thread_; QThread* original_thread_;
TaskManager* task_manager_; TaskManager* task_manager_;
Transcoder* transcoder_;
boost::shared_ptr<MusicStorage> destination_; boost::shared_ptr<MusicStorage> destination_;
QList<Song::FileType> supported_filetypes_;
const OrganiseFormat format_; const OrganiseFormat format_;
const bool copy_; const bool copy_;
const bool overwrite_; const bool overwrite_;
QStringList files_;
const bool eject_after_; const bool eject_after_;
int task_count_;
QTemporaryFile transcode_temp_name_;
int transcode_suffix_;
QList<Task> tasks_pending_;
QMap<QString, Task> tasks_transcoding_;
bool started_; bool started_;

View File

@ -94,3 +94,15 @@ void ConnectedDevice::FinishCopy(bool) {
void ConnectedDevice::FinishDelete(bool) { void ConnectedDevice::FinishDelete(bool) {
lister_->UpdateDeviceFreeSpace(unique_id_); lister_->UpdateDeviceFreeSpace(unique_id_);
} }
MusicStorage::TranscodeMode ConnectedDevice::GetTranscodeMode() const {
int index = manager_->FindDeviceById(unique_id_);
return MusicStorage::TranscodeMode(
manager_->index(index).data(DeviceManager::Role_TranscodeMode).toInt());
}
Song::FileType ConnectedDevice::GetTranscodeFormat() const {
int index = manager_->FindDeviceById(unique_id_);
return Song::FileType(
manager_->index(index).data(DeviceManager::Role_TranscodeFormat).toInt());
}

View File

@ -44,7 +44,8 @@ public:
virtual void Init() = 0; virtual void Init() = 0;
virtual QList<Song::FileType> SupportedFiletypes() { return QList<Song::FileType>(); } virtual TranscodeMode GetTranscodeMode() const;
virtual Song::FileType GetTranscodeFormat() const;
DeviceLister* lister() const { return lister_; } DeviceLister* lister() const { return lister_; }
QString unique_id() const { return unique_id_; } QString unique_id() const { return unique_id_; }

View File

@ -52,7 +52,7 @@ DeviceDatabaseBackend::DeviceList DeviceDatabaseBackend::GetAllDevices() {
dev.friendly_name_ = q.value(2).toString(); dev.friendly_name_ = q.value(2).toString();
dev.size_ = q.value(3).toLongLong(); dev.size_ = q.value(3).toLongLong();
dev.icon_name_ = q.value(4).toString(); dev.icon_name_ = q.value(4).toString();
dev.transcode_mode_ = TranscodeMode(q.value(5).toInt()); dev.transcode_mode_ = MusicStorage::TranscodeMode(q.value(5).toInt());
dev.transcode_format_ = Song::FileType(q.value(6).toInt()); dev.transcode_format_ = Song::FileType(q.value(6).toInt());
ret << dev; ret << dev;
} }
@ -118,7 +118,7 @@ void DeviceDatabaseBackend::RemoveDevice(int id) {
void DeviceDatabaseBackend::SetDeviceOptions(int id, void DeviceDatabaseBackend::SetDeviceOptions(int id,
const QString &friendly_name, const QString &icon_name, const QString &friendly_name, const QString &icon_name,
TranscodeMode mode, Song::FileType format) { MusicStorage::TranscodeMode mode, Song::FileType format) {
QMutexLocker l(db_->Mutex()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());

View File

@ -21,6 +21,7 @@
#include <boost/shared_ptr.hpp> #include <boost/shared_ptr.hpp>
#include "core/musicstorage.h"
#include "core/song.h" #include "core/song.h"
class Database; class Database;
@ -31,13 +32,6 @@ class DeviceDatabaseBackend : public QObject {
public: public:
Q_INVOKABLE DeviceDatabaseBackend(QObject* parent = 0); Q_INVOKABLE DeviceDatabaseBackend(QObject* parent = 0);
// Values are saved in the database - don't change
enum TranscodeMode {
Transcode_Always = 1,
Transcode_Never = 2,
Transcode_Unsupported = 3,
};
struct Device { struct Device {
Device() : id_(-1) {} Device() : id_(-1) {}
@ -47,7 +41,7 @@ public:
quint64 size_; quint64 size_;
QString icon_name_; QString icon_name_;
TranscodeMode transcode_mode_; MusicStorage::TranscodeMode transcode_mode_;
Song::FileType transcode_format_; Song::FileType transcode_format_;
}; };
typedef QList<Device> DeviceList; typedef QList<Device> DeviceList;
@ -63,7 +57,7 @@ public:
void SetDeviceOptions(int id, void SetDeviceOptions(int id,
const QString& friendly_name, const QString& icon_name, const QString& friendly_name, const QString& icon_name,
TranscodeMode mode, Song::FileType format); MusicStorage::TranscodeMode mode, Song::FileType format);
private: private:
boost::shared_ptr<Database> db_; boost::shared_ptr<Database> db_;

View File

@ -60,7 +60,7 @@ const int DeviceManager::kDeviceIconOverlaySize = 16;
DeviceManager::DeviceInfo::DeviceInfo() DeviceManager::DeviceInfo::DeviceInfo()
: database_id_(-1), : database_id_(-1),
transcode_mode_(DeviceDatabaseBackend::Transcode_Unsupported), transcode_mode_(MusicStorage::Transcode_Unsupported),
transcode_format_(Song::Type_Unknown), transcode_format_(Song::Type_Unknown),
task_percentage_(-1) task_percentage_(-1)
{ {
@ -618,7 +618,7 @@ void DeviceManager::Forget(int row) {
void DeviceManager::SetDeviceOptions(int row, void DeviceManager::SetDeviceOptions(int row,
const QString& friendly_name, const QString& icon_name, const QString& friendly_name, const QString& icon_name,
DeviceDatabaseBackend::TranscodeMode mode, Song::FileType format) { MusicStorage::TranscodeMode mode, Song::FileType format) {
DeviceInfo& info = devices_[row]; DeviceInfo& info = devices_[row];
info.friendly_name_ = friendly_name; info.friendly_name_ = friendly_name;
info.LoadIcon(QVariantList() << icon_name, friendly_name); info.LoadIcon(QVariantList() << icon_name, friendly_name);
@ -676,6 +676,8 @@ void DeviceManager::TasksChanged() {
DeviceInfo& info = devices_[index.row()]; DeviceInfo& info = devices_[index.row()];
info.task_percentage_ = -1; info.task_percentage_ = -1;
emit dataChanged(index, index); emit dataChanged(index, index);
active_tasks_.remove(active_tasks_.key(index));
} }
} }

View File

@ -85,7 +85,7 @@ public:
void SetDeviceOptions(int row, void SetDeviceOptions(int row,
const QString& friendly_name, const QString& icon_name, const QString& friendly_name, const QString& icon_name,
DeviceDatabaseBackend::TranscodeMode mode, Song::FileType format); MusicStorage::TranscodeMode mode, Song::FileType format);
// QAbstractListModel // QAbstractListModel
int rowCount(const QModelIndex &parent) const; int rowCount(const QModelIndex &parent) const;
@ -151,7 +151,7 @@ private:
QString icon_name_; QString icon_name_;
QIcon icon_; QIcon icon_;
DeviceDatabaseBackend::TranscodeMode transcode_mode_; MusicStorage::TranscodeMode transcode_mode_;
Song::FileType transcode_format_; Song::FileType transcode_format_;
int task_percentage_; int task_percentage_;

View File

@ -187,18 +187,18 @@ void DeviceProperties::UpdateFormats() {
manager_->GetConnectedDevice(index_.row()); manager_->GetConnectedDevice(index_.row());
// Transcode mode // Transcode mode
DeviceDatabaseBackend::TranscodeMode mode = DeviceDatabaseBackend::TranscodeMode( MusicStorage::TranscodeMode mode = MusicStorage::TranscodeMode(
index_.data(DeviceManager::Role_TranscodeMode).toInt()); index_.data(DeviceManager::Role_TranscodeMode).toInt());
switch (mode) { switch (mode) {
case DeviceDatabaseBackend::Transcode_Always: case MusicStorage::Transcode_Always:
ui_->transcode_all->setChecked(true); ui_->transcode_all->setChecked(true);
break; break;
case DeviceDatabaseBackend::Transcode_Never: case MusicStorage::Transcode_Never:
ui_->transcode_off->setChecked(true); ui_->transcode_off->setChecked(true);
break; break;
case DeviceDatabaseBackend::Transcode_Unsupported: case MusicStorage::Transcode_Unsupported:
default: default:
ui_->transcode_unsupported->setChecked(true); ui_->transcode_unsupported->setChecked(true);
break; break;
@ -238,13 +238,13 @@ void DeviceProperties::accept() {
QDialog::accept(); QDialog::accept();
// Transcode mode // Transcode mode
DeviceDatabaseBackend::TranscodeMode mode = DeviceDatabaseBackend::Transcode_Unsupported; MusicStorage::TranscodeMode mode = MusicStorage::Transcode_Unsupported;
if (ui_->transcode_all->isChecked()) if (ui_->transcode_all->isChecked())
mode = DeviceDatabaseBackend::Transcode_Always; mode = MusicStorage::Transcode_Always;
else if (ui_->transcode_off->isChecked()) else if (ui_->transcode_off->isChecked())
mode = DeviceDatabaseBackend::Transcode_Never; mode = MusicStorage::Transcode_Never;
else if (ui_->transcode_unsupported->isChecked()) else if (ui_->transcode_unsupported->isChecked())
mode = DeviceDatabaseBackend::Transcode_Unsupported; mode = MusicStorage::Transcode_Unsupported;
// Transcode format // Transcode format
Song::FileType format = Song::FileType(ui_->transcode_format->itemData( Song::FileType format = Song::FileType(ui_->transcode_format->itemData(
@ -288,23 +288,7 @@ void DeviceProperties::UpdateFormatsFinished() {
if (preset.type_ == Song::Type_Unknown) { if (preset.type_ == Song::Type_Unknown) {
// The user hasn't chosen a format for this device yet, so work our way down // The user hasn't chosen a format for this device yet, so work our way down
// a list of some preferred formats, picking the first one that is supported // a list of some preferred formats, picking the first one that is supported
QList<Song::FileType> best_formats; preset = Transcoder::PresetForFileType(Transcoder::PickBestFormat(list));
best_formats << Song::Type_Mpeg;
best_formats << Song::Type_OggVorbis;
best_formats << Song::Type_Asf;
foreach (Song::FileType type, best_formats) {
if (list.isEmpty() || list.contains(type)) {
preset = Transcoder::PresetForFileType(type);
break;
}
}
if (preset.type_ == Song::Type_Unknown) {
// Still haven't found a good format - pick the first one that the
// device advertises.
preset = Transcoder::PresetForFileType(list[0]);
}
} }
ui_->transcode_format->setCurrentIndex(ui_->transcode_format->findText(preset.name_)); ui_->transcode_format->setCurrentIndex(ui_->transcode_format->findText(preset.name_));

View File

@ -84,6 +84,9 @@ static int ProgressCallback(uint64_t const sent, uint64_t const total,
} }
bool MtpDevice::CopyToStorage(const CopyJob& job) { bool MtpDevice::CopyToStorage(const CopyJob& job) {
if (!connection_->is_valid())
return false;
// Convert metadata // Convert metadata
LIBMTP_track_t track; LIBMTP_track_t track;
job.metadata_.ToMTP(&track); job.metadata_.ToMTP(&track);
@ -165,6 +168,11 @@ QList<Song::FileType> MtpDevice::SupportedFiletypes() {
QMutexLocker l(&db_busy_); QMutexLocker l(&db_busy_);
MtpConnection connection(url_.host()); MtpConnection connection(url_.host());
if (!connection.is_valid()) {
qWarning() << "Error connecting to MTP device, couldn't get list of supported filetypes";
return ret;
}
if (LIBMTP_Get_Supported_Filetypes(connection.device(), &list, &length) if (LIBMTP_Get_Supported_Filetypes(connection.device(), &list, &length)
|| !list || !length) || !list || !length)
return ret; return ret;

View File

@ -206,6 +206,23 @@ TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
} }
} }
Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
if (supported.isEmpty())
return Song::Type_Unknown;
QList<Song::FileType> best_formats;
best_formats << Song::Type_Mpeg;
best_formats << Song::Type_OggVorbis;
best_formats << Song::Type_Asf;
foreach (Song::FileType type, best_formats) {
if (supported.isEmpty() || supported.contains(type))
return type;
}
return supported[0];
}
void Transcoder::AddJob(const QString& input, void Transcoder::AddJob(const QString& input,
const TranscoderPreset& preset, const TranscoderPreset& preset,
const QString& output) { const QString& output) {

View File

@ -55,6 +55,7 @@ class Transcoder : public QObject {
static TranscoderPreset PresetForFileType(Song::FileType type); static TranscoderPreset PresetForFileType(Song::FileType type);
static QList<TranscoderPreset> GetAllPresets(); static QList<TranscoderPreset> GetAllPresets();
static Song::FileType PickBestFormat(QList<Song::FileType> supported);
int max_threads() const { return max_threads_; } int max_threads() const { return max_threads_; }