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(Boost REQUIRED)
|
||||||
find_package(Gettext REQUIRED)
|
find_package(Gettext REQUIRED)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
find_package(Protobuf)
|
find_package(Protobuf REQUIRED)
|
||||||
|
|
||||||
pkg_check_modules(TAGLIB REQUIRED taglib>=1.6)
|
pkg_check_modules(TAGLIB REQUIRED taglib>=1.6)
|
||||||
pkg_check_modules(QJSON REQUIRED QJson)
|
pkg_check_modules(QJSON REQUIRED QJson)
|
||||||
|
@ -366,6 +366,8 @@ add_subdirectory(3rdparty/universalchardet)
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
add_subdirectory(dist)
|
add_subdirectory(dist)
|
||||||
add_subdirectory(tools/ultimate_lyrics_parser)
|
add_subdirectory(tools/ultimate_lyrics_parser)
|
||||||
|
add_subdirectory(tagreader/common)
|
||||||
|
add_subdirectory(tagreader/tagreader)
|
||||||
|
|
||||||
option(WITH_DEBIAN OFF)
|
option(WITH_DEBIAN OFF)
|
||||||
if(WITH_DEBIAN)
|
if(WITH_DEBIAN)
|
||||||
|
|
|
@ -64,14 +64,11 @@ set(SOURCES
|
||||||
core/crashreporting.cpp
|
core/crashreporting.cpp
|
||||||
core/database.cpp
|
core/database.cpp
|
||||||
core/deletefiles.cpp
|
core/deletefiles.cpp
|
||||||
core/encoding.cpp
|
|
||||||
core/filesystemmusicstorage.cpp
|
core/filesystemmusicstorage.cpp
|
||||||
core/fht.cpp
|
core/fht.cpp
|
||||||
core/fmpsparser.cpp
|
|
||||||
core/globalshortcutbackend.cpp
|
core/globalshortcutbackend.cpp
|
||||||
core/globalshortcuts.cpp
|
core/globalshortcuts.cpp
|
||||||
core/gnomeglobalshortcutbackend.cpp
|
core/gnomeglobalshortcutbackend.cpp
|
||||||
core/logging.cpp
|
|
||||||
core/mergedproxymodel.cpp
|
core/mergedproxymodel.cpp
|
||||||
core/multisortfilterproxy.cpp
|
core/multisortfilterproxy.cpp
|
||||||
core/musicstorage.cpp
|
core/musicstorage.cpp
|
||||||
|
@ -956,13 +953,11 @@ add_dependencies(clementine_lib pot)
|
||||||
|
|
||||||
|
|
||||||
target_link_libraries(clementine_lib
|
target_link_libraries(clementine_lib
|
||||||
chardet
|
|
||||||
echoprint
|
echoprint
|
||||||
sha2
|
sha2
|
||||||
${ECHONEST_LIBRARIES}
|
${ECHONEST_LIBRARIES}
|
||||||
${GOBJECT_LIBRARIES}
|
${GOBJECT_LIBRARIES}
|
||||||
${GLIB_LIBRARIES}
|
${GLIB_LIBRARIES}
|
||||||
${TAGLIB_LIBRARIES}
|
|
||||||
${QJSON_LIBRARIES}
|
${QJSON_LIBRARIES}
|
||||||
${QT_LIBRARIES}
|
${QT_LIBRARIES}
|
||||||
${GSTREAMER_BASE_LIBRARIES}
|
${GSTREAMER_BASE_LIBRARIES}
|
||||||
|
|
|
@ -15,38 +15,15 @@
|
||||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "fmpsparser.h"
|
#include "encoding.h"
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
#include "mpris_common.h"
|
#include "mpris_common.h"
|
||||||
#include "song.h"
|
#include "song.h"
|
||||||
#include "timeconstants.h"
|
#include "timeconstants.h"
|
||||||
|
#include "tagreader/common/messagehandler.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#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
|
#ifdef HAVE_LIBLASTFM
|
||||||
#include "internet/fixlastfm.h"
|
#include "internet/fixlastfm.h"
|
||||||
#include <lastfm/Track>
|
#include <lastfm/Track>
|
||||||
|
@ -62,6 +39,8 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtConcurrentRun>
|
#include <QtConcurrentRun>
|
||||||
|
|
||||||
|
#include <id3v1genres.h>
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
# include <mswmdm.h>
|
# include <mswmdm.h>
|
||||||
# include <QUuid>
|
# include <QUuid>
|
||||||
|
@ -79,7 +58,6 @@
|
||||||
#include <boost/scoped_ptr.hpp>
|
#include <boost/scoped_ptr.hpp>
|
||||||
using boost::scoped_ptr;
|
using boost::scoped_ptr;
|
||||||
|
|
||||||
#include "encoding.h"
|
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
#include "covers/albumcoverloader.h"
|
#include "covers/albumcoverloader.h"
|
||||||
#include "engines/enginebase.h"
|
#include "engines/enginebase.h"
|
||||||
|
@ -87,12 +65,6 @@ using boost::scoped_ptr;
|
||||||
#include "widgets/trackslider.h"
|
#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 {
|
namespace {
|
||||||
|
|
||||||
QStringList Prepend(const QString& text, const QStringList& list) {
|
QStringList Prepend(const QString& text, const QStringList& list) {
|
||||||
|
@ -109,10 +81,6 @@ QStringList Updateify(const QStringList& list) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
TagLib::String QStringToTaglibString(const QString& s) {
|
|
||||||
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,18 +110,10 @@ const QString Song::kFtsUpdateSpec = Updateify(Song::kFtsColumns).join(", ");
|
||||||
const QString Song::kManuallyUnsetCover = "(unset)";
|
const QString Song::kManuallyUnsetCover = "(unset)";
|
||||||
const QString Song::kEmbeddedCover = "(embedded)";
|
const QString Song::kEmbeddedCover = "(embedded)";
|
||||||
|
|
||||||
TagLibFileRefFactory Song::kDefaultFactory;
|
|
||||||
QMutex Song::sTaglibMutex;
|
|
||||||
|
|
||||||
|
|
||||||
struct Song::Private : public QSharedData {
|
struct Song::Private : public QSharedData {
|
||||||
Private();
|
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_;
|
bool valid_;
|
||||||
int id_;
|
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()
|
Song::Song()
|
||||||
: d(new Private),
|
: d(new Private)
|
||||||
factory_(&kDefaultFactory)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Song::Song(const Song &other)
|
Song::Song(const Song &other)
|
||||||
: d(other.d),
|
: d(other.d)
|
||||||
factory_(&kDefaultFactory)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Song::Song(FileRefFactory* factory)
|
|
||||||
: d(new Private),
|
|
||||||
factory_(factory) {
|
|
||||||
}
|
|
||||||
|
|
||||||
Song::~Song() {
|
Song::~Song() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Song& Song::operator =(const Song& other) {
|
Song& Song::operator =(const Song& other) {
|
||||||
d = other.d;
|
d = other.d;
|
||||||
factory_ = other.factory_;
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,15 +375,6 @@ void Song::set_genre_id3(int id) {
|
||||||
set_genre(TStringToQString(TagLib::ID3v1::genre(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) {
|
QString Song::Decode(const QString& tag, const QTextCodec* codec) {
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
return tag;
|
return tag;
|
||||||
|
@ -447,275 +383,71 @@ QString Song::Decode(const QString& tag, const QTextCodec* codec) {
|
||||||
return codec->toUnicode(tag.toUtf8());
|
return codec->toUnicode(tag.toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::HasProperMediaFile() const {
|
void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
|
||||||
#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
|
|
||||||
|
|
||||||
d->init_from_file_ = true;
|
d->init_from_file_ = true;
|
||||||
|
d->valid_ = pb.valid();
|
||||||
d->url_ = QUrl::fromLocalFile(filename);
|
d->title_ = QStringFromStdString(pb.title());
|
||||||
d->directory_id_ = directory_id;
|
d->album_ = QStringFromStdString(pb.album());
|
||||||
|
d->artist_ = QStringFromStdString(pb.artist());
|
||||||
QFileInfo info(filename);
|
d->albumartist_ = QStringFromStdString(pb.albumartist());
|
||||||
d->basefilename_ = info.fileName();
|
d->composer_ = QStringFromStdString(pb.composer());
|
||||||
|
d->track_ = pb.track();
|
||||||
QMutexLocker l(&sTaglibMutex);
|
d->disc_ = pb.disc();
|
||||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
d->bpm_ = pb.bpm();
|
||||||
|
d->year_ = pb.year();
|
||||||
if(fileref->isNull()) {
|
d->genre_ = QStringFromStdString(pb.genre());
|
||||||
return;
|
d->comment_ = QStringFromStdString(pb.comment());
|
||||||
}
|
d->compilation_ = pb.compilation();
|
||||||
|
d->rating_ = pb.rating();
|
||||||
d->filesize_ = info.size();
|
d->playcount_ = pb.playcount();
|
||||||
d->mtime_ = info.lastModified().toTime_t();
|
d->skipcount_ = pb.skipcount();
|
||||||
d->ctime_ = info.created().toTime_t();
|
d->lastplayed_ = pb.lastplayed();
|
||||||
|
d->score_ = pb.score();
|
||||||
// This is single byte encoding, therefore can't be CJK.
|
set_length_nanosec(pb.length_nanosec());
|
||||||
UniversalEncodingHandler detector(NS_FILTER_NON_CJK);
|
d->bitrate_ = pb.bitrate();
|
||||||
|
d->samplerate_ = pb.samplerate();
|
||||||
TagLib::Tag* tag = fileref->tag();
|
d->url_ = QUrl::fromEncoded(QByteArray(pb.url().data(), pb.url().size()));
|
||||||
QTextCodec* codec = NULL;
|
d->basefilename_ = QStringFromStdString(pb.basefilename());
|
||||||
if (tag) {
|
d->mtime_ = pb.mtime();
|
||||||
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file());
|
d->ctime_ = pb.ctime();
|
||||||
if (file && (file->ID3v2Tag() || file->ID3v1Tag())) {
|
d->filesize_ = pb.filesize();
|
||||||
codec = detector.Guess(*fileref);
|
d->suspicious_tags_ = pb.suspicious_tags();
|
||||||
}
|
d->art_automatic_ = QStringFromStdString(pb.art_automatic());
|
||||||
if (codec &&
|
d->filetype_ = static_cast<FileType>(pb.type());
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Song::ParseFMPSFrame(const QString& name, const QString& value) {
|
void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
|
||||||
FMPSParser parser;
|
const QByteArray url(d->url_.toEncoded());
|
||||||
if (!parser.Parse(value) || parser.is_empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
QVariant var;
|
pb->set_valid(d->valid_);
|
||||||
if (name == "FMPS_Rating") {
|
pb->set_title(DataCommaSizeFromQString(d->title_));
|
||||||
var = parser.result()[0][0];
|
pb->set_album(DataCommaSizeFromQString(d->album_));
|
||||||
if (var.type() == QVariant::Double) {
|
pb->set_artist(DataCommaSizeFromQString(d->artist_));
|
||||||
d->rating_ = var.toDouble();
|
pb->set_albumartist(DataCommaSizeFromQString(d->albumartist_));
|
||||||
}
|
pb->set_composer(DataCommaSizeFromQString(d->composer_));
|
||||||
} else if (name == "FMPS_Rating_User") {
|
pb->set_track(d->track_);
|
||||||
// Take a user rating only if there's no rating already set
|
pb->set_disc(d->disc_);
|
||||||
if (d->rating_ == -1 && parser.result()[0].count() >= 2) {
|
pb->set_bpm(d->bpm_);
|
||||||
var = parser.result()[0][1];
|
pb->set_year(d->year_);
|
||||||
if (var.type() == QVariant::Double) {
|
pb->set_genre(DataCommaSizeFromQString(d->genre_));
|
||||||
d->rating_ = var.toDouble();
|
pb->set_comment(DataCommaSizeFromQString(d->comment_));
|
||||||
}
|
pb->set_compilation(d->compilation_);
|
||||||
}
|
pb->set_rating(d->rating_);
|
||||||
} else if (name == "FMPS_PlayCount") {
|
pb->set_playcount(d->playcount_);
|
||||||
var = parser.result()[0][0];
|
pb->set_skipcount(d->skipcount_);
|
||||||
if (var.type() == QVariant::Double) {
|
pb->set_lastplayed(d->lastplayed_);
|
||||||
d->playcount_ = var.toDouble();
|
pb->set_score(d->score_);
|
||||||
}
|
pb->set_length_nanosec(length_nanosec());
|
||||||
} else if (name == "FMPS_PlayCount_User") {
|
pb->set_bitrate(d->bitrate_);
|
||||||
// Take a user rating only if there's no playcount already set
|
pb->set_samplerate(d->samplerate_);
|
||||||
if (d->rating_ == -1 && parser.result()[0].count() >= 2) {
|
pb->set_url(url.constData(), url.size());
|
||||||
var = parser.result()[0][1];
|
pb->set_basefilename(DataCommaSizeFromQString(d->basefilename_));
|
||||||
if (var.type() == QVariant::Double) {
|
pb->set_mtime(d->mtime_);
|
||||||
d->playcount_ = var.toDouble();
|
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::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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
|
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_;
|
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 {
|
bool Song::IsEditable() const {
|
||||||
return d->valid_ && !d->url_.isEmpty() && !is_stream() &&
|
return d->valid_ && !d->url_.isEmpty() && !is_stream() &&
|
||||||
d->filetype_ != Type_Unknown && !has_cue();
|
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 {
|
bool Song::operator==(const Song& other) const {
|
||||||
// TODO: this isn't working for radios
|
// TODO: this isn't working for radios
|
||||||
return url() == other.url() &&
|
return url() == other.url() &&
|
||||||
|
@ -1468,79 +1119,6 @@ uint qHash(const Song& song) {
|
||||||
return qHash(song.url().toString()) ^ qHash(song.beginning_nanosec());
|
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 {
|
bool Song::IsOnSameAlbum(const Song& other) const {
|
||||||
if (is_compilation() != other.is_compilation())
|
if (is_compilation() != other.is_compilation())
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "engines/engine_fwd.h"
|
#include "engines/engine_fwd.h"
|
||||||
|
#include "tagreader/common/tagreadermessages.pb.h"
|
||||||
|
|
||||||
class QSqlQuery;
|
class QSqlQuery;
|
||||||
class QUrl;
|
class QUrl;
|
||||||
|
@ -48,35 +49,13 @@ class QUrl;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace TagLib {
|
|
||||||
class FileRef;
|
|
||||||
class String;
|
|
||||||
|
|
||||||
namespace ID3v2 {
|
|
||||||
class Tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SqlRow;
|
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 {
|
class Song {
|
||||||
public:
|
public:
|
||||||
Song();
|
Song();
|
||||||
Song(const Song& other);
|
Song(const Song& other);
|
||||||
Song(FileRefFactory* factory);
|
|
||||||
~Song();
|
~Song();
|
||||||
|
|
||||||
static const QStringList kColumns;
|
static const QStringList kColumns;
|
||||||
|
@ -94,7 +73,8 @@ class Song {
|
||||||
|
|
||||||
static QString JoinSpec(const QString& table);
|
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 {
|
enum FileType {
|
||||||
Type_Unknown = 0,
|
Type_Unknown = 0,
|
||||||
Type_Asf = 1,
|
Type_Asf = 1,
|
||||||
|
@ -115,18 +95,10 @@ class Song {
|
||||||
static QString TextForFiletype(FileType type);
|
static QString TextForFiletype(FileType type);
|
||||||
QString TextForFiletype() const { return TextForFiletype(filetype()); }
|
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
|
// 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 length_nanosec);
|
||||||
void Init(const QString& title, const QString& artist, const QString& album, qint64 beginning, qint64 end);
|
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 InitFromQuery(const SqlRow& query, bool reliable_metadata, int col = 0);
|
||||||
void InitFromFilePartial(const QString& filename); // Just store the filename: incomplete but fast
|
void InitFromFilePartial(const QString& filename); // Just store the filename: incomplete but fast
|
||||||
#ifdef HAVE_LIBLASTFM
|
#ifdef HAVE_LIBLASTFM
|
||||||
|
@ -150,7 +122,6 @@ class Song {
|
||||||
void ToWmdm(IWMDMMetaData* metadata) const;
|
void ToWmdm(IWMDMMetaData* metadata) const;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static QString Decode(const TagLib::String& tag, const QTextCodec* codec = NULL);
|
|
||||||
static QString Decode(const QString& tag, const QTextCodec* codec = NULL);
|
static QString Decode(const QString& tag, const QTextCodec* codec = NULL);
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
|
@ -160,6 +131,7 @@ class Song {
|
||||||
void ToLastFM(lastfm::Track* track) const;
|
void ToLastFM(lastfm::Track* track) const;
|
||||||
#endif
|
#endif
|
||||||
void ToXesam(QVariantMap* map) const;
|
void ToXesam(QVariantMap* map) const;
|
||||||
|
void ToProtobuf(pb::tagreader::SongMetadata* pb) const;
|
||||||
|
|
||||||
// Simple accessors
|
// Simple accessors
|
||||||
bool is_valid() const;
|
bool is_valid() const;
|
||||||
|
@ -296,24 +268,9 @@ class Song {
|
||||||
|
|
||||||
Song& operator=(const Song& other);
|
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:
|
private:
|
||||||
struct Private;
|
struct Private;
|
||||||
QSharedDataPointer<Private> d;
|
QSharedDataPointer<Private> d;
|
||||||
|
|
||||||
FileRefFactory* factory_;
|
|
||||||
|
|
||||||
static TagLibFileRefFactory kDefaultFactory;
|
|
||||||
|
|
||||||
static QMutex sTaglibMutex;
|
|
||||||
};
|
};
|
||||||
Q_DECLARE_METATYPE(Song);
|
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
|
#ifdef Q_OS_LINUX
|
||||||
// Force Clementine's menu to be shown in the Clementine window and not in
|
// Force Clementine's menu to be shown in the Clementine window and not in
|
||||||
// the Unity global menubar thing. See:
|
// 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