|
|
|
/***************************************************************************
|
|
|
|
copyright : (C) 2004-2007 by Robby Stephenson
|
|
|
|
email : robby@periapsis.org
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
|
|
* *
|
|
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
|
|
* it under the terms of version 2 of the GNU General Public License as *
|
|
|
|
* published by the Free Software Foundation; *
|
|
|
|
* *
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
#include <config.h>
|
|
|
|
|
|
|
|
#include "audiofileimporter.h"
|
|
|
|
#include "../collections/musiccollection.h"
|
|
|
|
#include "../entry.h"
|
|
|
|
#include "../field.h"
|
|
|
|
#include "../latin1literal.h"
|
|
|
|
#include "../imagefactory.h"
|
|
|
|
#include "../tellico_utils.h"
|
|
|
|
#include "../tellico_kernel.h"
|
|
|
|
#include "../progressmanager.h"
|
|
|
|
#include "../tellico_debug.h"
|
|
|
|
|
|
|
|
#ifdef HAVE_TAGLIB
|
|
|
|
#include <taglib/fileref.h>
|
|
|
|
#include <taglib/tag.h>
|
|
|
|
#include <taglib/id3v2tag.h>
|
|
|
|
#include <taglib/mpegfile.h>
|
|
|
|
#include <taglib/vorbisfile.h>
|
|
|
|
#include <taglib/flacfile.h>
|
|
|
|
#include <taglib/audioproperties.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <klocale.h>
|
|
|
|
#include <kapplication.h>
|
|
|
|
|
|
|
|
#include <tqlabel.h>
|
|
|
|
#include <tqlayout.h>
|
|
|
|
#include <tqvgroupbox.h>
|
|
|
|
#include <tqcheckbox.h>
|
|
|
|
#include <tqdir.h>
|
|
|
|
#include <tqwhatsthis.h>
|
|
|
|
|
|
|
|
using Tellico::Import::AudioFileImporter;
|
|
|
|
|
|
|
|
AudioFileImporter::AudioFileImporter(const KURL& url_) : Tellico::Import::Importer(url_)
|
|
|
|
, m_coll(0)
|
|
|
|
, m_widget(0)
|
|
|
|
, m_cancelled(false) {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool AudioFileImporter::canImport(int type) const {
|
|
|
|
return type == Data::Collection::Album;
|
|
|
|
}
|
|
|
|
|
|
|
|
Tellico::Data::CollPtr AudioFileImporter::collection() {
|
|
|
|
#ifndef HAVE_TAGLIB
|
|
|
|
return 0;
|
|
|
|
#else
|
|
|
|
|
|
|
|
if(m_coll) {
|
|
|
|
return m_coll;
|
|
|
|
}
|
|
|
|
|
|
|
|
ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Scanning audio files..."), true);
|
|
|
|
item.setTotalSteps(100);
|
|
|
|
connect(&item, TQT_SIGNAL(signalCancelled(ProgressItem*)), TQT_SLOT(slotCancel()));
|
|
|
|
ProgressItem::Done done(this);
|
|
|
|
|
|
|
|
// TODO: allow remote audio file importing
|
|
|
|
TQStringList dirs = url().path();
|
|
|
|
if(m_recursive->isChecked()) {
|
|
|
|
dirs += Tellico::findAllSubDirs(dirs[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m_cancelled) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
const bool showProgress = options() & ImportProgress;
|
|
|
|
|
|
|
|
TQStringList files;
|
|
|
|
for(TQStringList::ConstIterator it = dirs.begin(); !m_cancelled && it != dirs.end(); ++it) {
|
|
|
|
if((*it).isEmpty()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQDir dir(*it);
|
|
|
|
dir.setFilter(TQDir::Files | TQDir::Readable | TQDir::Hidden); // hidden since I want directory files
|
|
|
|
const TQStringList list = dir.entryList();
|
|
|
|
for(TQStringList::ConstIterator it2 = list.begin(); it2 != list.end(); ++it2) {
|
|
|
|
files += dir.absFilePath(*it2);
|
|
|
|
}
|
|
|
|
// kapp->processEvents(); not needed ?
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m_cancelled) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
item.setTotalSteps(files.count());
|
|
|
|
|
|
|
|
const TQString title = TQString::fromLatin1("title");
|
|
|
|
const TQString artist = TQString::fromLatin1("artist");
|
|
|
|
const TQString year = TQString::fromLatin1("year");
|
|
|
|
const TQString genre = TQString::fromLatin1("genre");
|
|
|
|
const TQString track = TQString::fromLatin1("track");
|
|
|
|
const TQString comments = TQString::fromLatin1("comments");
|
|
|
|
const TQString file = TQString::fromLatin1("file");
|
|
|
|
|
|
|
|
m_coll = new Data::MusicCollection(true);
|
|
|
|
|
|
|
|
const bool addFile = m_addFilePath->isChecked();
|
|
|
|
const bool addBitrate = m_addBitrate->isChecked();
|
|
|
|
|
|
|
|
Data::FieldPtr f;
|
|
|
|
if(addFile) {
|
|
|
|
f = m_coll->fieldByName(file);
|
|
|
|
if(!f) {
|
|
|
|
f = new Data::Field(file, i18n("Files"), Data::Field::Table);
|
|
|
|
m_coll->addField(f);
|
|
|
|
}
|
|
|
|
f->setProperty(TQString::fromLatin1("column1"), i18n("Files"));
|
|
|
|
if(addBitrate) {
|
|
|
|
f->setProperty(TQString::fromLatin1("columns"), TQChar('2'));
|
|
|
|
f->setProperty(TQString::fromLatin1("column2"), i18n("Bitrate"));
|
|
|
|
} else {
|
|
|
|
f->setProperty(TQString::fromLatin1("columns"), TQChar('1'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TQMap<TQString, Data::EntryPtr> albumMap;
|
|
|
|
|
|
|
|
TQStringList directoryFiles;
|
|
|
|
const uint stepSize = TQMAX(static_cast<size_t>(1), files.count() / 100);
|
|
|
|
|
|
|
|
bool changeTrackTitle = true;
|
|
|
|
uint j = 0;
|
|
|
|
for(TQStringList::ConstIterator it = files.begin(); !m_cancelled && it != files.end(); ++it, ++j) {
|
|
|
|
TagLib::FileRef f(TQFile::encodeName(*it));
|
|
|
|
if(f.isNull() || !f.tag()) {
|
|
|
|
if((*it).endsWith(TQString::fromLatin1("/.directory"))) {
|
|
|
|
directoryFiles += *it;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
TagLib::Tag* tag = f.tag();
|
|
|
|
TQString album = TQString(TStringToQString(tag->album())).stripWhiteSpace();
|
|
|
|
if(album.isEmpty()) {
|
|
|
|
// can't do anything since tellico entries are by album
|
|
|
|
kdWarning() << "Skipping: no album listed for " << *it << endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
int disc = discNumber(f);
|
|
|
|
if(disc > 1 && !m_coll->hasField(TQString::fromLatin1("track%1").arg(disc))) {
|
|
|
|
Data::FieldPtr f2 = new Data::Field(TQString::fromLatin1("track%1").arg(disc),
|
|
|
|
i18n("Tracks (Disc %1)").arg(disc),
|
|
|
|
Data::Field::Table);
|
|
|
|
f2->setFormatFlag(Data::Field::FormatTitle);
|
|
|
|
f2->setProperty(TQString::fromLatin1("columns"), TQChar('3'));
|
|
|
|
f2->setProperty(TQString::fromLatin1("column1"), i18n("Title"));
|
|
|
|
f2->setProperty(TQString::fromLatin1("column2"), i18n("Artist"));
|
|
|
|
f2->setProperty(TQString::fromLatin1("column3"), i18n("Length"));
|
|
|
|
m_coll->addField(f2);
|
|
|
|
if(changeTrackTitle) {
|
|
|
|
Data::FieldPtr newTrack = new Data::Field(*m_coll->fieldByName(track));
|
|
|
|
newTrack->setTitle(i18n("Tracks (Disc %1)").arg(1));
|
|
|
|
m_coll->modifyField(newTrack);
|
|
|
|
changeTrackTitle = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bool various = false;
|
|
|
|
bool exists = true;
|
|
|
|
Data::EntryPtr entry = 0;
|
|
|
|
if(!(entry = albumMap[album.lower()])) {
|
|
|
|
entry = new Data::Entry(m_coll);
|
|
|
|
albumMap.insert(album.lower(), entry);
|
|
|
|
exists = false;
|
|
|
|
}
|
|
|
|
// album entries use the album name as the title
|
|
|
|
entry->setField(title, album);
|
|
|
|
TQString a = TQString(TStringToQString(tag->artist())).stripWhiteSpace();
|
|
|
|
if(!a.isEmpty()) {
|
|
|
|
if(exists && entry->field(artist).lower() != a.lower()) {
|
|
|
|
various = true;
|
|
|
|
entry->setField(artist, i18n("(Various)"));
|
|
|
|
} else {
|
|
|
|
entry->setField(artist, a);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(tag->year() > 0) {
|
|
|
|
entry->setField(year, TQString::number(tag->year()));
|
|
|
|
}
|
|
|
|
if(!tag->genre().isEmpty()) {
|
|
|
|
entry->setField(genre, TQString(TStringToQString(tag->genre())).stripWhiteSpace());
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!tag->title().isEmpty()) {
|
|
|
|
int trackNum = tag->track();
|
|
|
|
if(trackNum <= 0) { // try to figure out track number from file name
|
|
|
|
TQFileInfo f(*it);
|
|
|
|
TQString fileName = f.baseName();
|
|
|
|
TQString numString;
|
|
|
|
int i = 0;
|
|
|
|
const int len = fileName.length();
|
|
|
|
while(fileName[i].isNumber() && i < len) {
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
if(i == 0) { // does not start with a number
|
|
|
|
i = len - 1;
|
|
|
|
while(i >= 0 && fileName[i].isNumber()) {
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
// file name ends with a number
|
|
|
|
if(i != len - 1) {
|
|
|
|
numString = fileName.mid(i + 1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
numString = fileName.mid(0, i);
|
|
|
|
}
|
|
|
|
bool ok;
|
|
|
|
int number = numString.toInt(&ok);
|
|
|
|
if(ok) {
|
|
|
|
trackNum = number;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(trackNum > 0) {
|
|
|
|
TQString t = TQString(TStringToQString(tag->title())).stripWhiteSpace();
|
|
|
|
t += "::" + a;
|
|
|
|
const int len = f.audioProperties()->length();
|
|
|
|
if(len > 0) {
|
|
|
|
t += "::" + Tellico::minutes(len);
|
|
|
|
}
|
|
|
|
TQString realTrack = disc > 1 ? track + TQString::number(disc) : track;
|
|
|
|
entry->setField(realTrack, insertValue(entry->field(realTrack), t, trackNum));
|
|
|
|
if(addFile) {
|
|
|
|
TQString fileValue = *it;
|
|
|
|
if(addBitrate) {
|
|
|
|
fileValue += "::" + TQString::number(f.audioProperties()->bitrate());
|
|
|
|
}
|
|
|
|
entry->setField(file, insertValue(entry->field(file), fileValue, trackNum));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
myDebug() << *it << " contains no track number and track number cannot be determined, so the track is not imported." << endl;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
myDebug() << *it << " has an empty title, so the track is not imported." << endl;
|
|
|
|
}
|
|
|
|
if(!tag->comment().stripWhiteSpace().isEmpty()) {
|
|
|
|
TQString c = entry->field(comments);
|
|
|
|
if(!c.isEmpty()) {
|
|
|
|
c += TQString::fromLatin1("<br/>");
|
|
|
|
}
|
|
|
|
if(!tag->title().isEmpty()) {
|
|
|
|
c += TQString::fromLatin1("<em>") + TQString(TStringToQString(tag->title())).stripWhiteSpace() + TQString::fromLatin1("</em> - ");
|
|
|
|
}
|
|
|
|
c += TQString(TStringToQString(tag->comment().stripWhiteSpace())).stripWhiteSpace();
|
|
|
|
entry->setField(comments, c);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!exists) {
|
|
|
|
m_coll->addEntries(entry);
|
|
|
|
}
|
|
|
|
if(showProgress && j%stepSize == 0) {
|
|
|
|
ProgressManager::self()->setTotalSteps(this, files.count() + directoryFiles.count());
|
|
|
|
ProgressManager::self()->setProgress(this, j);
|
|
|
|
kapp->processEvents();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* kdDebug() << "-- TAG --" << endl;
|
|
|
|
kdDebug() << "title - \"" << tag->title().to8Bit() << "\"" << endl;
|
|
|
|
kdDebug() << "artist - \"" << tag->artist().to8Bit() << "\"" << endl;
|
|
|
|
kdDebug() << "album - \"" << tag->album().to8Bit() << "\"" << endl;
|
|
|
|
kdDebug() << "year - \"" << tag->year() << "\"" << endl;
|
|
|
|
kdDebug() << "comment - \"" << tag->comment().to8Bit() << "\"" << endl;
|
|
|
|
kdDebug() << "track - \"" << tag->track() << "\"" << endl;
|
|
|
|
kdDebug() << "genre - \"" << tag->genre().to8Bit() << "\"" << endl;*/
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m_cancelled) {
|
|
|
|
m_coll = 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQTextStream ts;
|
|
|
|
TQRegExp iconRx(TQString::fromLatin1("Icon\\s*=\\s*(.*)"));
|
|
|
|
for(TQStringList::ConstIterator it = directoryFiles.begin(); !m_cancelled && it != directoryFiles.end(); ++it, ++j) {
|
|
|
|
TQFile file(*it);
|
|
|
|
if(!file.open(IO_ReadOnly)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
ts.unsetDevice();
|
|
|
|
ts.setDevice(TQT_TQIODEVICE(&file));
|
|
|
|
for(TQString line = ts.readLine(); !line.isNull(); line = ts.readLine()) {
|
|
|
|
if(!iconRx.exactMatch(line)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
TQDir thisDir(*it);
|
|
|
|
thisDir.cdUp();
|
|
|
|
TQFileInfo fi(thisDir, iconRx.cap(1));
|
|
|
|
Data::EntryPtr entry = albumMap[thisDir.dirName()];
|
|
|
|
if(!entry) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
KURL u;
|
|
|
|
u.setPath(fi.absFilePath());
|
|
|
|
TQString id = ImageFactory::addImage(u, true);
|
|
|
|
if(!id.isEmpty()) {
|
|
|
|
entry->setField(TQString::fromLatin1("cover"), id);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(showProgress && j%stepSize == 0) {
|
|
|
|
ProgressManager::self()->setProgress(this, j);
|
|
|
|
kapp->processEvents();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(m_cancelled) {
|
|
|
|
m_coll = 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return m_coll;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
TQWidget* AudioFileImporter::widget(TQWidget* parent_, const char* name_) {
|
|
|
|
if(m_widget) {
|
|
|
|
return m_widget;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_widget = new TQWidget(parent_, name_);
|
|
|
|
TQVBoxLayout* l = new TQVBoxLayout(m_widget);
|
|
|
|
|
|
|
|
TQVGroupBox* box = new TQVGroupBox(i18n("Audio File Options"), m_widget);
|
|
|
|
|
|
|
|
m_recursive = new TQCheckBox(i18n("Recursive &folder search"), box);
|
|
|
|
TQWhatsThis::add(m_recursive, i18n("If checked, folders are recursively searched for audio files."));
|
|
|
|
// by default, make it checked
|
|
|
|
m_recursive->setChecked(true);
|
|
|
|
|
|
|
|
m_addFilePath = new TQCheckBox(i18n("Include file &location"), box);
|
|
|
|
TQWhatsThis::add(m_addFilePath, i18n("If checked, the file names for each track are added to the entries."));
|
|
|
|
m_addFilePath->setChecked(false);
|
|
|
|
connect(m_addFilePath, TQT_SIGNAL(toggled(bool)), TQT_SLOT(slotAddFileToggled(bool)));
|
|
|
|
|
|
|
|
m_addBitrate = new TQCheckBox(i18n("Include &bitrate"), box);
|
|
|
|
TQWhatsThis::add(m_addBitrate, i18n("If checked, the bitrate for each track is added to the entries."));
|
|
|
|
m_addBitrate->setChecked(false);
|
|
|
|
m_addBitrate->setEnabled(false);
|
|
|
|
|
|
|
|
l->addWidget(box);
|
|
|
|
l->addStretch(1);
|
|
|
|
return m_widget;
|
|
|
|
}
|
|
|
|
|
|
|
|
// pos_ is NOT zero-indexed!
|
|
|
|
TQString AudioFileImporter::insertValue(const TQString& str_, const TQString& value_, uint pos_) {
|
|
|
|
TQStringList list = Data::Field::split(str_, true);
|
|
|
|
for(uint i = list.count(); i < pos_; ++i) {
|
|
|
|
list += TQString();
|
|
|
|
}
|
|
|
|
if(!list[pos_-1].isNull()) {
|
|
|
|
myDebug() << "AudioFileImporter::insertValue() - overwriting track " << pos_ << endl;
|
|
|
|
myDebug() << "*** Old value: " << list[pos_-1] << endl;
|
|
|
|
myDebug() << "*** New value: " << value_ << endl;
|
|
|
|
}
|
|
|
|
list[pos_-1] = value_;
|
|
|
|
return list.join(TQString::fromLatin1("; "));
|
|
|
|
}
|
|
|
|
|
|
|
|
void AudioFileImporter::slotCancel() {
|
|
|
|
m_cancelled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AudioFileImporter::slotAddFileToggled(bool on_) {
|
|
|
|
m_addBitrate->setEnabled(on_);
|
|
|
|
if(!on_) {
|
|
|
|
m_addBitrate->setChecked(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int AudioFileImporter::discNumber(const TagLib::FileRef& ref_) const {
|
|
|
|
// default to 1 unless otherwise
|
|
|
|
int num = 1;
|
|
|
|
#ifdef HAVE_TAGLIB
|
|
|
|
TQString disc;
|
|
|
|
if(TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref_.file())) {
|
|
|
|
if(file->ID3v2Tag() && !file->ID3v2Tag()->frameListMap()["TPOS"].isEmpty()) {
|
|
|
|
disc = TQString(TStringToQString(file->ID3v2Tag()->frameListMap()["TPOS"].front()->toString())).stripWhiteSpace();
|
|
|
|
}
|
|
|
|
} else if(TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(ref_.file())) {
|
|
|
|
if(file->tag() && !file->tag()->fieldListMap()["DISCNUMBER"].isEmpty()) {
|
|
|
|
disc = TQString(TStringToQString(file->tag()->fieldListMap()["DISCNUMBER"].front())).stripWhiteSpace();
|
|
|
|
}
|
|
|
|
} else if(TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(ref_.file())) {
|
|
|
|
if(file->xiphComment() && !file->xiphComment()->fieldListMap()["DISCNUMBER"].isEmpty()) {
|
|
|
|
disc = TQString(TStringToQString(file->xiphComment()->fieldListMap()["DISCNUMBER"].front())).stripWhiteSpace();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!disc.isEmpty()) {
|
|
|
|
int pos = disc.find('/');
|
|
|
|
int n;
|
|
|
|
bool ok;
|
|
|
|
if(pos == -1) {
|
|
|
|
n = disc.toInt(&ok);
|
|
|
|
} else {
|
|
|
|
n = disc.left(pos).toInt(&ok);
|
|
|
|
}
|
|
|
|
if(ok && n > 0) {
|
|
|
|
num = n;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
return num;
|
|
|
|
}
|
|
|
|
|
|
|
|
#include "audiofileimporter.moc"
|