You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tellico/src/translators/audiofileimporter.cpp

428 lines
14 KiB

/***************************************************************************
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 <tdelocale.h>
#include <tdeapplication.h>
#include <tqlabel.h>
#include <tqlayout.h>
#include <tqvgroupbox.h>
#include <tqcheckbox.h>
#include <tqdir.h>
#include <tqwhatsthis.h>
#define TStringToTQString(s) TQString::fromUtf8((s).toCString(true))
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, TQ_SIGNAL(signalCancelled(ProgressItem*)), TQ_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 = TStringToTQString(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 = TStringToTQString(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, TStringToTQString(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 = TStringToTQString(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>") + TStringToTQString(tag->title()).stripWhiteSpace() + TQString::fromLatin1("</em> - ");
}
c += TStringToTQString(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(&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, TQ_SIGNAL(toggled(bool)), TQ_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 = TStringToTQString(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 = TStringToTQString(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 = TStringToTQString(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"