1
0
mirror of https://github.com/clementine-player/Clementine synced 2025-01-31 03:27:40 +01:00

Parse FMPS ratings and played counts from mp3 id3v2 tags

This commit is contained in:
David Sansome 2010-10-17 16:03:49 +00:00
parent 6c36198103
commit eb9660edad
16 changed files with 492 additions and 6 deletions

View File

@ -42,6 +42,7 @@ set(SOURCES
core/encoding.cpp
core/filesystemmusicstorage.cpp
core/fht.cpp
core/fmpsparser.cpp
core/globalshortcutbackend.cpp
core/globalshortcuts.cpp
core/gnomeglobalshortcutbackend.cpp

98
src/core/fmpsparser.cpp Normal file
View File

@ -0,0 +1,98 @@
/* This file is part of Clementine.
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 <QStringList>
#include <QtDebug>
#include <boost/bind.hpp>
FMPSParser::FMPSParser()
: float_re_("\\s*([+-]?\\d+(?:\\.\\d+)?)\\s*(?:$|(?=::|;;))"),
string_re_("((?:[^\\\\;:]|(?:\\\\[\\\\:;]))+)(?:$|(?=::|;;))"),
escape_re_("\\\\([\\\\:;])")
{
}
template <char Separator, typename F, typename T>
static int ParseContainer(const QStringRef& data, F f, QList<T>* ret) {
ret->clear();
T value;
int pos = 0;
while (pos < data.length()) {
const int len = data.length() - pos;
int matched_len = f(QStringRef(data.string(), data.position() + pos, len), &value);
if (matched_len == -1 || matched_len > len)
break;
ret->append(value);
pos += matched_len;
if (pos + 2 <= data.length() && data.at(pos) == Separator
&& data.at(pos+1) == Separator) {
pos += 2;
} else {
break;
}
}
return pos;
}
bool FMPSParser::Parse(const QString& data) {
result_ = Result();
return ParseListList(data, &result_) == data.length();
}
int FMPSParser::ParseValue(const QString& data, QVariant* ret) const {
return ParseValueRef(QStringRef(&data), ret);
}
int FMPSParser::ParseValueRef(const QStringRef& data, QVariant* ret) const {
int pos = float_re_.indexIn(*data.string(), data.position());
if (pos == data.position()) {
*ret = float_re_.cap(1).toDouble();
return float_re_.matchedLength();
}
pos = string_re_.indexIn(*data.string(), data.position());
if (pos == data.position()) {
QString value = string_re_.cap(1);
value.replace(escape_re_, "\\1");
*ret = value;
return string_re_.matchedLength();
}
return -1;
}
int FMPSParser::ParseList(const QString& data, QVariantList* ret) const {
return ParseListRef(QStringRef(&data), ret);
}
int FMPSParser::ParseListRef(const QStringRef& data, QVariantList* ret) const {
return ParseContainer<':'>(data, boost::bind(&FMPSParser::ParseValueRef, this, _1, _2), ret);
}
int FMPSParser::ParseListList(const QString& data, Result* ret) const {
return ParseListListRef(QStringRef(&data), ret);
}
int FMPSParser::ParseListListRef(const QStringRef& data, Result* ret) const {
return ParseContainer<';'>(data, boost::bind(&FMPSParser::ParseListRef, this, _1, _2), ret);
}

50
src/core/fmpsparser.h Normal file
View File

@ -0,0 +1,50 @@
/* This file is part of Clementine.
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 FMPSPARSER_H
#define FMPSPARSER_H
#include <QRegExp>
#include <QVariantList>
class FMPSParser {
public:
FMPSParser();
typedef QList<QVariantList> Result;
bool Parse(const QString& data);
Result result() const { return result_; }
bool is_empty() const { return result().isEmpty() || result()[0].isEmpty(); }
int ParseValue(const QString& data, QVariant* ret) const;
int ParseValueRef(const QStringRef& data, QVariant* ret) const;
int ParseList(const QString& data, QVariantList* ret) const;
int ParseListRef(const QStringRef& data, QVariantList* ret) const;
int ParseListList(const QString& data, Result* ret) const;
int ParseListListRef(const QStringRef& data, Result* ret) const;
private:
QRegExp float_re_;
QRegExp string_re_;
QRegExp escape_re_;
Result result_;
};
#endif // FMPSPARSER_H

View File

@ -14,6 +14,7 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "fmpsparser.h"
#include "song.h"
#include <algorithm>
@ -148,6 +149,8 @@ Song::Private::Private()
sampler_(false),
forced_compilation_on_(false),
forced_compilation_off_(false),
rating_(-1.0),
playcount_(0),
length_(-1),
bitrate_(-1),
samplerate_(-1),
@ -279,6 +282,17 @@ void Song::InitFromFile(const QString& filename, int directory_id) {
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->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()) {
@ -322,6 +336,41 @@ void Song::InitFromFile(const QString& filename, int directory_id) {
GuessFileType(fileref.get());
}
void Song::ParseFMPSFrame(const QString& name, const QString& value) {
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) {
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::ParseOggTag(const TagLib::Ogg::FieldListMap& map, const QTextCodec* codec,
QString* disc, QString* compilation) {
if (!map["COMPOSER"].isEmpty())
@ -409,9 +458,9 @@ void Song::InitFromQuery(const SqlRow& q, int col) {
d->art_manual_ = q.value(col + 23).toString();
d->filetype_ = FileType(q.value(col + 24).toInt());
// playcount = 25
d->playcount_ = q.value(col + 25).isNull() ? 0 : q.value(col + 25).toInt();
// lastplayed = 26
// rating = 27
d->rating_ = tofloat(col + 27);
d->forced_compilation_on_ = q.value(col + 28).toBool();
d->forced_compilation_off_ = q.value(col + 29).toBool();
@ -457,6 +506,8 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
d->ctime_ = track->time_added;
d->filesize_ = track->size;
d->filetype_ = track->type2 ? Type_Mpeg : Type_Mp4;
d->rating_ = float(track->rating) / 100; // 100 = 20 * 5 stars
d->playcount_ = track->playcount;
d->filename_ = QString::fromLocal8Bit(track->ipod_path);
d->filename_.replace(':', '/');
@ -485,6 +536,8 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
track->type1 = 0;
track->type2 = d->filetype_ == Type_Mp4 ? 0 : 1;
track->mediatype = 1; // Audio
track->rating = d->rating_ * 100; // 100 = 20 * 5 stars
track->playcount = d->playcount_;
}
#endif
@ -508,6 +561,9 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
d->mtime_ = track->modificationdate;
d->ctime_ = track->modificationdate;
d->rating_ = float(track->rating) / 100;
d->playcount_ = track->usecount;
switch (track->filetype) {
case LIBMTP_FILETYPE_WAV: d->filetype_ = Type_Wav; break;
case LIBMTP_FILETYPE_MP3: d->filetype_ = Type_Mpeg; break;
@ -544,8 +600,8 @@ void Song::InitFromLastFM(const lastfm::Track& track) {
track->wavecodec = 0;
track->bitrate = d->bitrate_;
track->bitratetype = 0;
track->rating = 0;
track->usecount = 0;
track->rating = d->rating_ * 100;
track->usecount = d->playcount_;
track->filesize = d->filesize_;
track->modificationdate = d->mtime_;
@ -848,9 +904,9 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":art_manual", d->art_manual_);
query->bindValue(":filetype", d->filetype_);
query->bindValue(":playcount", 0); // TODO
query->bindValue(":playcount", d->playcount_);
query->bindValue(":lastplayed", -1); // TODO
query->bindValue(":rating", -1);
query->bindValue(":rating", intval(d->rating_));
query->bindValue(":forced_compilation_on", d->forced_compilation_on_ ? 1 : 0);
query->bindValue(":forced_compilation_off", d->forced_compilation_off_ ? 1 : 0);

View File

@ -157,6 +157,8 @@ class Song {
return (d->compilation_ || d->sampler_ || d->forced_compilation_on_)
&& ! d->forced_compilation_off_;
}
float rating() const { return d->rating_; }
int playcount() const { return d->playcount_; }
int length() const { return d->length_; }
int bitrate() const { return d->bitrate_; }
@ -213,6 +215,8 @@ class Song {
void set_image(const QImage& i) { d->image_ = i; }
void set_forced_compilation_on(bool v) { d->forced_compilation_on_ = v; }
void set_forced_compilation_off(bool v) { d->forced_compilation_off_ = v; }
void set_rating(float v) { d->rating_ = v; }
void set_playcount(int v) { d->playcount_ = v; }
// Setters that should only be used by tests
void set_filename(const QString& v) { d->filename_ = v; }
@ -229,6 +233,7 @@ class 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 : public QSharedData {
@ -253,6 +258,9 @@ class Song {
bool forced_compilation_on_; // Set by the user
bool forced_compilation_off_; // Set by the user
float rating_;
int playcount_;
int length_;
int bitrate_;
int samplerate_;

View File

@ -100,6 +100,7 @@ add_test_file(asxparser_test.cpp false)
add_test_file(asxiniparser_test.cpp false)
add_test_file(database_test.cpp false)
add_test_file(fileformats_test.cpp false)
add_test_file(fmpsparser_test.cpp false)
add_test_file(librarybackend_test.cpp false)
add_test_file(librarymodel_test.cpp true)
add_test_file(m3uparser_test.cpp false)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/data/fmpsrating.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -15,5 +15,11 @@
<file>secretagent.asx</file>
<file>secretagent.pls</file>
<file>test.asxini</file>
<file>fmpsplaycount.mp3</file>
<file>fmpsplaycountboth.mp3</file>
<file>fmpsplaycountuser.mp3</file>
<file>fmpsrating.mp3</file>
<file>fmpsratingboth.mp3</file>
<file>fmpsratinguser.mp3</file>
</qresource>
</RCC>

214
tests/fmpsparser_test.cpp Normal file
View File

@ -0,0 +1,214 @@
/* This file is part of Clementine.
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 "test_utils.h"
#include "gtest/gtest.h"
#include "core/fmpsparser.h"
#include <QtDebug>
class FMPSParserTest : public testing::Test {
protected:
FMPSParser parser_;
};
TEST_F(FMPSParserTest, ParseFloats) {
QVariant value;
EXPECT_EQ(1, parser_.ParseValue("0", &value));
EXPECT_EQ(QVariant::Double, value.type());
EXPECT_EQ(0, value.toDouble());
EXPECT_EQ(3, parser_.ParseValue("123", &value));
EXPECT_EQ(QVariant::Double, value.type());
EXPECT_EQ(123, value.toDouble());
EXPECT_EQ(3, parser_.ParseValue("0.0", &value));
EXPECT_EQ(QVariant::Double, value.type());
EXPECT_EQ(0, value.toDouble());
EXPECT_EQ(2, parser_.ParseValue("-1", &value));
EXPECT_EQ(QVariant::Double, value.type());
EXPECT_EQ(-1, value.toDouble());
EXPECT_EQ(5, parser_.ParseValue("-1.23", &value));
EXPECT_EQ(QVariant::Double, value.type());
EXPECT_EQ(-1.23, value.toDouble());
EXPECT_EQ(4, parser_.ParseValue("+123", &value));
EXPECT_EQ(QVariant::Double, value.type());
EXPECT_EQ(123, value.toDouble());
parser_.ParseValue("1.", &value);
EXPECT_NE(QVariant::Double, value.type());
parser_.ParseValue("abc", &value);
EXPECT_NE(QVariant::Double, value.type());
}
TEST_F(FMPSParserTest, ParseStrings) {
QVariant value;
EXPECT_EQ(3, parser_.ParseValue("abc", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("abc", value.toString());
EXPECT_EQ(8, parser_.ParseValue("foo\\\\bar", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("foo\\bar", value.toString());
EXPECT_EQ(8, parser_.ParseValue("foo\\:bar", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("foo:bar", value.toString());
EXPECT_EQ(8, parser_.ParseValue("foo\\;bar", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("foo;bar", value.toString());
EXPECT_EQ(12, parser_.ParseValue("foo\\\\\\:\\;bar", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("foo\\:;bar", value.toString());
EXPECT_EQ(2, parser_.ParseValue("1.", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("1.", value.toString());
EXPECT_EQ(5, parser_.ParseValue("1.abc", &value));
EXPECT_EQ(QVariant::String, value.type());
EXPECT_EQ("1.abc", value.toString());
EXPECT_EQ(-1, parser_.ParseValue("foo\\bar", &value));
EXPECT_EQ(-1, parser_.ParseValue("foo:bar", &value));
EXPECT_EQ(-1, parser_.ParseValue("foo;bar", &value));
}
TEST_F(FMPSParserTest, ParseLists) {
QVariantList value;
EXPECT_EQ(3, parser_.ParseList("abc", &value));
EXPECT_EQ(1, value.length());
EXPECT_EQ("abc", value[0]);
EXPECT_EQ(3, parser_.ParseList("123", &value));
EXPECT_EQ(1, value.length());
EXPECT_EQ(123, value[0]);
EXPECT_EQ(8, parser_.ParseList("abc::def", &value));
EXPECT_EQ(2, value.length());
EXPECT_EQ("abc", value[0]);
EXPECT_EQ("def", value[1]);
EXPECT_EQ(13, parser_.ParseList("abc::def::ghi", &value));
EXPECT_EQ(3, value.length());
EXPECT_EQ("abc", value[0]);
EXPECT_EQ("def", value[1]);
EXPECT_EQ("ghi", value[2]);
EXPECT_EQ(12, parser_.ParseList("ab\\:c::\\\\def", &value));
EXPECT_EQ(2, value.length());
EXPECT_EQ("ab:c", value[0]);
EXPECT_EQ("\\def", value[1]);
EXPECT_EQ(5, parser_.ParseList("abc::def:", &value));
EXPECT_EQ(1, value.length());
EXPECT_EQ("abc", value[0]);
}
TEST_F(FMPSParserTest, ParseListLists) {
FMPSParser::Result value;
EXPECT_EQ(8, parser_.ParseListList("abc::def", &value));
EXPECT_EQ(1, value.length());
EXPECT_EQ(2, value[0].length());
EXPECT_EQ("abc", value[0][0]);
EXPECT_EQ("def", value[0][1]);
EXPECT_EQ(18, parser_.ParseListList("abc::def;;123::456", &value));
EXPECT_EQ(2, value.length());
EXPECT_EQ(2, value[0].length());
EXPECT_EQ(2, value[1].length());
EXPECT_EQ("abc", value[0][0]);
EXPECT_EQ("def", value[0][1]);
EXPECT_EQ(123, value[1][0]);
EXPECT_EQ(456, value[1][1]);
}
TEST_F(FMPSParserTest, Parse) {
EXPECT_TRUE(parser_.Parse("abc"));
EXPECT_TRUE(parser_.Parse("abc::def"));
EXPECT_TRUE(parser_.Parse("abc::def;;123::456;;foo::bar"));
EXPECT_TRUE(parser_.Parse("1."));
EXPECT_TRUE(parser_.Parse("1.abc"));
EXPECT_FALSE(parser_.Parse("1:"));
EXPECT_FALSE(parser_.Parse("1;"));
EXPECT_FALSE(parser_.Parse("1:abc"));
EXPECT_FALSE(parser_.Parse("abc;"));
}
TEST_F(FMPSParserTest, SpecExamples) {
FMPSParser::Result expected;
expected.clear();
expected << (QVariantList() << "Alice Abba" << 0.6);
expected << (QVariantList() << "Bob Beatles" << 0.8);
ASSERT_TRUE(parser_.Parse("Alice Abba::0.6;;Bob Beatles::0.8"));
EXPECT_EQ(expected, parser_.result());
expected.clear();
expected << (QVariantList() << "Rolling Stone" << "Ralph Gleason" << 0.83);
expected << (QVariantList() << "musicOMH.com" << "FMPS_Nothing" << 0.76);
expected << (QVariantList() << "Metacritic" << "FMPS_Nothing" << 0.8);
expected << (QVariantList() << "FMPS_Nothing" << "Some Dude" << 0.9);
ASSERT_TRUE(parser_.Parse("Rolling Stone::Ralph Gleason::0.83;;musicOMH.com::FMPS_Nothing::0.76;;Metacritic::FMPS_Nothing::0.8;;FMPS_Nothing::Some Dude::0.9"));
EXPECT_EQ(expected, parser_.result());
expected.clear();
expected << (QVariantList() << "Amarok" << "AutoRate" << 0.52);
expected << (QVariantList() << "VLC" << "Standard" << 0.6);
expected << (QVariantList() << "QuodLibet" << "RatingPlugin:X" << 0.35);
expected << (QVariantList() << "The Free Music Player Alliance" << "Rating Algorithm 1" << 0.5);
ASSERT_TRUE(parser_.Parse("Amarok::AutoRate::0.52;;VLC::Standard::0.6;;QuodLibet::RatingPlugin\\:X::0.35;;The Free Music Player Alliance::Rating Algorithm 1::0.5"));
EXPECT_EQ(expected, parser_.result());
expected.clear();
expected << (QVariantList() << "Willy Nelson" << "Guitar");
expected << (QVariantList() << "Eric Clapton" << "Guitar (Backup)");
expected << (QVariantList() << "B.B. King" << "Vocals");
ASSERT_TRUE(parser_.Parse("Willy Nelson::Guitar;;Eric Clapton::Guitar (Backup);;B.B. King::Vocals"));
EXPECT_EQ(expected, parser_.result());
expected.clear();
expected << (QVariantList() << "Alice Aardvark" << "[lyrics]");
expected << (QVariantList() << "Bob Baboon" << "[lyrics]");
expected << (QVariantList() << "http://www.lyricssite.net" << "[lyrics]");
ASSERT_TRUE(parser_.Parse("Alice Aardvark::[lyrics];;Bob Baboon::[lyrics];;http\\://www.lyricssite.net::[lyrics]"));
EXPECT_EQ(expected, parser_.result());
expected.clear();
expected << (QVariantList() << "Amarok" << "Album" << "2982ab29ef");
expected << (QVariantList() << "AmarokUser" << "Compilation" << "My Compilation");
expected << (QVariantList() << "Banshee" << "Compilation" << "ad8slpbzl229zier");
expected << (QVariantList() << "FMPSAlliance" << "Album" << "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3");
ASSERT_TRUE(parser_.Parse("Amarok::Album::2982ab29ef;;AmarokUser::Compilation::My Compilation;;Banshee::Compilation::ad8slpbzl229zier;;FMPSAlliance::Album::de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3"));
EXPECT_EQ(expected, parser_.result());
}

View File

@ -187,4 +187,46 @@ TEST_F(SongTest, DecodesAmbiguousLatin1AndWindows1252) {
EXPECT_EQ(QString::fromUtf8("Sudáfrica"), fixed);
}
TEST_F(SongTest, FMPSRating) {
TemporaryResource r(":/testdata/fmpsrating.mp3");
Song song;
song.InitFromFile(r.fileName(), -1);
EXPECT_FLOAT_EQ(0.42, song.rating());
}
TEST_F(SongTest, FMPSRatingUser) {
TemporaryResource r(":/testdata/fmpsratinguser.mp3");
Song song;
song.InitFromFile(r.fileName(), -1);
EXPECT_FLOAT_EQ(0.10, song.rating());
}
TEST_F(SongTest, FMPSRatingBoth) {
TemporaryResource r(":/testdata/fmpsratingboth.mp3");
Song song;
song.InitFromFile(r.fileName(), -1);
EXPECT_FLOAT_EQ(0.42, song.rating());
}
TEST_F(SongTest, FMPSPlayCount) {
TemporaryResource r(":/testdata/fmpsplaycount.mp3");
Song song;
song.InitFromFile(r.fileName(), -1);
EXPECT_EQ(123, song.playcount());
}
TEST_F(SongTest, FMPSPlayCountUser) {
TemporaryResource r(":/testdata/fmpsplaycountuser.mp3");
Song song;
song.InitFromFile(r.fileName(), -1);
EXPECT_EQ(42, song.playcount());
}
TEST_F(SongTest, FMPSPlayCountBoth) {
TemporaryResource r(":/testdata/fmpsplaycountboth.mp3");
Song song;
song.InitFromFile(r.fileName(), -1);
EXPECT_EQ(123, song.playcount());
}
} // namespace

View File

@ -33,6 +33,16 @@ std::ostream& operator <<(std::ostream& stream, const QVariant& var);
std::ostream& operator <<(std::ostream& stream, const QUrl& url);
std::ostream& operator <<(std::ostream& stream, const QNetworkRequest& req);
template <typename T>
std::ostream& operator <<(std::ostream& stream, const QList<T>& list) {
stream << "QList(";
foreach (const T& item, list) {
stream << item << ",";
}
stream << ")";
return stream;
}
void PrintTo(const ::QString& str, std::ostream& os);
void PrintTo(const ::QVariant& var, std::ostream& os);