Start work on moving everything that uses taglib out into an external process.
This commit is contained in:
parent
527135abb1
commit
85f2f087cb
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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
|
@ -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
|
||||
)
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue