Start work on moving everything that uses taglib out into an external process.

This commit is contained in:
David Sansome 2012-01-02 17:21:07 +00:00
parent 527135abb1
commit 85f2f087cb
18 changed files with 1419 additions and 5271 deletions

View File

@ -47,7 +47,7 @@ find_package(OpenGL REQUIRED)
find_package(Boost REQUIRED)
find_package(Gettext REQUIRED)
find_package(PkgConfig REQUIRED)
find_package(Protobuf)
find_package(Protobuf REQUIRED)
pkg_check_modules(TAGLIB REQUIRED taglib>=1.6)
pkg_check_modules(QJSON REQUIRED QJson)
@ -366,6 +366,8 @@ add_subdirectory(3rdparty/universalchardet)
add_subdirectory(tests)
add_subdirectory(dist)
add_subdirectory(tools/ultimate_lyrics_parser)
add_subdirectory(tagreader/common)
add_subdirectory(tagreader/tagreader)
option(WITH_DEBIAN OFF)
if(WITH_DEBIAN)

View File

@ -64,14 +64,11 @@ set(SOURCES
core/crashreporting.cpp
core/database.cpp
core/deletefiles.cpp
core/encoding.cpp
core/filesystemmusicstorage.cpp
core/fht.cpp
core/fmpsparser.cpp
core/globalshortcutbackend.cpp
core/globalshortcuts.cpp
core/gnomeglobalshortcutbackend.cpp
core/logging.cpp
core/mergedproxymodel.cpp
core/multisortfilterproxy.cpp
core/musicstorage.cpp
@ -956,13 +953,11 @@ add_dependencies(clementine_lib pot)
target_link_libraries(clementine_lib
chardet
echoprint
sha2
${ECHONEST_LIBRARIES}
${GOBJECT_LIBRARIES}
${GLIB_LIBRARIES}
${TAGLIB_LIBRARIES}
${QJSON_LIBRARIES}
${QT_LIBRARIES}
${GSTREAMER_BASE_LIBRARIES}

View File

@ -15,38 +15,15 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "fmpsparser.h"
#include "encoding.h"
#include "logging.h"
#include "mpris_common.h"
#include "song.h"
#include "timeconstants.h"
#include "tagreader/common/messagehandler.h"
#include <algorithm>
#include <sys/stat.h>
#include <aifffile.h>
#include <asffile.h>
#include <attachedpictureframe.h>
#include <commentsframe.h>
#include <fileref.h>
#include <flacfile.h>
#include <id3v1genres.h>
#include <id3v2tag.h>
#include <mp4file.h>
#include <mp4tag.h>
#include <mpcfile.h>
#include <mpegfile.h>
#include <oggfile.h>
#include <oggflacfile.h>
#include <speexfile.h>
#include <tag.h>
#include <textidentificationframe.h>
#include <trueaudiofile.h>
#include <tstring.h>
#include <vorbisfile.h>
#include <wavfile.h>
#ifdef HAVE_LIBLASTFM
#include "internet/fixlastfm.h"
#include <lastfm/Track>
@ -62,6 +39,8 @@
#include <QVariant>
#include <QtConcurrentRun>
#include <id3v1genres.h>
#ifdef Q_OS_WIN32
# include <mswmdm.h>
# include <QUuid>
@ -79,7 +58,6 @@
#include <boost/scoped_ptr.hpp>
using boost::scoped_ptr;
#include "encoding.h"
#include "utilities.h"
#include "covers/albumcoverloader.h"
#include "engines/enginebase.h"
@ -87,12 +65,6 @@ using boost::scoped_ptr;
#include "widgets/trackslider.h"
// Taglib added support for FLAC pictures in 1.7.0
#if (TAGLIB_MAJOR_VERSION > 1) || (TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION >= 7)
# define TAGLIB_HAS_FLAC_PICTURELIST
#endif
namespace {
QStringList Prepend(const QString& text, const QStringList& list) {
@ -109,10 +81,6 @@ QStringList Updateify(const QStringList& list) {
return ret;
}
TagLib::String QStringToTaglibString(const QString& s) {
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
}
} // namespace
@ -142,18 +110,10 @@ const QString Song::kFtsUpdateSpec = Updateify(Song::kFtsColumns).join(", ");
const QString Song::kManuallyUnsetCover = "(unset)";
const QString Song::kEmbeddedCover = "(embedded)";
TagLibFileRefFactory Song::kDefaultFactory;
QMutex Song::sTaglibMutex;
struct Song::Private : public QSharedData {
Private();
// This is here and not in Song itself so we don't have to include
// <xiphcomment.h> in the main header.
void ParseOggTag(const TagLib::Ogg::FieldListMap& map, const QTextCodec* codec,
QString* disc, QString* compilation);
bool valid_;
int id_;
@ -254,37 +214,22 @@ Song::Private::Private()
{
}
TagLib::FileRef* TagLibFileRefFactory::GetFileRef(const QString& filename) {
#ifdef Q_OS_WIN32
return new TagLib::FileRef(filename.toStdWString().c_str());
#else
return new TagLib::FileRef(QFile::encodeName(filename).constData());
#endif
}
Song::Song()
: d(new Private),
factory_(&kDefaultFactory)
: d(new Private)
{
}
Song::Song(const Song &other)
: d(other.d),
factory_(&kDefaultFactory)
: d(other.d)
{
}
Song::Song(FileRefFactory* factory)
: d(new Private),
factory_(factory) {
}
Song::~Song() {
}
Song& Song::operator =(const Song& other) {
d = other.d;
factory_ = other.factory_;
return *this;
}
@ -430,15 +375,6 @@ void Song::set_genre_id3(int id) {
set_genre(TStringToQString(TagLib::ID3v1::genre(id)));
}
QString Song::Decode(const TagLib::String& tag, const QTextCodec* codec) {
if (codec && tag.isLatin1()) { // Never override UTF-8.
const std::string fixed = QString::fromUtf8(tag.toCString(true)).toStdString();
return codec->toUnicode(fixed.c_str()).trimmed();
} else {
return TStringToQString(tag).trimmed();
}
}
QString Song::Decode(const QString& tag, const QTextCodec* codec) {
if (!codec) {
return tag;
@ -447,275 +383,71 @@ QString Song::Decode(const QString& tag, const QTextCodec* codec) {
return codec->toUnicode(tag.toUtf8());
}
bool Song::HasProperMediaFile() const {
#ifndef QT_NO_DEBUG_OUTPUT
if (qApp->thread() == QThread::currentThread())
qLog(Warning) << "HasProperMediaFile() on GUI thread!";
#endif
QMutexLocker l(&sTaglibMutex);
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(d->url_.toLocalFile()));
return !fileref->isNull() && fileref->tag();
}
void Song::InitFromFile(const QString& filename, int directory_id) {
#ifndef QT_NO_DEBUG_OUTPUT
if (qApp->thread() == QThread::currentThread())
qLog(Warning) << "InitFromFile() on GUI thread!";
#endif
void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
d->init_from_file_ = true;
d->url_ = QUrl::fromLocalFile(filename);
d->directory_id_ = directory_id;
QFileInfo info(filename);
d->basefilename_ = info.fileName();
QMutexLocker l(&sTaglibMutex);
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if(fileref->isNull()) {
return;
}
d->filesize_ = info.size();
d->mtime_ = info.lastModified().toTime_t();
d->ctime_ = info.created().toTime_t();
// This is single byte encoding, therefore can't be CJK.
UniversalEncodingHandler detector(NS_FILTER_NON_CJK);
TagLib::Tag* tag = fileref->tag();
QTextCodec* codec = NULL;
if (tag) {
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file());
if (file && (file->ID3v2Tag() || file->ID3v1Tag())) {
codec = detector.Guess(*fileref);
}
if (codec &&
codec->name() != "UTF-8" &&
codec->name() != "ISO-8859-1") {
// Mark tags where we detect an unusual codec as suspicious.
d->suspicious_tags_ = true;
}
d->title_ = Decode(tag->title());
d->artist_ = Decode(tag->artist());
d->album_ = Decode(tag->album());
d->genre_ = Decode(tag->genre());
d->year_ = tag->year();
d->track_ = tag->track();
d->valid_ = true;
}
QString disc;
QString compilation;
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
if (file->ID3v2Tag()) {
const TagLib::ID3v2::FrameListMap& map = file->ID3v2Tag()->frameListMap();
if (!map["TPOS"].isEmpty())
disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
if (!map["TBPM"].isEmpty())
d->bpm_ = TStringToQString(map["TBPM"].front()->toString()).trimmed().toFloat();
if (!map["TCOM"].isEmpty())
d->composer_ = Decode(map["TCOM"].front()->toString());
if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
d->albumartist_ = Decode(map["TPE2"].front()->toString());
if (!map["TCMP"].isEmpty())
compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (!map["APIC"].isEmpty())
set_embedded_cover();
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (int i=0 ; i<map["COMM"].size() ; ++i) {
const TagLib::ID3v2::CommentsFrame* frame =
dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
if (frame && TStringToQString(frame->description()) != "iTunNORM") {
d->comment_ = Decode(frame->text());
break;
}
}
// Parse FMPS frames
for (int i=0 ; i<map["TXXX"].size() ; ++i) {
const TagLib::ID3v2::UserTextIdentificationFrame* frame =
dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i]);
if (frame && frame->description().startsWith("FMPS_")) {
ParseFMPSFrame(TStringToQString(frame->description()),
TStringToQString(frame->fieldList()[1]));
}
}
}
} else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
if (file->tag()) {
d->ParseOggTag(file->tag()->fieldListMap(), NULL, &disc, &compilation);
}
d->comment_ = Decode(tag->comment());
} else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
if ( file->xiphComment() ) {
d->ParseOggTag(file->xiphComment()->fieldListMap(), NULL, &disc, &compilation);
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
if (!file->pictureList().isEmpty()) {
set_embedded_cover();
}
#endif
}
d->comment_ = Decode(tag->comment());
} else if (TagLib::MP4::File* file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
if (file->tag()) {
TagLib::MP4::Tag* mp4_tag = file->tag();
const TagLib::MP4::ItemListMap& items = mp4_tag->itemListMap();
TagLib::MP4::ItemListMap::ConstIterator it = items.find("aART");
if (it != items.end()) {
TagLib::StringList album_artists = it->second.toStringList();
if (!album_artists.isEmpty()) {
d->albumartist_ = Decode(album_artists.front());
}
}
}
} else if (tag) {
d->comment_ = Decode(tag->comment());
}
if ( !disc.isEmpty() ) {
int i = disc.indexOf('/');
if ( i != -1 )
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
d->disc_ = disc.left( i ).toInt();
else
d->disc_ = disc.toInt();
}
if ( compilation.isEmpty() ) {
// well, it wasn't set, but if the artist is VA assume it's a compilation
if ( d->artist_.toLower() == "various artists" )
d->compilation_ = true;
} else {
int i = compilation.toInt();
d->compilation_ = (i == 1);
}
if (fileref->audioProperties()) {
d->bitrate_ = fileref->audioProperties()->bitrate();
d->samplerate_ = fileref->audioProperties()->sampleRate();
set_length_nanosec(fileref->audioProperties()->length() * kNsecPerSec);
}
// Get the filetype if we can
GuessFileType(fileref.get());
// Set integer fields to -1 if they're not valid
#define intval(x) (x <= 0 ? -1 : x)
d->track_ = intval(d->track_);
d->disc_ = intval(d->disc_);
d->bpm_ = intval(d->bpm_);
d->year_ = intval(d->year_);
d->bitrate_ = intval(d->bitrate_);
d->samplerate_ = intval(d->samplerate_);
d->lastplayed_ = intval(d->lastplayed_);
d->rating_ = intval(d->rating_);
#undef intval
d->valid_ = pb.valid();
d->title_ = QStringFromStdString(pb.title());
d->album_ = QStringFromStdString(pb.album());
d->artist_ = QStringFromStdString(pb.artist());
d->albumartist_ = QStringFromStdString(pb.albumartist());
d->composer_ = QStringFromStdString(pb.composer());
d->track_ = pb.track();
d->disc_ = pb.disc();
d->bpm_ = pb.bpm();
d->year_ = pb.year();
d->genre_ = QStringFromStdString(pb.genre());
d->comment_ = QStringFromStdString(pb.comment());
d->compilation_ = pb.compilation();
d->rating_ = pb.rating();
d->playcount_ = pb.playcount();
d->skipcount_ = pb.skipcount();
d->lastplayed_ = pb.lastplayed();
d->score_ = pb.score();
set_length_nanosec(pb.length_nanosec());
d->bitrate_ = pb.bitrate();
d->samplerate_ = pb.samplerate();
d->url_ = QUrl::fromEncoded(QByteArray(pb.url().data(), pb.url().size()));
d->basefilename_ = QStringFromStdString(pb.basefilename());
d->mtime_ = pb.mtime();
d->ctime_ = pb.ctime();
d->filesize_ = pb.filesize();
d->suspicious_tags_ = pb.suspicious_tags();
d->art_automatic_ = QStringFromStdString(pb.art_automatic());
d->filetype_ = static_cast<FileType>(pb.type());
}
void Song::ParseFMPSFrame(const QString& name, const QString& value) {
FMPSParser parser;
if (!parser.Parse(value) || parser.is_empty())
return;
void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
const QByteArray url(d->url_.toEncoded());
QVariant var;
if (name == "FMPS_Rating") {
var = parser.result()[0][0];
if (var.type() == QVariant::Double) {
d->rating_ = var.toDouble();
}
} else if (name == "FMPS_Rating_User") {
// Take a user rating only if there's no rating already set
if (d->rating_ == -1 && parser.result()[0].count() >= 2) {
var = parser.result()[0][1];
if (var.type() == QVariant::Double) {
d->rating_ = var.toDouble();
}
}
} else if (name == "FMPS_PlayCount") {
var = parser.result()[0][0];
if (var.type() == QVariant::Double) {
d->playcount_ = var.toDouble();
}
} else if (name == "FMPS_PlayCount_User") {
// Take a user rating only if there's no playcount already set
if (d->rating_ == -1 && parser.result()[0].count() >= 2) {
var = parser.result()[0][1];
if (var.type() == QVariant::Double) {
d->playcount_ = var.toDouble();
}
}
}
}
void Song::Private::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
const QTextCodec* codec,
QString* disc, QString* compilation) {
if (!map["COMPOSER"].isEmpty())
composer_ = Decode(map["COMPOSER"].front(), codec);
if (!map["ALBUMARTIST"].isEmpty()) {
albumartist_ = Decode(map["ALBUMARTIST"].front(), codec);
} else if (!map["ALBUM ARTIST"].isEmpty()) {
albumartist_ = Decode(map["ALBUM ARTIST"].front(), codec);
}
if (!map["BPM"].isEmpty() )
bpm_ = TStringToQString( map["BPM"].front() ).trimmed().toFloat();
if (!map["DISCNUMBER"].isEmpty() )
*disc = TStringToQString( map["DISCNUMBER"].front() ).trimmed();
if (!map["COMPILATION"].isEmpty() )
*compilation = TStringToQString( map["COMPILATION"].front() ).trimmed();
if (!map["COVERART"].isEmpty())
art_automatic_ = kEmbeddedCover;
}
void Song::GuessFileType(TagLib::FileRef* fileref) {
#ifdef TAGLIB_WITH_ASF
if (dynamic_cast<TagLib::ASF::File*>(fileref->file()))
d->filetype_ = Type_Asf;
#endif
if (dynamic_cast<TagLib::FLAC::File*>(fileref->file()))
d->filetype_ = Type_Flac;
#ifdef TAGLIB_WITH_MP4
if (dynamic_cast<TagLib::MP4::File*>(fileref->file()))
d->filetype_ = Type_Mp4;
#endif
if (dynamic_cast<TagLib::MPC::File*>(fileref->file()))
d->filetype_ = Type_Mpc;
if (dynamic_cast<TagLib::MPEG::File*>(fileref->file()))
d->filetype_ = Type_Mpeg;
if (dynamic_cast<TagLib::Ogg::FLAC::File*>(fileref->file()))
d->filetype_ = Type_OggFlac;
if (dynamic_cast<TagLib::Ogg::Speex::File*>(fileref->file()))
d->filetype_ = Type_OggSpeex;
if (dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file()))
d->filetype_ = Type_OggVorbis;
if (dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file()))
d->filetype_ = Type_Aiff;
if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file()))
d->filetype_ = Type_Wav;
if (dynamic_cast<TagLib::TrueAudio::File*>(fileref->file()))
d->filetype_ = Type_TrueAudio;
pb->set_valid(d->valid_);
pb->set_title(DataCommaSizeFromQString(d->title_));
pb->set_album(DataCommaSizeFromQString(d->album_));
pb->set_artist(DataCommaSizeFromQString(d->artist_));
pb->set_albumartist(DataCommaSizeFromQString(d->albumartist_));
pb->set_composer(DataCommaSizeFromQString(d->composer_));
pb->set_track(d->track_);
pb->set_disc(d->disc_);
pb->set_bpm(d->bpm_);
pb->set_year(d->year_);
pb->set_genre(DataCommaSizeFromQString(d->genre_));
pb->set_comment(DataCommaSizeFromQString(d->comment_));
pb->set_compilation(d->compilation_);
pb->set_rating(d->rating_);
pb->set_playcount(d->playcount_);
pb->set_skipcount(d->skipcount_);
pb->set_lastplayed(d->lastplayed_);
pb->set_score(d->score_);
pb->set_length_nanosec(length_nanosec());
pb->set_bitrate(d->bitrate_);
pb->set_samplerate(d->samplerate_);
pb->set_url(url.constData(), url.size());
pb->set_basefilename(DataCommaSizeFromQString(d->basefilename_));
pb->set_mtime(d->mtime_);
pb->set_ctime(d->ctime_);
pb->set_filesize(d->filesize_);
pb->set_suspicious_tags(d->suspicious_tags_);
pb->set_art_automatic(DataCommaSizeFromQString(d->art_automatic_));
pb->set_type(static_cast< ::pb::tagreader::SongMetadata_Type>(d->filetype_));
}
void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
@ -1371,92 +1103,11 @@ bool Song::IsMetadataEqual(const Song& other) const {
d->cue_path_ == other.d->cue_path_;
}
void Song::SetTextFrame(const QString& id, const QString& value,
TagLib::ID3v2::Tag* tag) {
TagLib::ByteVector id_vector = id.toUtf8().constData();
// Remove the frame if it already exists
while (tag->frameListMap().contains(id_vector) &&
tag->frameListMap()[id_vector].size() != 0) {
tag->removeFrame(tag->frameListMap()[id_vector].front());
}
// Create and add a new frame
TagLib::ID3v2::TextIdentificationFrame* frame =
new TagLib::ID3v2::TextIdentificationFrame(id.toUtf8().constData(),
TagLib::String::UTF8);
frame->setText(QStringToTaglibString(value));
tag->addFrame(frame);
}
bool Song::IsEditable() const {
return d->valid_ && !d->url_.isEmpty() && !is_stream() &&
d->filetype_ != Type_Unknown && !has_cue();
}
bool Song::Save() const {
const QString filename = d->url_.toLocalFile();
if (filename.isNull())
return false;
QMutexLocker l(&sTaglibMutex);
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) // The file probably doesn't exist
return false;
fileref->tag()->setTitle(QStringToTaglibString(d->title_));
fileref->tag()->setArtist(QStringToTaglibString(d->artist_));
fileref->tag()->setAlbum(QStringToTaglibString(d->album_));
fileref->tag()->setGenre(QStringToTaglibString(d->genre_));
fileref->tag()->setComment(QStringToTaglibString(d->comment_));
fileref->tag()->setYear(d->year_);
fileref->tag()->setTrack(d->track_);
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
SetTextFrame("TPOS", d->disc_ <= 0 -1 ? QString() : QString::number(d->disc_), tag);
SetTextFrame("TBPM", d->bpm_ <= 0 -1 ? QString() : QString::number(d->bpm_), tag);
SetTextFrame("TCOM", d->composer_, tag);
SetTextFrame("TPE2", d->albumartist_, tag);
SetTextFrame("TCMP", d->compilation_ ? "1" : "0", tag);
}
else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
TagLib::Ogg::XiphComment* tag = file->tag();
tag->addField("COMPOSER", QStringToTaglibString(d->composer_), true);
tag->addField("BPM", QStringToTaglibString(d->bpm_ <= 0 -1 ? QString() : QString::number(d->bpm_)), true);
tag->addField("DISCNUMBER", QStringToTaglibString(d->disc_ <= 0 -1 ? QString() : QString::number(d->disc_)), true);
tag->addField("COMPILATION", QStringToTaglibString(d->compilation_ ? "1" : "0"), true);
}
else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment* tag = file->xiphComment();
tag->addField("COMPOSER", QStringToTaglibString(d->composer_), true);
tag->addField("BPM", QStringToTaglibString(d->bpm_ <= 0 -1 ? QString() : QString::number(d->bpm_)), true);
tag->addField("DISCNUMBER", QStringToTaglibString(d->disc_ <= 0 -1 ? QString() : QString::number(d->disc_)), true);
tag->addField("COMPILATION", QStringToTaglibString(d->compilation_ ? "1" : "0"), true);
}
bool ret = fileref->save();
#ifdef Q_OS_LINUX
if (ret) {
// Linux: inotify doesn't seem to notice the change to the file unless we
// change the timestamps as well. (this is what touch does)
utimensat(0, QFile::encodeName(filename).constData(), NULL, 0);
}
#endif // Q_OS_LINUX
return ret;
}
bool Song::Save(const Song& song) {
return song.Save();
}
QFuture<bool> Song::BackgroundSave() const {
QFuture<bool> future = QtConcurrent::run(&Song::Save, Song(*this));
return future;
}
bool Song::operator==(const Song& other) const {
// TODO: this isn't working for radios
return url() == other.url() &&
@ -1468,79 +1119,6 @@ uint qHash(const Song& song) {
return qHash(song.url().toString()) ^ qHash(song.beginning_nanosec());
}
QImage Song::LoadEmbeddedArt(const QString& filename) {
QImage ret;
if (filename.isEmpty())
return ret;
QMutexLocker l(&sTaglibMutex);
#ifdef Q_OS_WIN32
TagLib::FileRef ref(filename.toStdWString().c_str());
#else
TagLib::FileRef ref(QFile::encodeName(filename).constData());
#endif
if (ref.isNull() || !ref.file())
return ret;
// MP3
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref.file());
if (file && file->ID3v2Tag()) {
TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"];
if (apic_frames.isEmpty())
return ret;
TagLib::ID3v2::AttachedPictureFrame* pic =
static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
ret.loadFromData((const uchar*) pic->picture().data(), pic->picture().size());
return ret;
}
// Ogg vorbis/speex
TagLib::Ogg::XiphComment* xiph_comment =
dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag());
if (xiph_comment) {
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
// Ogg lacks a definitive standard for embedding cover art, but it seems
// b64 encoding a field called COVERART is the general convention
if (!map.contains("COVERART"))
return ret;
QByteArray image_data_b64(map["COVERART"].toString().toCString());
QByteArray image_data = QByteArray::fromBase64(image_data_b64);
if (!ret.loadFromData(image_data))
ret.loadFromData(image_data_b64); //maybe it's not b64 after all
return ret;
}
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
// Flac
TagLib::FLAC::File* flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file());
if (flac_file && flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
if (!pics.isEmpty()) {
// Use the first picture in the file - this could be made cleverer and
// pick the front cover if it's present.
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
TagLib::FLAC::Picture* picture = *it;
QByteArray image_data(picture->data().data(), picture->data().size());
ret.loadFromData(image_data);
return ret;
}
}
#endif
return ret;
}
bool Song::IsOnSameAlbum(const Song& other) const {
if (is_compilation() != other.is_compilation())
return false;

View File

@ -26,6 +26,7 @@
#include "config.h"
#include "engines/engine_fwd.h"
#include "tagreader/common/tagreadermessages.pb.h"
class QSqlQuery;
class QUrl;
@ -48,35 +49,13 @@ class QUrl;
}
#endif
namespace TagLib {
class FileRef;
class String;
namespace ID3v2 {
class Tag;
}
}
class SqlRow;
class FileRefFactory {
public:
virtual ~FileRefFactory() {}
virtual TagLib::FileRef* GetFileRef(const QString& filename) = 0;
};
class TagLibFileRefFactory : public FileRefFactory {
public:
virtual TagLib::FileRef* GetFileRef(const QString& filename);
};
class Song {
public:
Song();
Song(const Song& other);
Song(FileRefFactory* factory);
~Song();
static const QStringList kColumns;
@ -94,7 +73,8 @@ class Song {
static QString JoinSpec(const QString& table);
// Don't change these values - they're stored in the database
// Don't change these values - they're stored in the database, and defined
// in the tag reader protobuf.
enum FileType {
Type_Unknown = 0,
Type_Asf = 1,
@ -115,18 +95,10 @@ class Song {
static QString TextForFiletype(FileType type);
QString TextForFiletype() const { return TextForFiletype(filetype()); }
// Helper function to load embedded cover art from a music file. This is not
// actually used by the Song class, but instead it is called by
// AlbumCoverLoader and is here so it can lock on the taglib mutex.
static QImage LoadEmbeddedArt(const QString& filename);
// Checks if this Song can be properly initialized from it's media file.
// This requires the 'filename' attribute to be set first.
bool HasProperMediaFile() const;
// Constructors
void Init(const QString& title, const QString& artist, const QString& album, qint64 length_nanosec);
void Init(const QString& title, const QString& artist, const QString& album, qint64 beginning, qint64 end);
void InitFromFile(const QString& filename, int directory_id);
void InitFromProtobuf(const pb::tagreader::SongMetadata& pb);
void InitFromQuery(const SqlRow& query, bool reliable_metadata, int col = 0);
void InitFromFilePartial(const QString& filename); // Just store the filename: incomplete but fast
#ifdef HAVE_LIBLASTFM
@ -150,7 +122,6 @@ class Song {
void ToWmdm(IWMDMMetaData* metadata) const;
#endif
static QString Decode(const TagLib::String& tag, const QTextCodec* codec = NULL);
static QString Decode(const QString& tag, const QTextCodec* codec = NULL);
// Save
@ -160,6 +131,7 @@ class Song {
void ToLastFM(lastfm::Track* track) const;
#endif
void ToXesam(QVariantMap* map) const;
void ToProtobuf(pb::tagreader::SongMetadata* pb) const;
// Simple accessors
bool is_valid() const;
@ -296,24 +268,9 @@ class Song {
Song& operator=(const Song& other);
private:
void GuessFileType(TagLib::FileRef* fileref);
static bool Save(const Song& song);
// Helper methods for taglib
static void SetTextFrame(const QString& id, const QString& value,
TagLib::ID3v2::Tag* tag);
void ParseFMPSFrame(const QString& name, const QString& value);
private:
struct Private;
QSharedDataPointer<Private> d;
FileRefFactory* factory_;
static TagLibFileRefFactory kDefaultFactory;
static QMutex sTaglibMutex;
};
Q_DECLARE_METATYPE(Song);

View File

@ -0,0 +1,72 @@
/* This file is part of Clementine.
Copyright 2011, 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 "tagreaderclient.h"
#include <QProcess>
TagReaderClient::TagReaderClient(QObject* parent)
: QObject(parent),
process_(NULL),
handler_(NULL)
{
}
void TagReaderClient::Start() {
delete process_;
delete handler_;
process_ = new QProcess(this);
process_->start();
}
TagReaderReply* TagReaderClient::ReadFile(const QString& filename) {
pb::tagreader::Message message;
pb::tagreader::ReadFileRequest* req = message.mutable_read_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
return SendMessageWithReply(&message);
}
TagReaderReply* TagReaderClient::SaveFile(const QString& filename, const Song& metadata) {
pb::tagreader::Message message;
pb::tagreader::SaveFileRequest* req = message.mutable_save_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
metadata.ToProtobuf(req->mutable_metadata());
return SendMessageWithReply(&message);
}
TagReaderReply* TagReaderClient::IsMediaFile(const QString& filename) {
pb::tagreader::Message message;
pb::tagreader::IsMediaFileRequest* req = message.mutable_is_media_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
return SendMessageWithReply(&message);
}
TagReaderReply* TagReaderClient::LoadEmbeddedArt(const QString& filename) {
pb::tagreader::Message message;
pb::tagreader::LoadEmbeddedArtRequest* req = message.mutable_load_embedded_art_request();
req->set_filename(DataCommaSizeFromQString(filename));
return SendMessageWithReply(&message);
}

View File

@ -0,0 +1,50 @@
/* This file is part of Clementine.
Copyright 2011, 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/>.
*/
#ifndef TAGREADERCLIENT_H
#define TAGREADERCLIENT_H
#include "messagehandler.h"
#include "song.h"
#include "tagreadermessages.pb.h"
class QProcess;
class TagReaderClient : public QObject {
Q_OBJECT
public:
TagReaderClient(QObject* parent = 0);
typedef AbstractMessageHandler<pb::tagreader::Message> HandlerType;
typedef typename HandlerType::ReplyType ReplyType;
void Start();
ReplyType* ReadFile(const QString& filename);
ReplyType* SaveFile(const QString& filename, const Song& metadata);
ReplyType* IsMediaFile(const QString& filename);
ReplyType* LoadEmbeddedArt(const QString& filename);
private:
QProcess* process_;
HandlerType* handler_;
};
typedef TagReaderClient::ReplyType TagReaderReply;
#endif // TAGREADERCLIENT_H

View File

@ -275,10 +275,6 @@ int main(int argc, char *argv[]) {
}
}
// Detect technically invalid usage of non-ASCII in ID3v1 tags.
UniversalEncodingHandler handler;
TagLib::ID3v1::Tag::setStringHandler(&handler);
#ifdef Q_OS_LINUX
// Force Clementine's menu to be shown in the Clementine window and not in
// the Unity global menubar thing. See:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
include_directories(${PROTOBUF_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_SOURCE_DIR}/src)
set(COMMON_SOURCES
messagehandler.cpp
${CMAKE_SOURCE_DIR}/src/core/encoding.cpp
${CMAKE_SOURCE_DIR}/src/core/logging.cpp
)
set(COMMON_HEADERS
messagehandler.h
)
set(COMMON_MESSAGES
tagreadermessages.proto
)
qt4_wrap_cpp(COMMON_MOC ${COMMON_HEADERS})
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${COMMON_MESSAGES})
add_library(clementine-tagreader-common STATIC
${COMMON_SOURCES}
${COMMON_MOC}
${PROTO_SOURCES}
)
# Use protobuf-lite if it's available
if(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
set(protobuf ${PROTOBUF_LITE_LIBRARY})
else(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
set(protobuf ${PROTOBUF_LIBRARY})
endif(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
target_link_libraries(clementine-tagreader-common
${protobuf}
${CMAKE_THREAD_LIBS_INIT}
chardet
)

View File

@ -0,0 +1,107 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#include "messagehandler.h"
#include "core/logging.h"
#include <QAbstractSocket>
#include <QLocalSocket>
_MessageHandlerBase::_MessageHandlerBase(QIODevice* device, QObject* parent)
: QObject(parent),
device_(device),
flush_abstract_socket_(NULL),
flush_local_socket_(NULL),
reading_protobuf_(false),
expected_length_(0) {
buffer_.open(QIODevice::ReadWrite);
connect(device, SIGNAL(readyRead()), SLOT(DeviceReadyRead()));
// Yeah I know.
if (QAbstractSocket* socket = qobject_cast<QAbstractSocket*>(device)) {
flush_abstract_socket_ = &QAbstractSocket::flush;
connect(socket, SIGNAL(disconnected()), SLOT(SocketClosed()));
} else if (QLocalSocket* socket = qobject_cast<QLocalSocket*>(device)) {
flush_local_socket_ = &QLocalSocket::flush;
connect(socket, SIGNAL(disconnected()), SLOT(SocketClosed()));
} else {
qFatal("Unsupported device type passed to _MessageHandlerBase");
}
}
void _MessageHandlerBase::DeviceReadyRead() {
while (device_->bytesAvailable()) {
if (!reading_protobuf_) {
// Read the length of the next message
QDataStream s(device_);
s >> expected_length_;
reading_protobuf_ = true;
}
// Read some of the message
buffer_.write(device_->read(expected_length_ - buffer_.size()));
// Did we get everything?
if (buffer_.size() == expected_length_) {
// Parse the message
if (!MessageArrived(buffer_.data())) {
qLog(Error) << "Malformed protobuf message";
device_->close();
return;
}
// Clear the buffer
buffer_.close();
buffer_.setData(QByteArray());
buffer_.open(QIODevice::ReadWrite);
reading_protobuf_ = false;
}
}
}
void _MessageHandlerBase::WriteMessage(const QByteArray& data) {
QDataStream s(device_);
s << quint32(data.length());
s.writeRawData(data.data(), data.length());
// Sorry.
if (flush_abstract_socket_) {
((static_cast<QAbstractSocket*>(device_))->*(flush_abstract_socket_))();
} else if (flush_local_socket_) {
((static_cast<QLocalSocket*>(device_))->*(flush_local_socket_))();
}
}
_MessageReplyBase::_MessageReplyBase(int id, QObject* parent)
: QObject(parent),
id_(id),
finished_(false)
{
}
void _MessageReplyBase::Abort() {
Q_ASSERT(!finished_);
finished_ = true;
emit Finished(false);
}

View File

@ -0,0 +1,258 @@
/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#ifndef MESSAGEHANDLER_H
#define MESSAGEHANDLER_H
#include <QBuffer>
#include <QMap>
#include <QMutex>
#include <QMutexLocker>
#include <QObject>
#include <QThread>
class QAbstractSocket;
class QIODevice;
class QLocalSocket;
#define QStringFromStdString(x) \
QString::fromUtf8(x.data(), x.size())
#define DataCommaSizeFromQString(x) \
x.toUtf8().constData(), x.toUtf8().length()
// Base QObject for a reply future class that is returned immediately for
// requests that will occur in the background. Similar to QNetworkReply.
// Use MessageReply instead.
class _MessageReplyBase : public QObject {
Q_OBJECT
public:
_MessageReplyBase(int id, QObject* parent);
int id() const { return id_; }
bool is_finished() const { return finished_; }
void Abort();
signals:
void Finished(bool success);
protected:
int id_;
bool finished_;
};
// A reply future class that is returned immediately for requests that will
// occur in the background. Similar to QNetworkReply.
template <typename MessageType>
class MessageReply : public _MessageReplyBase {
public:
const MessageType& message() const { return message_; }
void SetReply(const MessageType& message);
private:
MessageType message_;
};
// Reads and writes uint32 length encoded protobufs to a socket.
// This base QObject is separate from AbstractMessageHandler because moc can't
// handle templated classes. Use AbstractMessageHandler instead.
class _MessageHandlerBase : public QObject {
Q_OBJECT
public:
_MessageHandlerBase(QIODevice* device, QObject* parent);
protected slots:
void WriteMessage(const QByteArray& data);
void DeviceReadyRead();
virtual bool SocketClosed() {}
protected:
virtual bool MessageArrived(const QByteArray& data) = 0;
protected:
typedef bool (QAbstractSocket::*FlushAbstractSocket)();
typedef bool (QLocalSocket::*FlushLocalSocket)();
QIODevice* device_;
FlushAbstractSocket flush_abstract_socket_;
FlushLocalSocket flush_local_socket_;
bool reading_protobuf_;
quint32 expected_length_;
QBuffer buffer_;
};
// Reads and writes uint32 length encoded MessageType messages to a socket.
// You should subclass this and implement the MessageArrived(MessageType)
// method.
template <typename MessageType>
class AbstractMessageHandler : public _MessageHandlerBase {
public:
AbstractMessageHandler(QIODevice* device, QObject* parent);
typedef MessageReply<MessageType> ReplyType;
// Serialises the message and writes it to the socket. This version MUST be
// called from the thread in which the AbstractMessageHandler was created.
void SendMessage(const MessageType& message);
// Serialises the message and writes it to the socket. This version may be
// called from any thread.
void SendMessageAsync(const MessageType& message);
// Creates a new reply future for the request with the next sequential ID,
// and sets the request's ID to the ID of the reply. When a reply arrives
// for this request the reply is triggered automatically and MessageArrived
// is NOT called. Can be called from any thread.
ReplyType* NewReply(MessageType* message);
// Same as NewReply, except the message is sent as well. Can be called from
// any thread.
ReplyType* SendMessageWithReply(MessageType* message);
// Sets the "id" field of reply to the same as the request, and sends the
// reply on the socket. Used on the worker side.
void SendReply(const MessageType& request, MessageType* reply);
protected:
// Called when a message is received from the socket.
virtual void MessageArrived(const MessageType& message) {}
// _MessageHandlerBase
bool MessageArrived(const QByteArray& data);
private:
QMutex mutex_;
int next_id_;
QMap<int, ReplyType*> pending_replies_;
};
template<typename MessageType>
AbstractMessageHandler<MessageType>::AbstractMessageHandler(
QIODevice* device, QObject* parent)
: _MessageHandlerBase(device, parent),
next_id_(1)
{
}
template<typename MessageType>
void AbstractMessageHandler<MessageType>::SendMessage(const MessageType& message) {
Q_ASSERT(QThread::currentThread() == thread());
std::string data = message.SerializeAsString();
WriteMessage(QByteArray(data.data(), data.size()));
}
template<typename MessageType>
void AbstractMessageHandler<MessageType>::SendMessageAsync(const MessageType& message) {
std::string data = message.SerializeAsString();
metaObject()->invokeMethod(this, "WriteMessage", Qt::QueuedConnection,
Q_ARG(QByteArray, QByteArray(data.data(), data.size())));
}
template<typename MessageType>
void AbstractMessageHandler<MessageType>::SendReply(const MessageType& request,
MessageType* reply) {
reply->set_id(request.id());
SendMessage(*reply);
}
template<typename MessageType>
bool AbstractMessageHandler<MessageType>::MessageArrived(const QByteArray& data) {
MessageType message;
if (!message.ParseFromArray(data.constData(), data.size())) {
return false;
}
ReplyType* reply = NULL;
{
QMutexLocker l(&mutex_);
reply = pending_replies_.take(message.id());
}
if (reply) {
// This is a reply to a message that we created earlier.
reply->SetReply(message);
} else {
MessageArrived(message);
}
return true;
}
template<typename MessageType>
typename AbstractMessageHandler<MessageType>::ReplyType*
AbstractMessageHandler<MessageType>::NewReply(
MessageType* message) {
ReplyType* reply = NULL;
{
QMutexLocker l(&mutex_);
const int id = next_id_ ++;
reply = new ReplyType(id, this);
pending_replies_[id] = reply;
}
message->set_id(reply->id());
return reply;
}
template<typename MessageType>
typename AbstractMessageHandler<MessageType>::ReplyType*
AbstractMessageHandler<MessageType>::SendMessageWithReply(
MessageType* message) {
ReplyType* reply = NewReply(message);
SendMessageAsync(*message);
return reply;
}
template<typename MessageType>
void AbstractMessageHandler<MessageType>::SocketClosed() {
QMutexLocker l(&mutex_);
foreach (ReplyType* reply, pending_replies_) {
reply->Abort();
}
pending_replies_.clear();
}
template<typename MessageType>
void MessageReply<MessageType>::SetReply(const MessageType& message) {
Q_ASSERT(!finished_);
message_.MergeFrom(message);
finished_ = true;
emit Finished(true);
}
#endif // MESSAGEHANDLER_H

View File

@ -0,0 +1,102 @@
package pb.tagreader;
option optimize_for = LITE_RUNTIME;
message SongMetadata {
enum Type {
UNKNOWN = 0;
ASF = 1;
FLAC = 2;
MP4 = 3;
MPC = 4;
MPEG = 5;
OGGFLAC = 6;
OGGSPEEX = 7;
OGGVORBIS = 8;
AIFF = 9;
WAV = 10;
TRUEAUDIO = 11;
CDDA = 12;
STREAM = 99;
}
optional bool valid = 1;
optional string title = 2;
optional string album = 3;
optional string artist = 4;
optional string albumartist = 5;
optional string composer = 6;
optional int32 track = 7;
optional int32 disc = 8;
optional float bpm = 9;
optional int32 year = 10;
optional string genre = 11;
optional string comment = 12;
optional bool compilation = 13;
optional float rating = 14;
optional int32 playcount = 15;
optional int32 skipcount = 16;
optional int32 lastplayed = 17;
optional int32 score = 18;
optional uint64 length_nanosec = 19;
optional int32 bitrate = 20;
optional int32 samplerate = 21;
optional string url = 22;
optional string basefilename = 23;
optional int32 mtime = 24;
optional int32 ctime = 25;
optional int32 filesize = 26;
optional bool suspicious_tags = 27;
optional string art_automatic = 28;
optional Type type = 29;
}
message ReadFileRequest {
optional string filename = 1;
}
message ReadFileResponse {
optional SongMetadata metadata = 1;
}
message SaveFileRequest {
optional string filename = 1;
optional SongMetadata metadata = 2;
}
message SaveFileResponse {
optional bool success = 1;
}
message IsMediaFileRequest {
optional string filename = 1;
}
message IsMediaFileResponse {
optional bool success = 1;
}
message LoadEmbeddedArtRequest {
optional string filename = 1;
}
message LoadEmbeddedArtResponse {
optional bytes data = 1;
}
message Message {
optional int32 id = 1;
optional ReadFileRequest read_file_request = 2;
optional ReadFileResponse read_file_response = 3;
optional SaveFileRequest save_file_request = 4;
optional SaveFileResponse save_file_response = 5;
optional IsMediaFileRequest is_media_file_request = 6;
optional IsMediaFileResponse is_media_file_response = 7;
optional LoadEmbeddedArtRequest load_embedded_art_request = 8;
optional LoadEmbeddedArtResponse load_embedded_art_response = 9;
}

View File

@ -0,0 +1,45 @@
include_directories(${PROTOBUF_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_BINARY_DIR}/../common)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../common)
include_directories(${CMAKE_SOURCE_DIR}/src)
link_directories(${SPOTIFY_LIBRARY_DIRS})
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(SOURCES
fmpsparser.cpp
main.cpp
tagreaderworker.cpp
)
set(HEADERS
)
qt4_wrap_cpp(MOC ${HEADERS})
add_executable(clementine-tagreader
${SOURCES}
${MOC}
)
target_link_libraries(clementine-tagreader
${TAGLIB_LIBRARIES}
${QT_QTCORE_LIBRARY}
${QT_QTNETWORK_LIBRARY}
clementine-tagreader-common
)
if(APPLE)
target_link_libraries(clementine-tagreader
/System/Library/Frameworks/Foundation.framework
)
endif(APPLE)
if(NOT APPLE)
# macdeploy.py takes care of this on mac
install(TARGETS clementine-tagreader
RUNTIME DESTINATION bin
)
endif(NOT APPLE)

View File

@ -0,0 +1,53 @@
/* This file is part of Clementine.
Copyright 2011, 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 "tagreaderworker.h"
#include "core/encoding.h"
#include <QCoreApplication>
#include <QLocalSocket>
#include <QStringList>
#include <iostream>
int main(int argc, char** argv) {
QCoreApplication a(argc, argv);
QStringList args(a.arguments());
if (args.count() != 2) {
std::cerr << "This program is used internally by Clementine to parse tags in music files\n"
"without exposing the whole application to crashes caused by malformed\n"
"files. It is not meant to be run on its own.\n";
return 1;
}
// Detect technically invalid usage of non-ASCII in ID3v1 tags.
UniversalEncodingHandler handler;
TagLib::ID3v1::Tag::setStringHandler(&handler);
// Connect to the parent process.
QLocalSocket socket;
socket.connectToServer(args[1]);
if (!socket.waitForConnected(2000)) {
std::cerr << "Failed to connect to the parent process.\n";
return 1;
}
TagReaderWorker worker(&socket);
return a.exec();
}

View File

@ -0,0 +1,541 @@
/* This file is part of Clementine.
Copyright 2011, 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 "fmpsparser.h"
#include "tagreaderworker.h"
#include "core/encoding.h"
#include "core/timeconstants.h"
#include <QDateTime>
#include <QFileInfo>
#include <QTextCodec>
#include <QUrl>
#include <aifffile.h>
#include <asffile.h>
#include <attachedpictureframe.h>
#include <commentsframe.h>
#include <fileref.h>
#include <flacfile.h>
#include <id3v2tag.h>
#include <mp4file.h>
#include <mp4tag.h>
#include <mpcfile.h>
#include <mpegfile.h>
#include <oggfile.h>
#include <oggflacfile.h>
#include <speexfile.h>
#include <tag.h>
#include <textidentificationframe.h>
#include <trueaudiofile.h>
#include <tstring.h>
#include <vorbisfile.h>
#include <wavfile.h>
#include <boost/scoped_ptr.hpp>
#include <sys/stat.h>
// Taglib added support for FLAC pictures in 1.7.0
#if (TAGLIB_MAJOR_VERSION > 1) || (TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION >= 7)
# define TAGLIB_HAS_FLAC_PICTURELIST
#endif
using boost::scoped_ptr;
class FileRefFactory {
public:
virtual ~FileRefFactory() {}
virtual TagLib::FileRef* GetFileRef(const QString& filename) = 0;
};
class TagLibFileRefFactory : public FileRefFactory {
public:
virtual TagLib::FileRef* GetFileRef(const QString& filename) {
#ifdef Q_OS_WIN32
return new TagLib::FileRef(filename.toStdWString().c_str());
#else
return new TagLib::FileRef(QFile::encodeName(filename).constData());
#endif
}
};
namespace {
TagLib::String StdStringToTaglibString(const std::string& s) {
return TagLib::String(s.c_str(), TagLib::String::UTF8);
}
TagLib::String QStringToTaglibString(const QString& s) {
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
}
}
TagReaderWorker::TagReaderWorker(QIODevice* socket, QObject* parent)
: AbstractMessageHandler<pb::tagreader::Message>(socket, parent),
kEmbeddedCover("(embedded)")
{
}
void TagReaderWorker::MessageArrived(const pb::tagreader::Message& message) {
pb::tagreader::Message reply;
if (message.has_read_file_request()) {
ReadFile(QStringFromStdString(message.read_file_request().filename()),
reply.mutable_read_file_response()->mutable_metadata());
} else if (message.has_save_file_request()) {
reply.mutable_save_file_response()->set_success(
SaveFile(QStringFromStdString(message.save_file_request().filename()),
message.save_file_request().metadata()));
} else if (message.has_is_media_file_request()) {
reply.mutable_is_media_file_response()->set_success(
IsMediaFile(QStringFromStdString(message.is_media_file_request().filename())));
} else if (message.has_load_embedded_art_request()) {
QByteArray data = LoadEmbeddedArt(
QStringFromStdString(message.load_embedded_art_request().filename()));
reply.mutable_load_embedded_art_response()->set_data(
data.constData(), data.size());
}
SendReply(message, &reply);
}
void TagReaderWorker::ReadFile(const QString& filename,
pb::tagreader::SongMetadata* song) const {
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
const QFileInfo info(filename);
song->set_basefilename(DataCommaSizeFromQString(info.fileName()));
song->set_url(url.constData(), url.size());
song->set_filesize(info.size());
song->set_mtime(info.lastModified().toTime_t());
song->set_ctime(info.created().toTime_t());
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if(fileref->isNull()) {
return;
}
// This is single byte encoding, therefore can't be CJK.
UniversalEncodingHandler detector(NS_FILTER_NON_CJK);
TagLib::Tag* tag = fileref->tag();
QTextCodec* codec = NULL;
if (tag) {
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file());
if (file && (file->ID3v2Tag() || file->ID3v1Tag())) {
codec = detector.Guess(*fileref);
}
if (codec &&
codec->name() != "UTF-8" &&
codec->name() != "ISO-8859-1") {
// Mark tags where we detect an unusual codec as suspicious.
song->set_suspicious_tags(true);
}
Decode(tag->title(), NULL, song->mutable_title());
Decode(tag->artist(), NULL, song->mutable_artist());
Decode(tag->album(), NULL, song->mutable_album());
Decode(tag->genre(), NULL, song->mutable_genre());
song->set_year(tag->year());
song->set_track(tag->track());
song->set_valid(true);
}
QString disc;
QString compilation;
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
if (file->ID3v2Tag()) {
const TagLib::ID3v2::FrameListMap& map = file->ID3v2Tag()->frameListMap();
if (!map["TPOS"].isEmpty())
disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
if (!map["TBPM"].isEmpty())
song->set_bpm(TStringToQString(map["TBPM"].front()->toString()).trimmed().toFloat());
if (!map["TCOM"].isEmpty())
Decode(map["TCOM"].front()->toString(), NULL, song->mutable_composer());
if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
Decode(map["TPE2"].front()->toString(), NULL, song->mutable_albumartist());
if (!map["TCMP"].isEmpty())
compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (!map["APIC"].isEmpty())
song->set_art_automatic(kEmbeddedCover);
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (int i=0 ; i<map["COMM"].size() ; ++i) {
const TagLib::ID3v2::CommentsFrame* frame =
dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
if (frame && TStringToQString(frame->description()) != "iTunNORM") {
Decode(frame->text(), NULL, song->mutable_comment());
break;
}
}
// Parse FMPS frames
for (int i=0 ; i<map["TXXX"].size() ; ++i) {
const TagLib::ID3v2::UserTextIdentificationFrame* frame =
dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i]);
if (frame && frame->description().startsWith("FMPS_")) {
ParseFMPSFrame(TStringToQString(frame->description()),
TStringToQString(frame->fieldList()[1]),
song);
}
}
}
} else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
if (file->tag()) {
ParseOggTag(file->tag()->fieldListMap(), NULL, &disc, &compilation, song);
}
Decode(tag->comment(), NULL, song->mutable_comment());
} else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
if ( file->xiphComment() ) {
ParseOggTag(file->xiphComment()->fieldListMap(), NULL, &disc, &compilation, song);
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
if (!file->pictureList().isEmpty()) {
song->set_art_automatic(kEmbeddedCover);
}
#endif
}
Decode(tag->comment(), NULL, song->mutable_comment());
} else if (TagLib::MP4::File* file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
if (file->tag()) {
TagLib::MP4::Tag* mp4_tag = file->tag();
const TagLib::MP4::ItemListMap& items = mp4_tag->itemListMap();
TagLib::MP4::ItemListMap::ConstIterator it = items.find("aART");
if (it != items.end()) {
TagLib::StringList album_artists = it->second.toStringList();
if (!album_artists.isEmpty()) {
Decode(album_artists.front(), NULL, song->mutable_albumartist());
}
}
}
} else if (tag) {
Decode(tag->comment(), NULL, song->mutable_comment());
}
if (!disc.isEmpty()) {
const int i = disc.indexOf('/');
if (i != -1) {
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
song->set_disc(disc.left(i).toInt());
} else {
song->set_disc(disc.toInt());
}
}
if (compilation.isEmpty()) {
// well, it wasn't set, but if the artist is VA assume it's a compilation
if (QStringFromStdString(song->artist()).toLower() == "various artists") {
song->set_compilation(true);
}
} else {
song->set_compilation(compilation.toInt() == 1);
}
if (fileref->audioProperties()) {
song->set_bitrate(fileref->audioProperties()->bitrate());
song->set_samplerate(fileref->audioProperties()->sampleRate());
song->set_length_nanosec(fileref->audioProperties()->length() * kNsecPerSec);
}
// Get the filetype if we can
song->set_type(GuessFileType(fileref.get()));
// Set integer fields to -1 if they're not valid
#define SetDefault(field) if (song->field() <= 0) { song->set_##field(-1); }
SetDefault(track);
SetDefault(disc);
SetDefault(bpm);
SetDefault(year);
SetDefault(bitrate);
SetDefault(samplerate);
SetDefault(lastplayed);
SetDefault(rating);
#undef SetDefault
}
void TagReaderWorker::Decode(const TagLib::String& tag, const QTextCodec* codec,
std::string* output) {
QString tmp;
if (codec && tag.isLatin1()) { // Never override UTF-8.
const std::string fixed = QString::fromUtf8(tag.toCString(true)).toStdString();
tmp = codec->toUnicode(fixed.c_str()).trimmed();
} else {
tmp = TStringToQString(tag).trimmed();
}
output->assign(DataCommaSizeFromQString(tmp));
}
void TagReaderWorker::Decode(const QString& tag, const QTextCodec* codec,
std::string* output) {
if (!codec) {
output->assign(DataCommaSizeFromQString(tag));
} else {
const QString decoded(codec->toUnicode(tag.toUtf8()));
output->assign(DataCommaSizeFromQString(decoded));
}
}
void TagReaderWorker::ParseFMPSFrame(const QString& name, const QString& value,
pb::tagreader::SongMetadata* song) const {
FMPSParser parser;
if (!parser.Parse(value) || parser.is_empty())
return;
QVariant var;
if (name == "FMPS_Rating") {
var = parser.result()[0][0];
if (var.type() == QVariant::Double) {
song->set_rating(var.toDouble());
}
} else if (name == "FMPS_Rating_User") {
// Take a user rating only if there's no rating already set
if (song->rating() == -1 && parser.result()[0].count() >= 2) {
var = parser.result()[0][1];
if (var.type() == QVariant::Double) {
song->set_rating(var.toDouble());
}
}
} else if (name == "FMPS_PlayCount") {
var = parser.result()[0][0];
if (var.type() == QVariant::Double) {
song->set_playcount(var.toDouble());
}
} else if (name == "FMPS_PlayCount_User") {
// Take a user rating only if there's no playcount already set
if (song->rating() == -1 && parser.result()[0].count() >= 2) {
var = parser.result()[0][1];
if (var.type() == QVariant::Double) {
song->set_playcount(var.toDouble());
}
}
}
}
void TagReaderWorker::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
const QTextCodec* codec,
QString* disc, QString* compilation,
pb::tagreader::SongMetadata* song) const {
if (!map["COMPOSER"].isEmpty())
Decode(map["COMPOSER"].front(), codec, song->mutable_composer());
if (!map["ALBUMARTIST"].isEmpty()) {
Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist());
} else if (!map["ALBUM ARTIST"].isEmpty()) {
Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist());
}
if (!map["BPM"].isEmpty() )
song->set_bpm(TStringToQString( map["BPM"].front() ).trimmed().toFloat());
if (!map["DISCNUMBER"].isEmpty() )
*disc = TStringToQString( map["DISCNUMBER"].front() ).trimmed();
if (!map["COMPILATION"].isEmpty() )
*compilation = TStringToQString( map["COMPILATION"].front() ).trimmed();
if (!map["COVERART"].isEmpty())
song->set_art_automatic(kEmbeddedCover);
}
pb::tagreader::SongMetadata_Type TagReaderWorker::GuessFileType(
TagLib::FileRef* fileref) const {
#ifdef TAGLIB_WITH_ASF
if (dynamic_cast<TagLib::ASF::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_ASF;
#endif
if (dynamic_cast<TagLib::FLAC::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_FLAC;
#ifdef TAGLIB_WITH_MP4
if (dynamic_cast<TagLib::MP4::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_MP4;
#endif
if (dynamic_cast<TagLib::MPC::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_MPC;
if (dynamic_cast<TagLib::MPEG::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_MPEG;
if (dynamic_cast<TagLib::Ogg::FLAC::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_OGGFLAC;
if (dynamic_cast<TagLib::Ogg::Speex::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_OGGSPEEX;
if (dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_OGGVORBIS;
if (dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_AIFF;
if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_WAV;
if (dynamic_cast<TagLib::TrueAudio::File*>(fileref->file()))
return pb::tagreader::SongMetadata_Type_TRUEAUDIO;
return pb::tagreader::SongMetadata_Type_UNKNOWN;
}
bool TagReaderWorker::SaveFile(const QString& filename,
const pb::tagreader::SongMetadata& song) const {
if (filename.isNull())
return false;
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (!fileref || fileref->isNull()) // The file probably doesn't exist
return false;
fileref->tag()->setTitle(StdStringToTaglibString(song.title()));
fileref->tag()->setArtist(StdStringToTaglibString(song.artist()));
fileref->tag()->setAlbum(StdStringToTaglibString(song.album()));
fileref->tag()->setGenre(StdStringToTaglibString(song.genre()));
fileref->tag()->setComment(StdStringToTaglibString(song.comment()));
fileref->tag()->setYear(song.year());
fileref->tag()->setTrack(song.track());
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
SetTextFrame("TPOS", song.disc() <= 0 -1 ? QString() : QString::number(song.disc()), tag);
SetTextFrame("TBPM", song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm()), tag);
SetTextFrame("TCOM", song.composer(), tag);
SetTextFrame("TPE2", song.albumartist(), tag);
SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag);
}
else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
TagLib::Ogg::XiphComment* tag = file->tag();
tag->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
tag->addField("BPM", QStringToTaglibString(song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm())), true);
tag->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 -1 ? QString() : QString::number(song.disc())), true);
tag->addField("COMPILATION", StdStringToTaglibString(song.compilation() ? "1" : "0"), true);
}
else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment* tag = file->xiphComment();
tag->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
tag->addField("BPM", QStringToTaglibString(song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm())), true);
tag->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 -1 ? QString() : QString::number(song.disc())), true);
tag->addField("COMPILATION", StdStringToTaglibString(song.compilation() ? "1" : "0"), true);
}
bool ret = fileref->save();
#ifdef Q_OS_LINUX
if (ret) {
// Linux: inotify doesn't seem to notice the change to the file unless we
// change the timestamps as well. (this is what touch does)
utimensat(0, QFile::encodeName(filename).constData(), NULL, 0);
}
#endif // Q_OS_LINUX
return ret;
}
void TagReaderWorker::SetTextFrame(const char* id, const QString& value,
TagLib::ID3v2::Tag* tag) const {
const QByteArray utf8(value.toUtf8());
SetTextFrame(id, std::string(utf8.constData(), utf8.length()), tag);
}
void TagReaderWorker::SetTextFrame(const char* id, const std::string& value,
TagLib::ID3v2::Tag* tag) const {
TagLib::ByteVector id_vector(id);
// Remove the frame if it already exists
while (tag->frameListMap().contains(id_vector) &&
tag->frameListMap()[id_vector].size() != 0) {
tag->removeFrame(tag->frameListMap()[id_vector].front());
}
// Create and add a new frame
TagLib::ID3v2::TextIdentificationFrame* frame =
new TagLib::ID3v2::TextIdentificationFrame(id_vector,
TagLib::String::UTF8);
frame->setText(StdStringToTaglibString(value));
tag->addFrame(frame);
}
bool TagReaderWorker::IsMediaFile(const QString& filename) const {
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
return !fileref->isNull() && fileref->tag();
}
QByteArray TagReaderWorker::LoadEmbeddedArt(const QString& filename) const {
if (filename.isEmpty())
return QByteArray();
#ifdef Q_OS_WIN32
TagLib::FileRef ref(filename.toStdWString().c_str());
#else
TagLib::FileRef ref(QFile::encodeName(filename).constData());
#endif
if (ref.isNull() || !ref.file())
return QByteArray();
// MP3
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref.file());
if (file && file->ID3v2Tag()) {
TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"];
if (apic_frames.isEmpty())
return QByteArray();
TagLib::ID3v2::AttachedPictureFrame* pic =
static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
return QByteArray((const char*) pic->picture().data(), pic->picture().size());
}
// Ogg vorbis/speex
TagLib::Ogg::XiphComment* xiph_comment =
dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag());
if (xiph_comment) {
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
// Ogg lacks a definitive standard for embedding cover art, but it seems
// b64 encoding a field called COVERART is the general convention
if (!map.contains("COVERART"))
return QByteArray();
return QByteArray::fromBase64(map["COVERART"].toString().toCString());
}
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
// Flac
TagLib::FLAC::File* flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file());
if (flac_file && flac_file->xiphComment()) {
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
if (!pics.isEmpty()) {
// Use the first picture in the file - this could be made cleverer and
// pick the front cover if it's present.
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
TagLib::FLAC::Picture* picture = *it;
return QByteArray(picture->data().data(), picture->data().size());
}
}
#endif
return QByteArray();
}

View File

@ -0,0 +1,75 @@
/* This file is part of Clementine.
Copyright 2011, 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/>.
*/
#ifndef TAGREADERWORKER_H
#define TAGREADERWORKER_H
#include "messagehandler.h"
#include "tagreadermessages.pb.h"
#include <taglib/xiphcomment.h>
namespace TagLib {
class FileRef;
class String;
namespace ID3v2 {
class Tag;
}
}
class FileRefFactory;
class TagReaderWorker : public AbstractMessageHandler<pb::tagreader::Message> {
public:
TagReaderWorker(QIODevice* socket, QObject* parent = NULL);
protected:
void MessageArrived(const pb::tagreader::Message& message);
private:
void ReadFile(const QString& filename, pb::tagreader::SongMetadata* song) const;
bool SaveFile(const QString& filename, const pb::tagreader::SongMetadata& song) const;
bool IsMediaFile(const QString& filename) const;
QByteArray LoadEmbeddedArt(const QString& filename) const;
static void Decode(const TagLib::String& tag, const QTextCodec* codec,
std::string* output);
static void Decode(const QString& tag, const QTextCodec* codec,
std::string* output);
void ParseFMPSFrame(const QString& name, const QString& value,
pb::tagreader::SongMetadata* song) const;
void ParseOggTag(const TagLib::Ogg::FieldListMap& map,
const QTextCodec* codec,
QString* disc, QString* compilation,
pb::tagreader::SongMetadata* song) const;
pb::tagreader::SongMetadata_Type GuessFileType(TagLib::FileRef* fileref) const;
void SetTextFrame(const char* id, const QString& value,
TagLib::ID3v2::Tag* tag) const;
void SetTextFrame(const char* id, const std::string& value,
TagLib::ID3v2::Tag* tag) const;
private:
FileRefFactory* factory_;
const std::string kEmbeddedCover;
};
#endif // TAGREADERWORKER_H