Avoid having similar filenames when organising songs (number them instead)

This commit is contained in:
Arnaud Bienner 2014-02-02 19:28:45 +01:00
parent 96075faf88
commit a349a96f5a
7 changed files with 138 additions and 21 deletions

View File

@ -36,7 +36,7 @@ const int Organise::kTranscodeProgressInterval = 500;
Organise::Organise(TaskManager* task_manager, Organise::Organise(TaskManager* task_manager,
boost::shared_ptr<MusicStorage> destination, boost::shared_ptr<MusicStorage> destination,
const OrganiseFormat &format, bool copy, bool overwrite, const OrganiseFormat &format, bool copy, bool overwrite,
const SongList& songs, bool eject_after) const NewSongInfoList& songs_info, bool eject_after)
: thread_(NULL), : thread_(NULL),
task_manager_(task_manager), task_manager_(task_manager),
transcoder_(new Transcoder(this)), transcoder_(new Transcoder(this)),
@ -45,7 +45,7 @@ Organise::Organise(TaskManager* task_manager,
copy_(copy), copy_(copy),
overwrite_(overwrite), overwrite_(overwrite),
eject_after_(eject_after), eject_after_(eject_after),
task_count_(songs.count()), task_count_(songs_info.count()),
transcode_suffix_(1), transcode_suffix_(1),
tasks_complete_(0), tasks_complete_(0),
started_(false), started_(false),
@ -54,8 +54,8 @@ Organise::Organise(TaskManager* task_manager,
{ {
original_thread_ = thread(); original_thread_ = thread();
for (const Song& song : songs) { for (const NewSongInfo& song_info : songs_info) {
tasks_pending_ << Task(song); tasks_pending_ << Task(song_info);
} }
} }
@ -81,7 +81,7 @@ void Organise::ProcessSomeFiles() {
if (!destination_->StartCopy(&supported_filetypes_)) { if (!destination_->StartCopy(&supported_filetypes_)) {
// Failed to start - mark everything as failed :( // Failed to start - mark everything as failed :(
for (const Task& task : tasks_pending_) for (const Task& task : tasks_pending_)
files_with_errors_ << task.song_.url().toLocalFile(); files_with_errors_ << task.song_info_.song_.url().toLocalFile();
tasks_pending_.clear(); tasks_pending_.clear();
} }
started_ = true; started_ = true;
@ -124,10 +124,10 @@ void Organise::ProcessSomeFiles() {
break; break;
Task task = tasks_pending_.takeFirst(); Task task = tasks_pending_.takeFirst();
qLog(Info) << "Processing" << task.song_.url().toLocalFile(); qLog(Info) << "Processing" << task.song_info_.song_.url().toLocalFile();
// Use a Song instead of a tag reader // Use a Song instead of a tag reader
Song song = task.song_; Song song = task.song_info_.song_;
if (!song.is_valid()) if (!song.is_valid())
continue; continue;
@ -157,14 +157,14 @@ void Organise::ProcessSomeFiles() {
QString::number(transcode_suffix_++); QString::number(transcode_suffix_++);
task.new_extension_ = preset.extension_; task.new_extension_ = preset.extension_;
task.new_filetype_ = dest_type; task.new_filetype_ = dest_type;
tasks_transcoding_[task.song_.url().toLocalFile()] = task; tasks_transcoding_[task.song_info_.song_.url().toLocalFile()] = task;
qLog(Debug) << "Transcoding to" << task.transcoded_filename_; qLog(Debug) << "Transcoding to" << task.transcoded_filename_;
// Start the transcoding - this will happen in the background and // Start the transcoding - this will happen in the background and
// FileTranscoded() will get called when it's done. At that point the // 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. // task will get re-added to the pending queue with the new filename.
transcoder_->AddJob(task.song_.url().toLocalFile(), preset, task.transcoded_filename_); transcoder_->AddJob(task.song_info_.song_.url().toLocalFile(), preset, task.transcoded_filename_);
transcoder_->Start(); transcoder_->Start();
continue; continue;
} }
@ -172,8 +172,8 @@ void Organise::ProcessSomeFiles() {
MusicStorage::CopyJob job; MusicStorage::CopyJob job;
job.source_ = task.transcoded_filename_.isEmpty() ? job.source_ = task.transcoded_filename_.isEmpty() ?
task.song_.url().toLocalFile() : task.transcoded_filename_; task.song_info_.song_.url().toLocalFile() : task.transcoded_filename_;
job.destination_ = format_.GetFilenameForSong(song); job.destination_ = task.song_info_.new_filename_;
job.metadata_ = song; job.metadata_ = song;
job.overwrite_ = overwrite_; job.overwrite_ = overwrite_;
job.remove_original_ = !copy_; job.remove_original_ = !copy_;
@ -181,7 +181,7 @@ void Organise::ProcessSomeFiles() {
this, _1, !task.transcoded_filename_.isEmpty()); this, _1, !task.transcoded_filename_.isEmpty());
if (!destination_->CopyToStorage(job)) { if (!destination_->CopyToStorage(job)) {
files_with_errors_ << task.song_.basefilename(); files_with_errors_ << task.song_info_.song_.basefilename();
} }
// Clean up the temporary transcoded file // Clean up the temporary transcoded file

View File

@ -34,10 +34,19 @@ class Organise : public QObject {
Q_OBJECT Q_OBJECT
public: public:
struct NewSongInfo {
NewSongInfo(const Song& song = Song(), const QString& new_filename = QString())
: song_(song), new_filename_(new_filename) {}
Song song_;
QString new_filename_;
};
typedef QList<NewSongInfo> NewSongInfoList;
Organise(TaskManager* task_manager, Organise(TaskManager* task_manager,
boost::shared_ptr<MusicStorage> destination, boost::shared_ptr<MusicStorage> destination,
const OrganiseFormat& format, bool copy, bool overwrite, const OrganiseFormat& format, bool copy, bool overwrite,
const SongList& songs, bool eject_after); const NewSongInfoList& songs, bool eject_after);
static const int kBatchSize; static const int kBatchSize;
static const int kTranscodeProgressInterval; static const int kTranscodeProgressInterval;
@ -61,10 +70,10 @@ private:
private: private:
struct Task { struct Task {
explicit Task(const Song& song = Song()) explicit Task(const NewSongInfo& song_info = NewSongInfo())
: song_(song), transcode_progress_(0.0) {} : song_info_(song_info), transcode_progress_(0.0) {}
Song song_; NewSongInfo song_info_;
float transcode_progress_; float transcode_progress_;
QString transcoded_filename_; QString transcoded_filename_;

View File

@ -161,8 +161,7 @@ QString OrganiseFormat::TagValue(const QString &tag, const Song &song) const {
QString::number(song.length_nanosec() / kNsecPerSec); QString::number(song.length_nanosec() / kNsecPerSec);
else if (tag == "bitrate") value = QString::number(song.bitrate()); else if (tag == "bitrate") value = QString::number(song.bitrate());
else if (tag == "samplerate") value = QString::number(song.samplerate()); else if (tag == "samplerate") value = QString::number(song.samplerate());
else if (tag == "extension") value = else if (tag == "extension") value = QFileInfo(song.url().toLocalFile()).suffix();
song.url().toLocalFile().section('.', -1, -1);
else if (tag == "artistinitial") { else if (tag == "artistinitial") {
value = song.effective_albumartist().trimmed(); value = song.effective_albumartist().trimmed();
if (replace_the_ && !value.isEmpty()) if (replace_the_ && !value.isEmpty())

View File

@ -22,9 +22,11 @@
#include "core/musicstorage.h" #include "core/musicstorage.h"
#include "core/organise.h" #include "core/organise.h"
#include "core/tagreaderclient.h" #include "core/tagreaderclient.h"
#include "core/utilities.h"
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QHash>
#include <QMenu> #include <QMenu>
#include <QPushButton> #include <QPushButton>
#include <QResizeEvent> #include <QResizeEvent>
@ -162,6 +164,29 @@ void OrganiseDialog::InsertTag(const QString &tag) {
ui_->naming->insertPlainText("%" + tag); ui_->naming->insertPlainText("%" + tag);
} }
Organise::NewSongInfoList OrganiseDialog::ComputeNewSongsFilenames(
const SongList& songs, const OrganiseFormat& format) {
// Check if we will have multiple files with the same name.
// If so, they will erase each other if the overwrite flag is set.
// Better to rename them: e.g. foo.bar -> foo(2).bar
QHash<QString, int> filenames;
Organise::NewSongInfoList new_songs_info;
for (const Song& song : songs) {
QString new_filename = format.GetFilenameForSong(song);
if (filenames.contains(new_filename)) {
QString song_number = QString::number(++filenames[new_filename]);
new_filename =
Utilities::PathWithoutFilenameExtension(new_filename) +
"(" + song_number + ")." + QFileInfo(new_filename).suffix();
}
filenames.insert(new_filename, 1);
new_songs_info << Organise::NewSongInfo(song, new_filename);
}
return new_songs_info;
}
void OrganiseDialog::UpdatePreviews() { void OrganiseDialog::UpdatePreviews() {
const QModelIndex destination = ui_->destination->model()->index( const QModelIndex destination = ui_->destination->model()->index(
ui_->destination->currentIndex(), 0); ui_->destination->currentIndex(), 0);
@ -205,14 +230,16 @@ void OrganiseDialog::UpdatePreviews() {
if (!format_valid) if (!format_valid)
return; return;
new_songs_info_ = ComputeNewSongsFilenames(songs_, format_);
// Update the previews // Update the previews
ui_->preview->clear(); ui_->preview->clear();
ui_->preview_group->setVisible(has_local_destination); ui_->preview_group->setVisible(has_local_destination);
ui_->naming_group->setVisible(has_local_destination); ui_->naming_group->setVisible(has_local_destination);
if (has_local_destination) { if (has_local_destination) {
for (const Song& song : songs_) { for (const Organise::NewSongInfo& song_info : new_songs_info_) {
QString filename = storage->LocalPath() + "/" + QString filename = storage->LocalPath() + "/" +
format_.GetFilenameForSong(song); song_info.new_filename_;
ui_->preview->addItem(QDir::toNativeSeparators(filename)); ui_->preview->addItem(QDir::toNativeSeparators(filename));
} }
} }
@ -278,7 +305,7 @@ void OrganiseDialog::accept() {
const bool copy = ui_->aftercopying->currentIndex() == 0; const bool copy = ui_->aftercopying->currentIndex() == 0;
Organise* organise = new Organise( Organise* organise = new Organise(
task_manager_, storage, format_, copy, ui_->overwrite->isChecked(), task_manager_, storage, format_, copy, ui_->overwrite->isChecked(),
songs_, ui_->eject_after->isChecked()); new_songs_info_, ui_->eject_after->isChecked());
connect(organise, SIGNAL(Finished(QStringList)), SLOT(OrganiseFinished(QStringList))); connect(organise, SIGNAL(Finished(QStringList)), SLOT(OrganiseFinished(QStringList)));
organise->Start(); organise->Start();

View File

@ -23,6 +23,9 @@
#include <QMap> #include <QMap>
#include <QUrl> #include <QUrl>
#include "gtest/gtest_prod.h"
#include "core/organise.h"
#include "core/organiseformat.h" #include "core/organiseformat.h"
#include "core/song.h" #include "core/song.h"
@ -68,17 +71,24 @@ private slots:
void OrganiseFinished(const QStringList& files_with_errors); void OrganiseFinished(const QStringList& files_with_errors);
private: private:
static Organise::NewSongInfoList ComputeNewSongsFilenames(
const SongList& songs,
const OrganiseFormat& format);
Ui_OrganiseDialog* ui_; Ui_OrganiseDialog* ui_;
TaskManager* task_manager_; TaskManager* task_manager_;
OrganiseFormat format_; OrganiseFormat format_;
SongList songs_; SongList songs_;
Organise::NewSongInfoList new_songs_info_;
quint64 total_size_; quint64 total_size_;
std::unique_ptr<OrganiseErrorDialog> error_dialog_; std::unique_ptr<OrganiseErrorDialog> error_dialog_;
bool resized_by_user_; bool resized_by_user_;
FRIEND_TEST(OrganiseDialogTest, ComputeNewSongsFilenamesTest);
}; };
#endif // ORGANISEDIALOG_H #endif // ORGANISEDIALOG_H

View File

@ -132,6 +132,7 @@ add_test_file(fmpsparser_test.cpp false)
#add_test_file(m3uparser_test.cpp false) #add_test_file(m3uparser_test.cpp false)
add_test_file(mergedproxymodel_test.cpp false) add_test_file(mergedproxymodel_test.cpp false)
add_test_file(organiseformat_test.cpp false) add_test_file(organiseformat_test.cpp false)
add_test_file(organisedialog_test.cpp false)
#add_test_file(playlist_test.cpp true) #add_test_file(playlist_test.cpp true)
#add_test_file(plsparser_test.cpp false) #add_test_file(plsparser_test.cpp false)
add_test_file(scopedtransaction_test.cpp false) add_test_file(scopedtransaction_test.cpp false)

View File

@ -0,0 +1,71 @@
/* This file is part of Clementine.
Copyright 2014, David Sansome <me@davidsansome.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 "gtest/gtest.h"
#include "test_utils.h"
#include "core/song.h"
#include "core/organiseformat.h"
#include "ui/organisedialog.h"
TEST(OrganiseDialogTest, ComputeNewSongsFilenamesTest) {
// Create some songs, with multiple similar songs
SongList songs;
{
Song song;
song.set_title("Test1");
song.set_album("Album");
songs << song;
}
// Empty song
{
Song song;
song.set_basefilename("filename.mp3");
songs << song;
}
// Without extension
for (int i = 0; i < 2; i++) {
Song song;
song.set_title("Test2");
song.set_url(QUrl("file://test" + QString::number(i)));
songs << song;
}
// With file extension
for (int i = 0; i < 3; i++) {
Song song;
song.set_artist("Foo");
song.set_title("Bar");
song.set_url(QUrl("file://foobar" + QString::number(i) + ".mp3"));
songs << song;
}
// Generate new filenames
OrganiseFormat format;
format.set_format(OrganiseDialog::kDefaultFormat);
Organise::NewSongInfoList new_songs_info = OrganiseDialog::ComputeNewSongsFilenames(songs, format);
EXPECT_EQ("/Album/Test1.", new_songs_info[0].new_filename_);
EXPECT_EQ("//filename.mp3", new_songs_info[1].new_filename_);
EXPECT_EQ("//Test2.", new_songs_info[2].new_filename_);
EXPECT_EQ("//Test2(2).", new_songs_info[3].new_filename_);
EXPECT_EQ("Foo//Bar.mp3", new_songs_info[4].new_filename_);
EXPECT_EQ("Foo//Bar(2).mp3", new_songs_info[5].new_filename_);
EXPECT_EQ("Foo//Bar(3).mp3", new_songs_info[6].new_filename_);
}