Merge pull request #4753 from paperbagcorner/ripper
Reorganize the CD ripper
This commit is contained in:
commit
de0f9a4213
@ -1035,14 +1035,16 @@ optional_source(HAVE_AUDIOCD
|
|||||||
devices/cddadevice.cpp
|
devices/cddadevice.cpp
|
||||||
devices/cddalister.cpp
|
devices/cddalister.cpp
|
||||||
devices/cddasongloader.cpp
|
devices/cddasongloader.cpp
|
||||||
ui/ripcd.cpp
|
ripper/ripcddialog.cpp
|
||||||
|
ripper/ripper.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
devices/cddadevice.h
|
devices/cddadevice.h
|
||||||
devices/cddalister.h
|
devices/cddalister.h
|
||||||
devices/cddasongloader.h
|
devices/cddasongloader.h
|
||||||
ui/ripcd.h
|
ripper/ripcddialog.h
|
||||||
|
ripper/ripper.h
|
||||||
UI
|
UI
|
||||||
ui/ripcd.ui
|
ripper/ripcddialog.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
# mtp device
|
# mtp device
|
||||||
|
296
src/ripper/ripcddialog.cpp
Normal file
296
src/ripper/ripcddialog.cpp
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||||
|
|
||||||
|
Clementine 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.
|
||||||
|
|
||||||
|
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ripper/ripcddialog.h"
|
||||||
|
|
||||||
|
#include <cdio/cdio.h>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/tagreaderclient.h"
|
||||||
|
#include "core/utilities.h"
|
||||||
|
#include "ripper/ripper.h"
|
||||||
|
#include "ui_ripcddialog.h"
|
||||||
|
#include "transcoder/transcoder.h"
|
||||||
|
#include "transcoder/transcoderoptionsdialog.h"
|
||||||
|
#include "ui/iconloader.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool ComparePresetsByName(const TranscoderPreset& left,
|
||||||
|
const TranscoderPreset& right) {
|
||||||
|
return left.name_ < right.name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int kCheckboxColumn = 0;
|
||||||
|
const int kTrackNumberColumn = 1;
|
||||||
|
const int kTrackTitleColumn = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* RipCDDialog::kSettingsGroup = "Transcoder";
|
||||||
|
const int RipCDDialog::kMaxDestinationItems = 10;
|
||||||
|
|
||||||
|
RipCDDialog::RipCDDialog(QWidget* parent)
|
||||||
|
: QDialog(parent),
|
||||||
|
ui_(new Ui_RipCDDialog),
|
||||||
|
ripper_(new Ripper(this)),
|
||||||
|
working_(false) {
|
||||||
|
// Init
|
||||||
|
ui_->setupUi(this);
|
||||||
|
|
||||||
|
// Set column widths in the QTableWidget.
|
||||||
|
ui_->tableWidget->horizontalHeader()->setResizeMode(
|
||||||
|
kCheckboxColumn, QHeaderView::ResizeToContents);
|
||||||
|
ui_->tableWidget->horizontalHeader()->setResizeMode(
|
||||||
|
kTrackNumberColumn, QHeaderView::ResizeToContents);
|
||||||
|
ui_->tableWidget->horizontalHeader()->setResizeMode(kTrackTitleColumn,
|
||||||
|
QHeaderView::Stretch);
|
||||||
|
|
||||||
|
// Add a rip button
|
||||||
|
rip_button_ = ui_->button_box->addButton(tr("Start ripping"),
|
||||||
|
QDialogButtonBox::ActionRole);
|
||||||
|
cancel_button_ = ui_->button_box->button(QDialogButtonBox::Cancel);
|
||||||
|
close_button_ = ui_->button_box->button(QDialogButtonBox::Close);
|
||||||
|
|
||||||
|
// Hide elements
|
||||||
|
cancel_button_->hide();
|
||||||
|
ui_->progress_group->hide();
|
||||||
|
|
||||||
|
connect(ui_->select_all_button, SIGNAL(clicked()), SLOT(SelectAll()));
|
||||||
|
connect(ui_->select_none_button, SIGNAL(clicked()), SLOT(SelectNone()));
|
||||||
|
connect(ui_->invert_selection_button, SIGNAL(clicked()),
|
||||||
|
SLOT(InvertSelection()));
|
||||||
|
connect(rip_button_, SIGNAL(clicked()), SLOT(ClickedRipButton()));
|
||||||
|
connect(cancel_button_, SIGNAL(clicked()), ripper_, SLOT(Cancel()));
|
||||||
|
connect(close_button_, SIGNAL(clicked()), SLOT(hide()));
|
||||||
|
|
||||||
|
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
|
||||||
|
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
|
||||||
|
|
||||||
|
connect(ripper_, SIGNAL(Finished()), SLOT(Finished()));
|
||||||
|
connect(ripper_, SIGNAL(Cancelled()), SLOT(Cancelled()));
|
||||||
|
connect(ripper_, SIGNAL(ProgressInterval(int, int)),
|
||||||
|
SLOT(SetupProgressBarLimits(int, int)));
|
||||||
|
connect(ripper_, SIGNAL(Progress(int)), SLOT(UpdateProgressBar(int)));
|
||||||
|
|
||||||
|
setWindowTitle(tr("Rip CD"));
|
||||||
|
AddDestinationDirectory(QDir::homePath());
|
||||||
|
|
||||||
|
// Get presets
|
||||||
|
QList<TranscoderPreset> presets = Transcoder::GetAllPresets();
|
||||||
|
qSort(presets.begin(), presets.end(), ComparePresetsByName);
|
||||||
|
for (const TranscoderPreset& preset : presets) {
|
||||||
|
ui_->format->addItem(
|
||||||
|
QString("%1 (.%2)").arg(preset.name_).arg(preset.extension_),
|
||||||
|
QVariant::fromValue(preset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
last_add_dir_ = s.value("last_add_dir", QDir::homePath()).toString();
|
||||||
|
|
||||||
|
QString last_output_format = s.value("last_output_format", "ogg").toString();
|
||||||
|
for (int i = 0; i < ui_->format->count(); ++i) {
|
||||||
|
if (last_output_format ==
|
||||||
|
ui_->format->itemData(i).value<TranscoderPreset>().extension_) {
|
||||||
|
ui_->format->setCurrentIndex(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RipCDDialog::~RipCDDialog() {}
|
||||||
|
|
||||||
|
bool RipCDDialog::CheckCDIOIsValid() { return ripper_->CheckCDIOIsValid(); }
|
||||||
|
|
||||||
|
void RipCDDialog::showEvent(QShowEvent* event) {
|
||||||
|
BuildTrackListTable();
|
||||||
|
if (!working_) {
|
||||||
|
ui_->progress_group->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::ClickedRipButton() {
|
||||||
|
if (ripper_->MediaChanged()) {
|
||||||
|
QMessageBox cdio_fail(QMessageBox::Critical, tr("Error Ripping CD"),
|
||||||
|
tr("Media has changed. Reloading"));
|
||||||
|
cdio_fail.exec();
|
||||||
|
if (CheckCDIOIsValid()) {
|
||||||
|
BuildTrackListTable();
|
||||||
|
} else {
|
||||||
|
ui_->tableWidget->clearContents();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tracks and album information to the ripper.
|
||||||
|
ripper_->ClearTracks();
|
||||||
|
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
||||||
|
.value<TranscoderPreset>();
|
||||||
|
for (int i = 1; i <= ui_->tableWidget->rowCount(); ++i) {
|
||||||
|
if (!checkboxes_.value(i - 1)->isChecked()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString transcoded_filename = GetOutputFileName(
|
||||||
|
ParseFileFormatString(ui_->format_filename->text(), i));
|
||||||
|
QString title = track_names_.value(i - 1)->text();
|
||||||
|
ripper_->AddTrack(i, title, transcoded_filename, preset);
|
||||||
|
}
|
||||||
|
ripper_->SetAlbumInformation(
|
||||||
|
ui_->albumLineEdit->text(), ui_->artistLineEdit->text(),
|
||||||
|
ui_->genreLineEdit->text(), ui_->yearLineEdit->text().toInt(),
|
||||||
|
ui_->discLineEdit->text().toInt(), preset.type_);
|
||||||
|
|
||||||
|
SetWorking(true);
|
||||||
|
ripper_->Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::Options() {
|
||||||
|
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
||||||
|
.value<TranscoderPreset>();
|
||||||
|
|
||||||
|
TranscoderOptionsDialog dialog(preset.type_, this);
|
||||||
|
if (dialog.is_valid()) {
|
||||||
|
dialog.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a folder to the destination box.
|
||||||
|
void RipCDDialog::AddDestination() {
|
||||||
|
int index = ui_->destination->currentIndex();
|
||||||
|
QString initial_dir = (!ui_->destination->itemData(index).isNull()
|
||||||
|
? ui_->destination->itemData(index).toString()
|
||||||
|
: QDir::homePath());
|
||||||
|
QString dir =
|
||||||
|
QFileDialog::getExistingDirectory(this, tr("Add folder"), initial_dir);
|
||||||
|
|
||||||
|
if (!dir.isEmpty()) {
|
||||||
|
// Keep only a finite number of items in the box.
|
||||||
|
while (ui_->destination->count() >= kMaxDestinationItems) {
|
||||||
|
ui_->destination->removeItem(0); // The oldest item.
|
||||||
|
}
|
||||||
|
AddDestinationDirectory(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a directory to the 'destination' combo box.
|
||||||
|
void RipCDDialog::AddDestinationDirectory(QString dir) {
|
||||||
|
QIcon icon = IconLoader::Load("folder");
|
||||||
|
QVariant data = QVariant::fromValue(dir);
|
||||||
|
// Do not insert duplicates.
|
||||||
|
int duplicate_index = ui_->destination->findData(data);
|
||||||
|
if (duplicate_index == -1) {
|
||||||
|
ui_->destination->addItem(icon, dir, data);
|
||||||
|
ui_->destination->setCurrentIndex(ui_->destination->count() - 1);
|
||||||
|
} else {
|
||||||
|
ui_->destination->setCurrentIndex(duplicate_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::SelectAll() {
|
||||||
|
for (QCheckBox* checkbox : checkboxes_) {
|
||||||
|
checkbox->setCheckState(Qt::Checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::SelectNone() {
|
||||||
|
for (QCheckBox* checkbox : checkboxes_) {
|
||||||
|
checkbox->setCheckState(Qt::Unchecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::InvertSelection() {
|
||||||
|
for (QCheckBox* checkbox : checkboxes_) {
|
||||||
|
checkbox->setCheckState(checkbox->isChecked() ? Qt::Unchecked
|
||||||
|
: Qt::Checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::Finished() { SetWorking(false); }
|
||||||
|
|
||||||
|
void RipCDDialog::Cancelled() {
|
||||||
|
ui_->progress_bar->setValue(0);
|
||||||
|
SetWorking(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::SetupProgressBarLimits(int min, int max) {
|
||||||
|
ui_->progress_bar->setRange(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::UpdateProgressBar(int progress) {
|
||||||
|
ui_->progress_bar->setValue(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::SetWorking(bool working) {
|
||||||
|
working_ = working;
|
||||||
|
rip_button_->setVisible(!working);
|
||||||
|
cancel_button_->setVisible(working);
|
||||||
|
close_button_->setVisible(!working);
|
||||||
|
ui_->input_group->setEnabled(!working);
|
||||||
|
ui_->output_group->setEnabled(!working);
|
||||||
|
ui_->progress_group->setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RipCDDialog::BuildTrackListTable() {
|
||||||
|
checkboxes_.clear();
|
||||||
|
track_names_.clear();
|
||||||
|
|
||||||
|
int tracks = ripper_->TracksOnDisc();
|
||||||
|
|
||||||
|
ui_->tableWidget->setRowCount(tracks);
|
||||||
|
for (int i = 1; i <= tracks; i++) {
|
||||||
|
QCheckBox* checkbox_i = new QCheckBox(ui_->tableWidget);
|
||||||
|
checkbox_i->setCheckState(Qt::Checked);
|
||||||
|
checkboxes_.append(checkbox_i);
|
||||||
|
ui_->tableWidget->setCellWidget(i - 1, kCheckboxColumn, checkbox_i);
|
||||||
|
ui_->tableWidget->setCellWidget(i - 1, kTrackNumberColumn,
|
||||||
|
new QLabel(QString::number(i)));
|
||||||
|
QString track_title = QString("Track %1").arg(i);
|
||||||
|
QLineEdit* line_edit_track_title_i =
|
||||||
|
new QLineEdit(track_title, ui_->tableWidget);
|
||||||
|
track_names_.append(line_edit_track_title_i);
|
||||||
|
ui_->tableWidget->setCellWidget(i - 1, kTrackTitleColumn,
|
||||||
|
line_edit_track_title_i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RipCDDialog::GetOutputFileName(const QString& basename) const {
|
||||||
|
QString path =
|
||||||
|
ui_->destination->itemData(ui_->destination->currentIndex()).toString();
|
||||||
|
QString extension = ui_->format->itemData(ui_->format->currentIndex())
|
||||||
|
.value<TranscoderPreset>()
|
||||||
|
.extension_;
|
||||||
|
return path + '/' + basename + '.' + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RipCDDialog::ParseFileFormatString(const QString& file_format,
|
||||||
|
int track_no) const {
|
||||||
|
QString to_return = file_format;
|
||||||
|
to_return.replace(QString("%artist%"), ui_->artistLineEdit->text());
|
||||||
|
to_return.replace(QString("%album%"), ui_->albumLineEdit->text());
|
||||||
|
to_return.replace(QString("%genre%"), ui_->genreLineEdit->text());
|
||||||
|
to_return.replace(QString("%year%"), ui_->yearLineEdit->text());
|
||||||
|
to_return.replace(QString("%tracknum%"), QString::number(track_no));
|
||||||
|
to_return.replace(QString("%track%"),
|
||||||
|
track_names_.value(track_no - 1)->text());
|
||||||
|
return to_return;
|
||||||
|
}
|
80
src/ripper/ripcddialog.h
Normal file
80
src/ripper/ripcddialog.h
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||||
|
|
||||||
|
Clementine 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.
|
||||||
|
|
||||||
|
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SRC_RIPPER_RIPCDDIALOG_H_
|
||||||
|
#define SRC_RIPPER_RIPCDDIALOG_H_
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QFile>
|
||||||
|
|
||||||
|
#include "core/song.h"
|
||||||
|
#include "core/tagreaderclient.h"
|
||||||
|
|
||||||
|
class QCheckBox;
|
||||||
|
class QLineEdit;
|
||||||
|
|
||||||
|
class Ripper;
|
||||||
|
class Ui_RipCDDialog;
|
||||||
|
|
||||||
|
class RipCDDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RipCDDialog(QWidget* parent = nullptr);
|
||||||
|
~RipCDDialog();
|
||||||
|
bool CheckCDIOIsValid();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void showEvent(QShowEvent* event);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void ClickedRipButton();
|
||||||
|
void Options();
|
||||||
|
void AddDestination();
|
||||||
|
void SelectAll();
|
||||||
|
void SelectNone();
|
||||||
|
void InvertSelection();
|
||||||
|
void Finished();
|
||||||
|
void Cancelled();
|
||||||
|
void SetupProgressBarLimits(int min, int max);
|
||||||
|
void UpdateProgressBar(int progress);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const char* kSettingsGroup;
|
||||||
|
static const int kMaxDestinationItems;
|
||||||
|
|
||||||
|
// Constructs a filename from the given base name with a path taken
|
||||||
|
// from the ui dialog and an extension that corresponds to the audio
|
||||||
|
// format chosen in the ui.
|
||||||
|
void AddDestinationDirectory(QString dir);
|
||||||
|
void BuildTrackListTable();
|
||||||
|
QString GetOutputFileName(const QString& basename) const;
|
||||||
|
QString ParseFileFormatString(const QString& file_format, int track_no) const;
|
||||||
|
void SetWorking(bool working);
|
||||||
|
|
||||||
|
QList<QCheckBox*> checkboxes_;
|
||||||
|
QList<QLineEdit*> track_names_;
|
||||||
|
QString last_add_dir_;
|
||||||
|
QPushButton* cancel_button_;
|
||||||
|
QPushButton* close_button_;
|
||||||
|
QPushButton* rip_button_;
|
||||||
|
std::unique_ptr<Ui_RipCDDialog> ui_;
|
||||||
|
Ripper* ripper_;
|
||||||
|
bool working_;
|
||||||
|
};
|
||||||
|
#endif // SRC_RIPPER_RIPCDDIALOG_H_
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>RipCD</class>
|
<class>RipCDDialog</class>
|
||||||
<widget class="QDialog" name="RipCD">
|
<widget class="QDialog" name="RipCDDialog">
|
||||||
<property name="windowModality">
|
<property name="windowModality">
|
||||||
<enum>Qt::NonModal</enum>
|
<enum>Qt::NonModal</enum>
|
||||||
</property>
|
</property>
|
311
src/ripper/ripper.cpp
Normal file
311
src/ripper/ripper.cpp
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||||
|
|
||||||
|
Clementine 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.
|
||||||
|
|
||||||
|
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ripper.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QMutexLocker>
|
||||||
|
#include <QtConcurrentRun>
|
||||||
|
|
||||||
|
#include "core/closure.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/tagreaderclient.h"
|
||||||
|
#include "transcoder/transcoder.h"
|
||||||
|
#include "core/utilities.h"
|
||||||
|
|
||||||
|
// winspool.h defines this :(
|
||||||
|
#ifdef AddJob
|
||||||
|
#undef AddJob
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kWavHeaderRiffMarker[] = "RIFF";
|
||||||
|
const char kWavFileTypeFormatChunk[] = "WAVEfmt ";
|
||||||
|
const char kWavDataString[] = "data";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Ripper::Ripper(QObject* parent)
|
||||||
|
: QObject(parent),
|
||||||
|
transcoder_(new Transcoder(this)),
|
||||||
|
cancel_requested_(false),
|
||||||
|
finished_success_(0),
|
||||||
|
finished_failed_(0),
|
||||||
|
files_tagged_(0) {
|
||||||
|
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
||||||
|
|
||||||
|
connect(this, SIGNAL(RippingComplete()), transcoder_, SLOT(Start()));
|
||||||
|
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
||||||
|
SLOT(TranscodingJobComplete(QString, QString, bool)));
|
||||||
|
connect(transcoder_, SIGNAL(AllJobsComplete()),
|
||||||
|
SLOT(AllTranscodingJobsComplete()));
|
||||||
|
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ripper::~Ripper() { cdio_destroy(cdio_); }
|
||||||
|
|
||||||
|
void Ripper::AddTrack(int track_number, const QString& title,
|
||||||
|
const QString& transcoded_filename,
|
||||||
|
const TranscoderPreset& preset) {
|
||||||
|
if (track_number < 1 || track_number > TracksOnDisc()) {
|
||||||
|
qLog(Warning) << "Invalid track number:" << track_number << "Ignoring";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TrackInformation track(track_number, title, transcoded_filename, preset);
|
||||||
|
tracks_.append(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::SetAlbumInformation(const QString& album, const QString& artist,
|
||||||
|
const QString& genre, int year, int disc,
|
||||||
|
Song::FileType type) {
|
||||||
|
album_.album = album;
|
||||||
|
album_.artist = artist;
|
||||||
|
album_.genre = genre;
|
||||||
|
album_.year = year;
|
||||||
|
album_.disc = disc;
|
||||||
|
album_.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Ripper::TracksOnDisc() const {
|
||||||
|
int number_of_tracks = cdio_get_num_tracks(cdio_);
|
||||||
|
// Return zero tracks if there is an error, e.g. no medium found.
|
||||||
|
if (number_of_tracks == CDIO_INVALID_TRACK) number_of_tracks = 0;
|
||||||
|
return number_of_tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Ripper::AddedTracks() const { return tracks_.length(); }
|
||||||
|
|
||||||
|
void Ripper::ClearTracks() { tracks_.clear(); }
|
||||||
|
|
||||||
|
bool Ripper::CheckCDIOIsValid() {
|
||||||
|
if (cdio_) {
|
||||||
|
cdio_destroy(cdio_);
|
||||||
|
}
|
||||||
|
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
||||||
|
// Refresh the status of the cd media. This will prevent unnecessary
|
||||||
|
// rebuilds of the track list table.
|
||||||
|
if (cdio_) {
|
||||||
|
cdio_get_media_changed(cdio_);
|
||||||
|
}
|
||||||
|
return cdio_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Ripper::MediaChanged() const {
|
||||||
|
if (cdio_ && cdio_get_media_changed(cdio_))
|
||||||
|
return true;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::Start() {
|
||||||
|
{
|
||||||
|
QMutexLocker l(&mutex_);
|
||||||
|
cancel_requested_ = false;
|
||||||
|
}
|
||||||
|
SetupProgressInterval();
|
||||||
|
|
||||||
|
qLog(Debug) << "Ripping" << AddedTracks() << "tracks.";
|
||||||
|
QtConcurrent::run(this, &Ripper::Rip);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::Cancel() {
|
||||||
|
{
|
||||||
|
QMutexLocker l(&mutex_);
|
||||||
|
cancel_requested_ = true;
|
||||||
|
}
|
||||||
|
transcoder_->Cancel();
|
||||||
|
RemoveTemporaryDirectory();
|
||||||
|
emit(Cancelled());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::TranscodingJobComplete(const QString& input, const QString& output,
|
||||||
|
bool success) {
|
||||||
|
if (success)
|
||||||
|
finished_success_++;
|
||||||
|
else
|
||||||
|
finished_failed_++;
|
||||||
|
UpdateProgress();
|
||||||
|
|
||||||
|
// The the transcoder does not overwrite files. Instead, it changes
|
||||||
|
// the name of the output file. We need to update the transcoded
|
||||||
|
// filename for the corresponding track so that we tag the correct
|
||||||
|
// file later on.
|
||||||
|
for (QList<TrackInformation>::iterator it = tracks_.begin();
|
||||||
|
it != tracks_.end(); ++it) {
|
||||||
|
if (it->temporary_filename == input) {
|
||||||
|
it->transcoded_filename = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::AllTranscodingJobsComplete() {
|
||||||
|
RemoveTemporaryDirectory();
|
||||||
|
TagFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::LogLine(const QString& message) { qLog(Debug) << message; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* WAV Header documentation
|
||||||
|
* as taken from:
|
||||||
|
* http://www.topherlee.com/software/pcm-tut-wavformat.html
|
||||||
|
* Pos Value Description
|
||||||
|
* 0-3 | "RIFF" | Marks the file as a riff file.
|
||||||
|
* | Characters are each 1 byte long.
|
||||||
|
* 4-7 | File size (integer) | Size of the overall file - 8 bytes,
|
||||||
|
* | in bytes (32-bit integer).
|
||||||
|
* 8-11 | "WAVE" | File Type Header. For our purposes,
|
||||||
|
* | it always equals "WAVE".
|
||||||
|
* 13-16 | "fmt " | Format chunk marker. Includes trailing null.
|
||||||
|
* 17-20 | 16 | Length of format data as listed above
|
||||||
|
* 21-22 | 1 | Type of format (1 is PCM) - 2 byte integer
|
||||||
|
* 23-24 | 2 | Number of Channels - 2 byte integer
|
||||||
|
* 25-28 | 44100 | Sample Rate - 32 byte integer. Common values
|
||||||
|
* | are 44100 (CD), 48000 (DAT).
|
||||||
|
* | Sample Rate = Number of Samples per second, or Hertz.
|
||||||
|
* 29-32 | 176400 | (Sample Rate * BitsPerSample * Channels) / 8.
|
||||||
|
* 33-34 | 4 | (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16
|
||||||
|
* bit mono4 - 16 bit stereo
|
||||||
|
* 35-36 | 16 | Bits per sample
|
||||||
|
* 37-40 | "data" | "data" chunk header.
|
||||||
|
* | Marks the beginning of the data section.
|
||||||
|
* 41-44 | File size (data) | Size of the data section.
|
||||||
|
*/
|
||||||
|
void Ripper::WriteWAVHeader(QFile* stream, int32_t i_bytecount) {
|
||||||
|
QDataStream data_stream(stream);
|
||||||
|
data_stream.setByteOrder(QDataStream::LittleEndian);
|
||||||
|
// sizeof() - 1 to avoid including "\0" in the file too
|
||||||
|
data_stream.writeRawData(kWavHeaderRiffMarker,
|
||||||
|
sizeof(kWavHeaderRiffMarker) - 1); /* 0-3 */
|
||||||
|
data_stream << qint32(i_bytecount + 44 - 8); /* 4-7 */
|
||||||
|
data_stream.writeRawData(kWavFileTypeFormatChunk,
|
||||||
|
sizeof(kWavFileTypeFormatChunk) - 1); /* 8-15 */
|
||||||
|
data_stream << (qint32)16; /* 16-19 */
|
||||||
|
data_stream << (qint16)1; /* 20-21 */
|
||||||
|
data_stream << (qint16)2; /* 22-23 */
|
||||||
|
data_stream << (qint32)44100; /* 24-27 */
|
||||||
|
data_stream << (qint32)(44100 * 2 * 2); /* 28-31 */
|
||||||
|
data_stream << (qint16)4; /* 32-33 */
|
||||||
|
data_stream << (qint16)16; /* 34-35 */
|
||||||
|
data_stream.writeRawData(kWavDataString,
|
||||||
|
sizeof(kWavDataString) - 1); /* 36-39 */
|
||||||
|
data_stream << (qint32)i_bytecount; /* 40-43 */
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::Rip() {
|
||||||
|
temporary_directory_ = Utilities::MakeTempDir() + "/";
|
||||||
|
finished_success_ = 0;
|
||||||
|
finished_failed_ = 0;
|
||||||
|
|
||||||
|
// Set up progress bar
|
||||||
|
UpdateProgress();
|
||||||
|
|
||||||
|
for (QList<TrackInformation>::iterator it = tracks_.begin();
|
||||||
|
it != tracks_.end(); ++it) {
|
||||||
|
QString filename =
|
||||||
|
QString("%1%2.wav").arg(temporary_directory_).arg(it->track_number);
|
||||||
|
QFile destination_file(filename);
|
||||||
|
destination_file.open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
lsn_t i_first_lsn = cdio_get_track_lsn(cdio_, it->track_number);
|
||||||
|
lsn_t i_last_lsn = cdio_get_track_last_lsn(cdio_, it->track_number);
|
||||||
|
WriteWAVHeader(&destination_file,
|
||||||
|
(i_last_lsn - i_first_lsn + 1) * CDIO_CD_FRAMESIZE_RAW);
|
||||||
|
|
||||||
|
QByteArray buffered_input_bytes(CDIO_CD_FRAMESIZE_RAW, '\0');
|
||||||
|
for (lsn_t i_cursor = i_first_lsn; i_cursor <= i_last_lsn; i_cursor++) {
|
||||||
|
{
|
||||||
|
QMutexLocker l(&mutex_);
|
||||||
|
if (cancel_requested_) {
|
||||||
|
qLog(Debug) << "CD ripping canceled.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cdio_read_audio_sector(cdio_, buffered_input_bytes.data(),
|
||||||
|
i_cursor) == DRIVER_OP_SUCCESS) {
|
||||||
|
destination_file.write(buffered_input_bytes.data(),
|
||||||
|
buffered_input_bytes.size());
|
||||||
|
} else {
|
||||||
|
qLog(Error) << "CD read error";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finished_success_++;
|
||||||
|
UpdateProgress();
|
||||||
|
|
||||||
|
it->temporary_filename = filename;
|
||||||
|
transcoder_->AddJob(it->temporary_filename, it->preset,
|
||||||
|
it->transcoded_filename);
|
||||||
|
}
|
||||||
|
emit(RippingComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The progress interval is [0, 200*AddedTracks()], where the first
|
||||||
|
// half corresponds to the CD ripping and the second half corresponds
|
||||||
|
// to the transcoding.
|
||||||
|
void Ripper::SetupProgressInterval() {
|
||||||
|
int max = AddedTracks() * 2 * 100;
|
||||||
|
emit ProgressInterval(0, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::UpdateProgress() {
|
||||||
|
int progress = (finished_success_ + finished_failed_) * 100;
|
||||||
|
QMap<QString, float> current_jobs = transcoder_->GetProgress();
|
||||||
|
for (float value : current_jobs.values()) {
|
||||||
|
progress += qBound(0, static_cast<int>(value * 100), 99);
|
||||||
|
}
|
||||||
|
emit Progress(progress);
|
||||||
|
qLog(Debug) << "Progress:" << progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::RemoveTemporaryDirectory() {
|
||||||
|
if (!temporary_directory_.isEmpty())
|
||||||
|
Utilities::RemoveRecursive(temporary_directory_);
|
||||||
|
temporary_directory_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::TagFiles() {
|
||||||
|
files_tagged_ = 0;
|
||||||
|
for (const TrackInformation& track : tracks_) {
|
||||||
|
Song song;
|
||||||
|
song.InitFromFilePartial(track.transcoded_filename);
|
||||||
|
song.set_track(track.track_number);
|
||||||
|
song.set_title(track.title);
|
||||||
|
song.set_album(album_.album);
|
||||||
|
song.set_artist(album_.artist);
|
||||||
|
song.set_genre(album_.genre);
|
||||||
|
song.set_year(album_.year);
|
||||||
|
song.set_disc(album_.disc);
|
||||||
|
song.set_filetype(album_.type);
|
||||||
|
|
||||||
|
TagReaderReply* reply =
|
||||||
|
TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
|
||||||
|
NewClosure(reply, SIGNAL(Finished(bool)), this,
|
||||||
|
SLOT(FileTagged(TagReaderReply*)), reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ripper::FileTagged(TagReaderReply* reply) {
|
||||||
|
files_tagged_++;
|
||||||
|
qLog(Debug) << "Tagged" << files_tagged_ << "of" << tracks_.length()
|
||||||
|
<< "files";
|
||||||
|
if (files_tagged_ == tracks_.length()) {
|
||||||
|
qLog(Debug) << "CD ripper finished.";
|
||||||
|
emit(Finished());
|
||||||
|
}
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
132
src/ripper/ripper.h
Normal file
132
src/ripper/ripper.h
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/* This file is part of Clementine.
|
||||||
|
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||||
|
|
||||||
|
Clementine 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.
|
||||||
|
|
||||||
|
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SRC_RIPPER_RIPPER_H_
|
||||||
|
#define SRC_RIPPER_RIPPER_H_
|
||||||
|
|
||||||
|
#include <cdio/cdio.h>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "core/song.h"
|
||||||
|
#include "core/tagreaderclient.h"
|
||||||
|
#include "transcoder/transcoder.h"
|
||||||
|
|
||||||
|
class QFile;
|
||||||
|
|
||||||
|
// Rips selected tracks from an audio CD, transcodes them to a chosen
|
||||||
|
// format, and finally tags the files with the supplied metadata.
|
||||||
|
//
|
||||||
|
// Usage: Add tracks with AddTrack() and album metadata with
|
||||||
|
// SetAlbumInformation(). Then start the ripper with Start(). The ripper
|
||||||
|
// emits the Finished() signal when it's done or the Cancelled()
|
||||||
|
// signal if the ripping has been cancelled.
|
||||||
|
class Ripper : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Ripper(QObject* parent = nullptr);
|
||||||
|
~Ripper();
|
||||||
|
|
||||||
|
// Adds a track to the rip list if the track number corresponds to a
|
||||||
|
// track on the audio cd. The track will transcoded according to the
|
||||||
|
// chosen TranscoderPreset.
|
||||||
|
void AddTrack(int track_number, const QString& title,
|
||||||
|
const QString& transcoded_filename,
|
||||||
|
const TranscoderPreset& preset);
|
||||||
|
// Sets album metadata. This information is used when tagging the
|
||||||
|
// final files.
|
||||||
|
void SetAlbumInformation(const QString& album, const QString& artist,
|
||||||
|
const QString& genre, int year, int disc,
|
||||||
|
Song::FileType type);
|
||||||
|
// Returns the number of audio tracks on the disc.
|
||||||
|
int TracksOnDisc() const;
|
||||||
|
// Returns the number of tracks added to the rip list.
|
||||||
|
int AddedTracks() const;
|
||||||
|
// Clears the rip list.
|
||||||
|
void ClearTracks();
|
||||||
|
// Returns true if a cd device was successfully opened.
|
||||||
|
bool CheckCDIOIsValid();
|
||||||
|
// Returns true if the cd media has changed.
|
||||||
|
bool MediaChanged() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void Finished();
|
||||||
|
void Cancelled();
|
||||||
|
void ProgressInterval(int min, int max);
|
||||||
|
void Progress(int progress);
|
||||||
|
void RippingComplete();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void Start();
|
||||||
|
void Cancel();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void TranscodingJobComplete(const QString& input, const QString& output,
|
||||||
|
bool success);
|
||||||
|
void AllTranscodingJobsComplete();
|
||||||
|
void LogLine(const QString& message);
|
||||||
|
void FileTagged(TagReaderReply* reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TrackInformation {
|
||||||
|
TrackInformation(int track_number, const QString& title,
|
||||||
|
const QString& transcoded_filename,
|
||||||
|
const TranscoderPreset& preset)
|
||||||
|
: track_number(track_number),
|
||||||
|
title(title),
|
||||||
|
transcoded_filename(transcoded_filename),
|
||||||
|
preset(preset) {}
|
||||||
|
|
||||||
|
int track_number;
|
||||||
|
QString title;
|
||||||
|
QString transcoded_filename;
|
||||||
|
TranscoderPreset preset;
|
||||||
|
QString temporary_filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AlbumInformation {
|
||||||
|
AlbumInformation() : year(0), disc(0), type(Song::Type_Unknown) {}
|
||||||
|
|
||||||
|
QString album;
|
||||||
|
QString artist;
|
||||||
|
QString genre;
|
||||||
|
int year;
|
||||||
|
int disc;
|
||||||
|
Song::FileType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
void WriteWAVHeader(QFile* stream, int32_t i_bytecount);
|
||||||
|
void Rip();
|
||||||
|
void SetupProgressInterval();
|
||||||
|
void UpdateProgress();
|
||||||
|
void RemoveTemporaryDirectory();
|
||||||
|
void TagFiles();
|
||||||
|
|
||||||
|
CdIo_t* cdio_;
|
||||||
|
Transcoder* transcoder_;
|
||||||
|
QString temporary_directory_;
|
||||||
|
bool cancel_requested_;
|
||||||
|
QMutex mutex_;
|
||||||
|
int finished_success_;
|
||||||
|
int finished_failed_;
|
||||||
|
int files_tagged_;
|
||||||
|
QList<TrackInformation> tracks_;
|
||||||
|
AlbumInformation album_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SRC_RIPPER_RIPPER_H_
|
@ -95,6 +95,9 @@
|
|||||||
#include "playlist/songplaylistitem.h"
|
#include "playlist/songplaylistitem.h"
|
||||||
#include "playlistparsers/playlistparser.h"
|
#include "playlistparsers/playlistparser.h"
|
||||||
#include "internet/podcasts/podcastservice.h"
|
#include "internet/podcasts/podcastservice.h"
|
||||||
|
#ifdef HAVE_AUDIOCD
|
||||||
|
#include "ripper/ripcddialog.h"
|
||||||
|
#endif
|
||||||
#include "smartplaylists/generator.h"
|
#include "smartplaylists/generator.h"
|
||||||
#include "smartplaylists/generatormimedata.h"
|
#include "smartplaylists/generatormimedata.h"
|
||||||
#include "songinfo/artistinfoview.h"
|
#include "songinfo/artistinfoview.h"
|
||||||
@ -110,9 +113,6 @@
|
|||||||
#include "ui/organisedialog.h"
|
#include "ui/organisedialog.h"
|
||||||
#include "ui/organiseerrordialog.h"
|
#include "ui/organiseerrordialog.h"
|
||||||
#include "ui/qtsystemtrayicon.h"
|
#include "ui/qtsystemtrayicon.h"
|
||||||
#ifdef HAVE_AUDIOCD
|
|
||||||
#include "ui/ripcd.h"
|
|
||||||
#endif
|
|
||||||
#include "ui/settingsdialog.h"
|
#include "ui/settingsdialog.h"
|
||||||
#include "ui/systemtrayicon.h"
|
#include "ui/systemtrayicon.h"
|
||||||
#include "ui/trackselectiondialog.h"
|
#include "ui/trackselectiondialog.h"
|
||||||
@ -312,6 +312,7 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd,
|
|||||||
ui_->action_quit->setIcon(IconLoader::Load("application-exit"));
|
ui_->action_quit->setIcon(IconLoader::Load("application-exit"));
|
||||||
ui_->action_remove_from_playlist->setIcon(IconLoader::Load("list-remove"));
|
ui_->action_remove_from_playlist->setIcon(IconLoader::Load("list-remove"));
|
||||||
ui_->action_repeat_mode->setIcon(IconLoader::Load("media-playlist-repeat"));
|
ui_->action_repeat_mode->setIcon(IconLoader::Load("media-playlist-repeat"));
|
||||||
|
ui_->action_rip_audio_cd->setIcon(IconLoader::Load("media-optical"));
|
||||||
ui_->action_shuffle->setIcon(IconLoader::Load("x-clementine-shuffle"));
|
ui_->action_shuffle->setIcon(IconLoader::Load("x-clementine-shuffle"));
|
||||||
ui_->action_shuffle_mode->setIcon(IconLoader::Load("media-playlist-shuffle"));
|
ui_->action_shuffle_mode->setIcon(IconLoader::Load("media-playlist-shuffle"));
|
||||||
ui_->action_stop->setIcon(IconLoader::Load("media-playback-stop"));
|
ui_->action_stop->setIcon(IconLoader::Load("media-playback-stop"));
|
||||||
@ -389,7 +390,8 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd,
|
|||||||
connect(ui_->action_open_media, SIGNAL(triggered()), SLOT(AddFile()));
|
connect(ui_->action_open_media, SIGNAL(triggered()), SLOT(AddFile()));
|
||||||
connect(ui_->action_open_cd, SIGNAL(triggered()), SLOT(AddCDTracks()));
|
connect(ui_->action_open_cd, SIGNAL(triggered()), SLOT(AddCDTracks()));
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
connect(ui_->action_rip_audio_cd, SIGNAL(triggered()), SLOT(OpenRipCD()));
|
connect(ui_->action_rip_audio_cd, SIGNAL(triggered()),
|
||||||
|
SLOT(OpenRipCDDialog()));
|
||||||
#else
|
#else
|
||||||
ui_->action_rip_audio_cd->setVisible(false);
|
ui_->action_rip_audio_cd->setVisible(false);
|
||||||
#endif
|
#endif
|
||||||
@ -1883,13 +1885,13 @@ void MainWindow::AddStreamAccepted() {
|
|||||||
AddToPlaylist(data);
|
AddToPlaylist(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::OpenRipCD() {
|
void MainWindow::OpenRipCDDialog() {
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
if (!rip_cd_) {
|
if (!rip_cd_dialog_) {
|
||||||
rip_cd_.reset(new RipCD);
|
rip_cd_dialog_.reset(new RipCDDialog);
|
||||||
}
|
}
|
||||||
if (rip_cd_->CheckCDIOIsValid()) {
|
if (rip_cd_dialog_->CheckCDIOIsValid()) {
|
||||||
rip_cd_->show();
|
rip_cd_dialog_->show();
|
||||||
} else {
|
} else {
|
||||||
QMessageBox cdio_fail(QMessageBox::Critical, tr("Error"),
|
QMessageBox cdio_fail(QMessageBox::Critical, tr("Error"),
|
||||||
tr("Failed reading CD drive"));
|
tr("Failed reading CD drive"));
|
||||||
|
@ -68,7 +68,7 @@ class InternetItem;
|
|||||||
class InternetModel;
|
class InternetModel;
|
||||||
class InternetViewContainer;
|
class InternetViewContainer;
|
||||||
class Remote;
|
class Remote;
|
||||||
class RipCD;
|
class RipCDDialog;
|
||||||
class Song;
|
class Song;
|
||||||
class SongInfoBase;
|
class SongInfoBase;
|
||||||
class SongInfoView;
|
class SongInfoView;
|
||||||
@ -218,7 +218,7 @@ signals:
|
|||||||
void AddFolder();
|
void AddFolder();
|
||||||
void AddStream();
|
void AddStream();
|
||||||
void AddStreamAccepted();
|
void AddStreamAccepted();
|
||||||
void OpenRipCD();
|
void OpenRipCDDialog();
|
||||||
void AddCDTracks();
|
void AddCDTracks();
|
||||||
void AddPodcast();
|
void AddPodcast();
|
||||||
|
|
||||||
@ -297,7 +297,7 @@ signals:
|
|||||||
LibraryViewContainer* library_view_;
|
LibraryViewContainer* library_view_;
|
||||||
FileView* file_view_;
|
FileView* file_view_;
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
std::unique_ptr<RipCD> rip_cd_;
|
std::unique_ptr<RipCDDialog> rip_cd_dialog_;
|
||||||
#endif
|
#endif
|
||||||
PlaylistListContainer* playlist_list_;
|
PlaylistListContainer* playlist_list_;
|
||||||
InternetViewContainer* internet_view_;
|
InternetViewContainer* internet_view_;
|
||||||
|
@ -411,7 +411,6 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="action_open_media"/>
|
<addaction name="action_open_media"/>
|
||||||
<addaction name="action_open_cd"/>
|
<addaction name="action_open_cd"/>
|
||||||
<addaction name="action_rip_audio_cd"/>
|
|
||||||
<addaction name="action_add_podcast"/>
|
<addaction name="action_add_podcast"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_previous_track"/>
|
<addaction name="action_previous_track"/>
|
||||||
@ -473,6 +472,7 @@
|
|||||||
<addaction name="action_equalizer"/>
|
<addaction name="action_equalizer"/>
|
||||||
<addaction name="action_visualisations"/>
|
<addaction name="action_visualisations"/>
|
||||||
<addaction name="action_transcode"/>
|
<addaction name="action_transcode"/>
|
||||||
|
<addaction name="action_rip_audio_cd"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_update_library"/>
|
<addaction name="action_update_library"/>
|
||||||
<addaction name="action_full_library_scan"/>
|
<addaction name="action_full_library_scan"/>
|
||||||
@ -847,7 +847,7 @@
|
|||||||
</action>
|
</action>
|
||||||
<action name="action_rip_audio_cd">
|
<action name="action_rip_audio_cd">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Rip audio CD...</string>
|
<string>Rip audio CD</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_remove_unavailable">
|
<action name="action_remove_unavailable">
|
||||||
|
520
src/ui/ripcd.cpp
520
src/ui/ripcd.cpp
@ -1,520 +0,0 @@
|
|||||||
/* This file is part of Clementine.
|
|
||||||
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
|
||||||
|
|
||||||
Clementine 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.
|
|
||||||
|
|
||||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ripcd.h"
|
|
||||||
#include "config.h"
|
|
||||||
#include "ui_ripcd.h"
|
|
||||||
#include "transcoder/transcoder.h"
|
|
||||||
#include "transcoder/transcoderoptionsdialog.h"
|
|
||||||
#include "ui/iconloader.h"
|
|
||||||
#include "core/closure.h"
|
|
||||||
#include "core/logging.h"
|
|
||||||
#include "core/tagreaderclient.h"
|
|
||||||
#include "core/utilities.h"
|
|
||||||
|
|
||||||
#include <QSettings>
|
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QDataStream>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QMutexLocker>
|
|
||||||
#include <QtDebug>
|
|
||||||
#include <QtConcurrentRun>
|
|
||||||
#include <cdio/cdio.h>
|
|
||||||
|
|
||||||
// winspool.h defines this :(
|
|
||||||
#ifdef AddJob
|
|
||||||
#undef AddJob
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
bool ComparePresetsByName(const TranscoderPreset& left,
|
|
||||||
const TranscoderPreset& right) {
|
|
||||||
return left.name_ < right.name_;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char kWavHeaderRiffMarker[] = "RIFF";
|
|
||||||
const char kWavFileTypeFormatChunk[] = "WAVEfmt ";
|
|
||||||
const char kWavDataString[] = "data";
|
|
||||||
|
|
||||||
const int kCheckboxColumn = 0;
|
|
||||||
const int kTrackNumberColumn = 1;
|
|
||||||
const int kTrackTitleColumn = 2;
|
|
||||||
}
|
|
||||||
const char* RipCD::kSettingsGroup = "Transcoder";
|
|
||||||
const int RipCD::kProgressInterval = 500;
|
|
||||||
const int RipCD::kMaxDestinationItems = 10;
|
|
||||||
|
|
||||||
RipCD::RipCD(QWidget* parent)
|
|
||||||
: QDialog(parent),
|
|
||||||
transcoder_(new Transcoder(this)),
|
|
||||||
queued_(0),
|
|
||||||
finished_success_(0),
|
|
||||||
finished_failed_(0),
|
|
||||||
ui_(new Ui_RipCD),
|
|
||||||
cancel_requested_(false),
|
|
||||||
files_tagged_(0) {
|
|
||||||
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
|
||||||
// Init
|
|
||||||
ui_->setupUi(this);
|
|
||||||
|
|
||||||
// Set column widths in the QTableWidget.
|
|
||||||
ui_->tableWidget->horizontalHeader()->setResizeMode(
|
|
||||||
kCheckboxColumn, QHeaderView::ResizeToContents);
|
|
||||||
ui_->tableWidget->horizontalHeader()->setResizeMode(
|
|
||||||
kTrackNumberColumn, QHeaderView::ResizeToContents);
|
|
||||||
ui_->tableWidget->horizontalHeader()->setResizeMode(kTrackTitleColumn,
|
|
||||||
QHeaderView::Stretch);
|
|
||||||
|
|
||||||
// Add a rip button
|
|
||||||
rip_button_ = ui_->button_box->addButton(tr("Start ripping"),
|
|
||||||
QDialogButtonBox::ActionRole);
|
|
||||||
cancel_button_ = ui_->button_box->button(QDialogButtonBox::Cancel);
|
|
||||||
close_button_ = ui_->button_box->button(QDialogButtonBox::Close);
|
|
||||||
|
|
||||||
// Hide elements
|
|
||||||
cancel_button_->hide();
|
|
||||||
ui_->progress_group->hide();
|
|
||||||
|
|
||||||
connect(ui_->select_all_button, SIGNAL(clicked()), SLOT(SelectAll()));
|
|
||||||
connect(ui_->select_none_button, SIGNAL(clicked()), SLOT(SelectNone()));
|
|
||||||
connect(ui_->invert_selection_button, SIGNAL(clicked()),
|
|
||||||
SLOT(InvertSelection()));
|
|
||||||
connect(rip_button_, SIGNAL(clicked()), SLOT(ClickedRipButton()));
|
|
||||||
connect(cancel_button_, SIGNAL(clicked()), SLOT(Cancel()));
|
|
||||||
connect(close_button_, SIGNAL(clicked()), SLOT(hide()));
|
|
||||||
|
|
||||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
|
||||||
SLOT(TranscodingJobComplete(QString, QString, bool)));
|
|
||||||
connect(transcoder_, SIGNAL(AllJobsComplete()),
|
|
||||||
SLOT(AllTranscodingJobsComplete()));
|
|
||||||
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
|
|
||||||
connect(this, SIGNAL(RippingComplete()), SLOT(ThreadedTranscoding()));
|
|
||||||
connect(this, SIGNAL(SignalUpdateProgress()), SLOT(UpdateProgress()));
|
|
||||||
|
|
||||||
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
|
|
||||||
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
|
|
||||||
|
|
||||||
setWindowTitle(tr("Rip CD"));
|
|
||||||
AddDestinationDirectory(QDir::homePath());
|
|
||||||
|
|
||||||
// Get presets
|
|
||||||
QList<TranscoderPreset> presets = Transcoder::GetAllPresets();
|
|
||||||
qSort(presets.begin(), presets.end(), ComparePresetsByName);
|
|
||||||
for (const TranscoderPreset& preset : presets) {
|
|
||||||
ui_->format->addItem(
|
|
||||||
QString("%1 (.%2)").arg(preset.name_, preset.extension_),
|
|
||||||
QVariant::fromValue(preset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load settings
|
|
||||||
QSettings s;
|
|
||||||
s.beginGroup(kSettingsGroup);
|
|
||||||
last_add_dir_ = s.value("last_add_dir", QDir::homePath()).toString();
|
|
||||||
|
|
||||||
QString last_output_format = s.value("last_output_format", "ogg").toString();
|
|
||||||
for (int i = 0; i < ui_->format->count(); ++i) {
|
|
||||||
if (last_output_format ==
|
|
||||||
ui_->format->itemData(i).value<TranscoderPreset>().extension_) {
|
|
||||||
ui_->format->setCurrentIndex(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui_->progress_bar->setValue(0);
|
|
||||||
ui_->progress_bar->setMaximum(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
RipCD::~RipCD() { cdio_destroy(cdio_); }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* WAV Header documentation
|
|
||||||
* as taken from:
|
|
||||||
* http://www.topherlee.com/software/pcm-tut-wavformat.html
|
|
||||||
* Pos Value Description
|
|
||||||
* 0-3 | "RIFF" | Marks the file as a riff file.
|
|
||||||
* | Characters are each 1 byte long.
|
|
||||||
* 4-7 | File size (integer) | Size of the overall file - 8 bytes,
|
|
||||||
* | in bytes (32-bit integer).
|
|
||||||
* 8-11 | "WAVE" | File Type Header. For our purposes,
|
|
||||||
* | it always equals "WAVE".
|
|
||||||
* 13-16 | "fmt " | Format chunk marker. Includes trailing null.
|
|
||||||
* 17-20 | 16 | Length of format data as listed above
|
|
||||||
* 21-22 | 1 | Type of format (1 is PCM) - 2 byte integer
|
|
||||||
* 23-24 | 2 | Number of Channels - 2 byte integer
|
|
||||||
* 25-28 | 44100 | Sample Rate - 32 byte integer. Common values
|
|
||||||
* | are 44100 (CD), 48000 (DAT).
|
|
||||||
* | Sample Rate = Number of Samples per second, or Hertz.
|
|
||||||
* 29-32 | 176400 | (Sample Rate * BitsPerSample * Channels) / 8.
|
|
||||||
* 33-34 | 4 | (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16
|
|
||||||
* bit mono4 - 16 bit stereo
|
|
||||||
* 35-36 | 16 | Bits per sample
|
|
||||||
* 37-40 | "data" | "data" chunk header.
|
|
||||||
* | Marks the beginning of the data section.
|
|
||||||
* 41-44 | File size (data) | Size of the data section.
|
|
||||||
*/
|
|
||||||
void RipCD::WriteWAVHeader(QFile* stream, int32_t i_bytecount) {
|
|
||||||
QDataStream data_stream(stream);
|
|
||||||
data_stream.setByteOrder(QDataStream::LittleEndian);
|
|
||||||
// sizeof() - 1 to avoid including "\0" in the file too
|
|
||||||
data_stream.writeRawData(kWavHeaderRiffMarker,
|
|
||||||
sizeof(kWavHeaderRiffMarker) - 1); /* 0-3 */
|
|
||||||
data_stream << qint32(i_bytecount + 44 - 8); /* 4-7 */
|
|
||||||
data_stream.writeRawData(kWavFileTypeFormatChunk,
|
|
||||||
sizeof(kWavFileTypeFormatChunk) - 1); /* 8-15 */
|
|
||||||
data_stream << (qint32)16; /* 16-19 */
|
|
||||||
data_stream << (qint16)1; /* 20-21 */
|
|
||||||
data_stream << (qint16)2; /* 22-23 */
|
|
||||||
data_stream << (qint32)44100; /* 24-27 */
|
|
||||||
data_stream << (qint32)(44100 * 2 * 2); /* 28-31 */
|
|
||||||
data_stream << (qint16)4; /* 32-33 */
|
|
||||||
data_stream << (qint16)16; /* 34-35 */
|
|
||||||
data_stream.writeRawData(kWavDataString,
|
|
||||||
sizeof(kWavDataString) - 1); /* 36-39 */
|
|
||||||
data_stream << (qint32)i_bytecount; /* 40-43 */
|
|
||||||
}
|
|
||||||
|
|
||||||
int RipCD::NumTracksToRip() {
|
|
||||||
int k = 0;
|
|
||||||
for (int i = 0; i < checkboxes_.length(); i++) {
|
|
||||||
if (checkboxes_.value(i)->isChecked()) {
|
|
||||||
k++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return k;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::ThreadClickedRipButton() {
|
|
||||||
temporary_directory_ = Utilities::MakeTempDir() + "/";
|
|
||||||
finished_success_ = 0;
|
|
||||||
finished_failed_ = 0;
|
|
||||||
ui_->progress_bar->setMaximum(NumTracksToRip() * 2 * 100);
|
|
||||||
|
|
||||||
// Set up progress bar
|
|
||||||
emit(SignalUpdateProgress());
|
|
||||||
|
|
||||||
for (const TrackInformation& track : tracks_) {
|
|
||||||
QString filename =
|
|
||||||
QString("%1%2.wav").arg(temporary_directory_).arg(track.track_number);
|
|
||||||
QFile* destination_file = new QFile(filename);
|
|
||||||
destination_file->open(QIODevice::WriteOnly);
|
|
||||||
|
|
||||||
lsn_t i_first_lsn = cdio_get_track_lsn(cdio_, track.track_number);
|
|
||||||
lsn_t i_last_lsn = cdio_get_track_last_lsn(cdio_, track.track_number);
|
|
||||||
WriteWAVHeader(destination_file,
|
|
||||||
(i_last_lsn - i_first_lsn + 1) * CDIO_CD_FRAMESIZE_RAW);
|
|
||||||
|
|
||||||
QByteArray buffered_input_bytes(CDIO_CD_FRAMESIZE_RAW, '\0');
|
|
||||||
for (lsn_t i_cursor = i_first_lsn; i_cursor <= i_last_lsn; i_cursor++) {
|
|
||||||
{
|
|
||||||
QMutexLocker l(&mutex_);
|
|
||||||
if (cancel_requested_) {
|
|
||||||
qLog(Debug) << "CD ripping canceled.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cdio_read_audio_sector(cdio_, buffered_input_bytes.data(),
|
|
||||||
i_cursor) == DRIVER_OP_SUCCESS) {
|
|
||||||
destination_file->write(buffered_input_bytes.data(),
|
|
||||||
buffered_input_bytes.size());
|
|
||||||
} else {
|
|
||||||
qLog(Error) << "CD read error";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finished_success_++;
|
|
||||||
emit(SignalUpdateProgress());
|
|
||||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
|
||||||
.value<TranscoderPreset>();
|
|
||||||
|
|
||||||
transcoder_->AddJob(filename, preset, track.transcoded_filename);
|
|
||||||
}
|
|
||||||
emit(RippingComplete());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RipCD::GetOutputFileName(const QString& basename) const {
|
|
||||||
QString path =
|
|
||||||
ui_->destination->itemData(ui_->destination->currentIndex()).toString();
|
|
||||||
QString extension = ui_->format->itemData(ui_->format->currentIndex())
|
|
||||||
.value<TranscoderPreset>()
|
|
||||||
.extension_;
|
|
||||||
return path + '/' + basename + '.' + extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RipCD::ParseFileFormatString(const QString& file_format,
|
|
||||||
int track_no) const {
|
|
||||||
QString to_return = file_format;
|
|
||||||
to_return.replace(QString("%artist%"), ui_->artistLineEdit->text());
|
|
||||||
to_return.replace(QString("%album%"), ui_->albumLineEdit->text());
|
|
||||||
to_return.replace(QString("%genre%"), ui_->genreLineEdit->text());
|
|
||||||
to_return.replace(QString("%year%"), ui_->yearLineEdit->text());
|
|
||||||
to_return.replace(QString("%tracknum%"), QString::number(track_no));
|
|
||||||
to_return.replace(QString("%track%"),
|
|
||||||
track_names_.value(track_no - 1)->text());
|
|
||||||
return to_return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::UpdateProgress() {
|
|
||||||
int progress = (finished_success_ + finished_failed_) * 100;
|
|
||||||
QMap<QString, float> current_jobs = transcoder_->GetProgress();
|
|
||||||
for (float value : current_jobs.values()) {
|
|
||||||
progress += qBound(0, static_cast<int>(value * 100), 99);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui_->progress_bar->setValue(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::ThreadedTranscoding() {
|
|
||||||
transcoder_->Start();
|
|
||||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
|
||||||
.value<TranscoderPreset>();
|
|
||||||
// Save the last output format
|
|
||||||
QSettings s;
|
|
||||||
s.beginGroup(kSettingsGroup);
|
|
||||||
s.setValue("last_output_format", preset.extension_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::ClickedRipButton() {
|
|
||||||
if (cdio_ && cdio_get_media_changed(cdio_)) {
|
|
||||||
QMessageBox cdio_fail(QMessageBox::Critical, tr("Error Ripping CD"),
|
|
||||||
tr("Media has changed. Reloading"));
|
|
||||||
cdio_fail.exec();
|
|
||||||
if (CheckCDIOIsValid()) {
|
|
||||||
BuildTrackListTable();
|
|
||||||
} else {
|
|
||||||
ui_->tableWidget->clearContents();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tracks to the rip list.
|
|
||||||
tracks_.clear();
|
|
||||||
for (int i = 1; i <= i_tracks_; ++i) {
|
|
||||||
if (!checkboxes_.value(i - 1)->isChecked()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
QString transcoded_filename = GetOutputFileName(
|
|
||||||
ParseFileFormatString(ui_->format_filename->text(), i));
|
|
||||||
QString title = track_names_.value(i - 1)->text();
|
|
||||||
AddTrack(i, title, transcoded_filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do nothing if no tracks are selected.
|
|
||||||
if (tracks_.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Start ripping.
|
|
||||||
SetWorking(true);
|
|
||||||
{
|
|
||||||
QMutexLocker l(&mutex_);
|
|
||||||
cancel_requested_ = false;
|
|
||||||
}
|
|
||||||
QtConcurrent::run(this, &RipCD::ThreadClickedRipButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::AddTrack(int track_number, const QString& title,
|
|
||||||
const QString& transcoded_filename) {
|
|
||||||
TrackInformation track(track_number, title, transcoded_filename);
|
|
||||||
tracks_.append(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::TranscodingJobComplete(const QString& input, const QString& output, bool success) {
|
|
||||||
(*(success ? &finished_success_ : &finished_failed_))++;
|
|
||||||
emit(SignalUpdateProgress());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::AllTranscodingJobsComplete() {
|
|
||||||
RemoveTemporaryDirectory();
|
|
||||||
|
|
||||||
// Save tags.
|
|
||||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
|
||||||
.value<TranscoderPreset>();
|
|
||||||
AlbumInformation album(
|
|
||||||
ui_->albumLineEdit->text(), ui_->artistLineEdit->text(),
|
|
||||||
ui_->genreLineEdit->text(), ui_->yearLineEdit->text().toInt(),
|
|
||||||
ui_->discLineEdit->text().toInt(), preset.type_);
|
|
||||||
TagFiles(album, tracks_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::TagFiles(const AlbumInformation& album,
|
|
||||||
const QList<TrackInformation>& tracks) {
|
|
||||||
files_tagged_ = 0;
|
|
||||||
for (const TrackInformation& track : tracks_) {
|
|
||||||
Song song;
|
|
||||||
song.InitFromFilePartial(track.transcoded_filename);
|
|
||||||
song.set_track(track.track_number);
|
|
||||||
song.set_title(track.title);
|
|
||||||
song.set_album(album.album);
|
|
||||||
song.set_artist(album.artist);
|
|
||||||
song.set_genre(album.genre);
|
|
||||||
song.set_year(album.year);
|
|
||||||
song.set_disc(album.disc);
|
|
||||||
song.set_filetype(album.type);
|
|
||||||
|
|
||||||
TagReaderReply* reply =
|
|
||||||
TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
|
|
||||||
NewClosure(reply, SIGNAL(Finished(bool)), this,
|
|
||||||
SLOT(FileTagged(TagReaderReply*)), reply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::FileTagged(TagReaderReply* reply) {
|
|
||||||
files_tagged_++;
|
|
||||||
qLog(Debug) << "Tagged" << files_tagged_ << "of" << tracks_.length()
|
|
||||||
<< "files";
|
|
||||||
|
|
||||||
// Stop working if all files are tagged.
|
|
||||||
if (files_tagged_ == tracks_.length()) {
|
|
||||||
qLog(Debug) << "CD ripper finished.";
|
|
||||||
SetWorking(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
reply->deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::Options() {
|
|
||||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
|
||||||
.value<TranscoderPreset>();
|
|
||||||
|
|
||||||
TranscoderOptionsDialog dialog(preset.type_, this);
|
|
||||||
if (dialog.is_valid()) {
|
|
||||||
dialog.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a folder to the destination box.
|
|
||||||
void RipCD::AddDestination() {
|
|
||||||
int index = ui_->destination->currentIndex();
|
|
||||||
QString initial_dir = (!ui_->destination->itemData(index).isNull()
|
|
||||||
? ui_->destination->itemData(index).toString()
|
|
||||||
: QDir::homePath());
|
|
||||||
QString dir =
|
|
||||||
QFileDialog::getExistingDirectory(this, tr("Add folder"), initial_dir);
|
|
||||||
|
|
||||||
if (!dir.isEmpty()) {
|
|
||||||
// Keep only a finite number of items in the box.
|
|
||||||
while (ui_->destination->count() >= kMaxDestinationItems) {
|
|
||||||
ui_->destination->removeItem(0); // The oldest item.
|
|
||||||
}
|
|
||||||
AddDestinationDirectory(dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a directory to the 'destination' combo box.
|
|
||||||
void RipCD::AddDestinationDirectory(QString dir) {
|
|
||||||
QIcon icon = IconLoader::Load("folder");
|
|
||||||
QVariant data = QVariant::fromValue(dir);
|
|
||||||
// Do not insert duplicates.
|
|
||||||
int duplicate_index = ui_->destination->findData(data);
|
|
||||||
if (duplicate_index == -1) {
|
|
||||||
ui_->destination->addItem(icon, dir, data);
|
|
||||||
ui_->destination->setCurrentIndex(ui_->destination->count() - 1);
|
|
||||||
} else {
|
|
||||||
ui_->destination->setCurrentIndex(duplicate_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::Cancel() {
|
|
||||||
{
|
|
||||||
QMutexLocker l(&mutex_);
|
|
||||||
cancel_requested_ = true;
|
|
||||||
}
|
|
||||||
ui_->progress_bar->setValue(0);
|
|
||||||
transcoder_->Cancel();
|
|
||||||
RemoveTemporaryDirectory();
|
|
||||||
SetWorking(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RipCD::CheckCDIOIsValid() {
|
|
||||||
if (cdio_) {
|
|
||||||
cdio_destroy(cdio_);
|
|
||||||
}
|
|
||||||
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
|
||||||
// Refresh the status of the cd media. This will prevent unnecessary
|
|
||||||
// rebuilds of the track list table.
|
|
||||||
cdio_get_media_changed(cdio_);
|
|
||||||
return cdio_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::SetWorking(bool working) {
|
|
||||||
rip_button_->setVisible(!working);
|
|
||||||
cancel_button_->setVisible(working);
|
|
||||||
close_button_->setVisible(!working);
|
|
||||||
ui_->input_group->setEnabled(!working);
|
|
||||||
ui_->output_group->setEnabled(!working);
|
|
||||||
ui_->progress_group->setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::SelectAll() {
|
|
||||||
for (QCheckBox* checkbox : checkboxes_) {
|
|
||||||
checkbox->setCheckState(Qt::Checked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::SelectNone() {
|
|
||||||
for (QCheckBox* checkbox : checkboxes_) {
|
|
||||||
checkbox->setCheckState(Qt::Unchecked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::InvertSelection() {
|
|
||||||
for (QCheckBox* checkbox : checkboxes_) {
|
|
||||||
if (checkbox->isChecked()) {
|
|
||||||
checkbox->setCheckState(Qt::Unchecked);
|
|
||||||
} else {
|
|
||||||
checkbox->setCheckState(Qt::Checked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::RemoveTemporaryDirectory() {
|
|
||||||
if (!temporary_directory_.isEmpty())
|
|
||||||
Utilities::RemoveRecursive(temporary_directory_);
|
|
||||||
temporary_directory_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::BuildTrackListTable() {
|
|
||||||
checkboxes_.clear();
|
|
||||||
track_names_.clear();
|
|
||||||
|
|
||||||
i_tracks_ = cdio_get_num_tracks(cdio_);
|
|
||||||
// Build an empty table if there is an error, e.g. no medium found.
|
|
||||||
if (i_tracks_ == CDIO_INVALID_TRACK)
|
|
||||||
i_tracks_ = 0;
|
|
||||||
|
|
||||||
ui_->tableWidget->setRowCount(i_tracks_);
|
|
||||||
for (int i = 1; i <= i_tracks_; i++) {
|
|
||||||
QCheckBox* checkbox_i = new QCheckBox(ui_->tableWidget);
|
|
||||||
checkbox_i->setCheckState(Qt::Checked);
|
|
||||||
checkboxes_.append(checkbox_i);
|
|
||||||
ui_->tableWidget->setCellWidget(i - 1, kCheckboxColumn, checkbox_i);
|
|
||||||
ui_->tableWidget->setCellWidget(i - 1, kTrackNumberColumn,
|
|
||||||
new QLabel(QString::number(i)));
|
|
||||||
QString track_title = QString("Track %1").arg(i);
|
|
||||||
QLineEdit* line_edit_track_title_i =
|
|
||||||
new QLineEdit(track_title, ui_->tableWidget);
|
|
||||||
track_names_.append(line_edit_track_title_i);
|
|
||||||
ui_->tableWidget->setCellWidget(i - 1, kTrackTitleColumn,
|
|
||||||
line_edit_track_title_i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RipCD::LogLine(const QString& message) { qLog(Debug) << message; }
|
|
||||||
|
|
||||||
void RipCD::showEvent(QShowEvent* event) { BuildTrackListTable(); }
|
|
139
src/ui/ripcd.h
139
src/ui/ripcd.h
@ -1,139 +0,0 @@
|
|||||||
/* This file is part of Clementine.
|
|
||||||
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
|
||||||
|
|
||||||
Clementine 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.
|
|
||||||
|
|
||||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef SRC_UI_RIPCD_H_
|
|
||||||
#define SRC_UI_RIPCD_H_
|
|
||||||
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QThread>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QMutex>
|
|
||||||
#include <cdio/cdio.h>
|
|
||||||
#include "core/song.h"
|
|
||||||
#include "core/tagreaderclient.h"
|
|
||||||
#include "ui_ripcd.h"
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class Ui_RipCD;
|
|
||||||
class Transcoder;
|
|
||||||
|
|
||||||
struct TranscoderPreset;
|
|
||||||
|
|
||||||
class RipCD : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit RipCD(QWidget* parent = nullptr);
|
|
||||||
~RipCD();
|
|
||||||
bool CheckCDIOIsValid();
|
|
||||||
void BuildTrackListTable();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void showEvent(QShowEvent* event);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct TrackInformation {
|
|
||||||
TrackInformation(int track_number, const QString& title,
|
|
||||||
const QString& transcoded_filename)
|
|
||||||
: track_number(track_number),
|
|
||||||
title(title),
|
|
||||||
transcoded_filename(transcoded_filename) {}
|
|
||||||
|
|
||||||
int track_number;
|
|
||||||
QString title;
|
|
||||||
QString transcoded_filename;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AlbumInformation {
|
|
||||||
AlbumInformation(const QString& album, const QString& artist,
|
|
||||||
const QString& genre, int year, int disc,
|
|
||||||
Song::FileType type)
|
|
||||||
: album(album),
|
|
||||||
artist(artist),
|
|
||||||
genre(genre),
|
|
||||||
year(year),
|
|
||||||
disc(disc),
|
|
||||||
type(type) {}
|
|
||||||
|
|
||||||
QString album;
|
|
||||||
QString artist;
|
|
||||||
QString genre;
|
|
||||||
int year;
|
|
||||||
int disc;
|
|
||||||
Song::FileType type;
|
|
||||||
};
|
|
||||||
|
|
||||||
static const char* kSettingsGroup;
|
|
||||||
static const int kProgressInterval;
|
|
||||||
static const int kMaxDestinationItems;
|
|
||||||
Transcoder* transcoder_;
|
|
||||||
int queued_;
|
|
||||||
int finished_success_;
|
|
||||||
int finished_failed_;
|
|
||||||
track_t i_tracks_;
|
|
||||||
std::unique_ptr<Ui_RipCD> ui_;
|
|
||||||
CdIo_t* cdio_;
|
|
||||||
QList<QCheckBox*> checkboxes_;
|
|
||||||
QList<QLineEdit*> track_names_;
|
|
||||||
QList<TrackInformation> tracks_;
|
|
||||||
QString last_add_dir_;
|
|
||||||
QPushButton* cancel_button_;
|
|
||||||
QPushButton* close_button_;
|
|
||||||
QPushButton* rip_button_;
|
|
||||||
QString temporary_directory_;
|
|
||||||
bool cancel_requested_;
|
|
||||||
QMutex mutex_;
|
|
||||||
int files_tagged_;
|
|
||||||
|
|
||||||
void WriteWAVHeader(QFile* stream, int32_t i_bytecount);
|
|
||||||
int NumTracksToRip();
|
|
||||||
void AddTrack(int track_number, const QString& title,
|
|
||||||
const QString& transcoded_filename);
|
|
||||||
void ThreadClickedRipButton();
|
|
||||||
// Constructs a filename from the given base name with a path taken
|
|
||||||
// from the ui dialog and an extension that corresponds to the audio
|
|
||||||
// format chosen in the ui.
|
|
||||||
QString GetOutputFileName(const QString& basename) const;
|
|
||||||
QString ParseFileFormatString(const QString& file_format, int track_no) const;
|
|
||||||
void SetWorking(bool working);
|
|
||||||
void AddDestinationDirectory(QString dir);
|
|
||||||
void RemoveTemporaryDirectory();
|
|
||||||
void TagFiles(const AlbumInformation& album,
|
|
||||||
const QList<TrackInformation>& tracks);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void RippingComplete();
|
|
||||||
void SignalUpdateProgress();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void UpdateProgress();
|
|
||||||
void ThreadedTranscoding();
|
|
||||||
void ClickedRipButton();
|
|
||||||
void TranscodingJobComplete(const QString& input, const QString& output, bool success);
|
|
||||||
void AllTranscodingJobsComplete();
|
|
||||||
void FileTagged(TagReaderReply* reply);
|
|
||||||
void Options();
|
|
||||||
void AddDestination();
|
|
||||||
void Cancel();
|
|
||||||
void SelectAll();
|
|
||||||
void SelectNone();
|
|
||||||
void InvertSelection();
|
|
||||||
void LogLine(const QString& message);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SRC_UI_RIPCD_H_
|
|
Loading…
x
Reference in New Issue
Block a user