2010-09-26 16:21:23 +02:00
|
|
|
/* This file is part of Clementine.
|
2010-11-20 14:27:10 +01:00
|
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
2010-09-26 16:21:23 +02:00
|
|
|
|
|
|
|
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/>.
|
|
|
|
*/
|
|
|
|
|
2010-10-16 16:11:23 +02:00
|
|
|
#include "songinfotextview.h"
|
2013-10-09 19:33:59 +02:00
|
|
|
#include "ultimatelyricslyric.h"
|
2010-10-10 18:09:20 +02:00
|
|
|
#include "ultimatelyricsprovider.h"
|
2011-04-22 18:50:29 +02:00
|
|
|
#include "core/logging.h"
|
2010-10-16 19:20:54 +02:00
|
|
|
#include "core/network.h"
|
2010-09-26 16:21:23 +02:00
|
|
|
|
2013-10-09 19:33:59 +02:00
|
|
|
#include <QCoreApplication>
|
2010-09-26 16:21:23 +02:00
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QTextCodec>
|
2013-10-09 19:33:59 +02:00
|
|
|
#include <QThread>
|
2010-09-26 16:21:23 +02:00
|
|
|
|
|
|
|
#include <boost/scoped_ptr.hpp>
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
const int UltimateLyricsProvider::kRedirectLimit = 5;
|
2010-09-26 16:21:23 +02:00
|
|
|
|
|
|
|
|
2010-10-16 19:20:54 +02:00
|
|
|
UltimateLyricsProvider::UltimateLyricsProvider()
|
|
|
|
: network_(new NetworkAccessManager(this)),
|
2010-10-10 18:46:35 +02:00
|
|
|
relevance_(0),
|
2013-12-03 10:21:52 +01:00
|
|
|
redirect_count_(0),
|
|
|
|
url_hop_(false)
|
2010-09-26 16:21:23 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
void UltimateLyricsProvider::FetchInfo(int id, const Song& metadata) {
|
2010-09-26 16:21:23 +02:00
|
|
|
// Get the text codec
|
|
|
|
const QTextCodec* codec = QTextCodec::codecForName(charset_.toAscii().constData());
|
|
|
|
if (!codec) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Warning) << "Invalid codec" << charset_;
|
2010-10-10 18:09:20 +02:00
|
|
|
emit Finished(id);
|
|
|
|
return;
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fill in fields in the URL
|
|
|
|
QString url_text(url_);
|
2012-02-12 16:17:18 +01:00
|
|
|
ReplaceFields(metadata, &url_text);
|
2010-09-26 16:21:23 +02:00
|
|
|
|
|
|
|
QUrl url(url_text);
|
2011-06-09 18:32:45 +02:00
|
|
|
qLog(Debug) << "Fetching lyrics from" << url;
|
2010-09-26 16:21:23 +02:00
|
|
|
|
|
|
|
// Fetch the URL, follow redirects
|
2012-02-12 16:17:18 +01:00
|
|
|
metadata_ = metadata;
|
2010-10-10 18:09:20 +02:00
|
|
|
redirect_count_ = 0;
|
2010-10-16 19:20:54 +02:00
|
|
|
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
|
|
|
requests_[reply] = id;
|
|
|
|
connect(reply, SIGNAL(finished()), SLOT(LyricsFetched()));
|
2010-10-10 18:09:20 +02:00
|
|
|
}
|
2010-09-26 16:21:23 +02:00
|
|
|
|
2010-10-16 19:20:54 +02:00
|
|
|
void UltimateLyricsProvider::LyricsFetched() {
|
|
|
|
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
|
2013-12-03 10:21:52 +01:00
|
|
|
if (!reply) {
|
|
|
|
url_hop_ = false;
|
2010-10-16 19:20:54 +02:00
|
|
|
return;
|
2013-12-03 10:21:52 +01:00
|
|
|
}
|
2010-10-16 19:20:54 +02:00
|
|
|
|
|
|
|
int id = requests_.take(reply);
|
2010-10-10 18:09:20 +02:00
|
|
|
reply->deleteLater();
|
2010-09-26 16:21:23 +02:00
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
2013-12-03 10:21:52 +01:00
|
|
|
url_hop_ = false;
|
2010-10-10 18:09:20 +02:00
|
|
|
emit Finished(id);
|
|
|
|
return;
|
|
|
|
}
|
2010-09-26 16:21:23 +02:00
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
// Handle redirects
|
|
|
|
QVariant redirect_target = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
|
|
|
if (redirect_target.isValid()) {
|
|
|
|
if (redirect_count_ >= kRedirectLimit) {
|
2013-12-03 10:21:52 +01:00
|
|
|
url_hop_ = false;
|
2010-10-10 18:09:20 +02:00
|
|
|
emit Finished(id);
|
|
|
|
return;
|
|
|
|
}
|
2010-09-26 19:30:17 +02:00
|
|
|
|
|
|
|
QUrl target = redirect_target.toUrl();
|
|
|
|
if (target.scheme().isEmpty() || target.host().isEmpty()) {
|
|
|
|
QString path = target.path();
|
2010-10-10 18:09:20 +02:00
|
|
|
target = reply->url();
|
2010-09-26 19:30:17 +02:00
|
|
|
target.setPath(path);
|
|
|
|
}
|
2010-10-10 18:09:20 +02:00
|
|
|
|
|
|
|
redirect_count_ ++;
|
2010-10-16 19:20:54 +02:00
|
|
|
QNetworkReply* reply = network_->get(QNetworkRequest(target));
|
|
|
|
requests_[reply] = id;
|
|
|
|
connect(reply, SIGNAL(finished()), SLOT(LyricsFetched()));
|
2010-10-10 18:09:20 +02:00
|
|
|
return;
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
const QTextCodec* codec = QTextCodec::codecForName(charset_.toAscii().constData());
|
2010-09-26 16:21:23 +02:00
|
|
|
const QString original_content = codec->toUnicode(reply->readAll());
|
2010-10-10 18:09:20 +02:00
|
|
|
QString lyrics;
|
2010-09-26 16:21:23 +02:00
|
|
|
|
|
|
|
// Check for invalid indicators
|
|
|
|
foreach (const QString& indicator, invalid_indicators_) {
|
2010-10-10 18:09:20 +02:00
|
|
|
if (original_content.contains(indicator)) {
|
2011-06-09 18:32:45 +02:00
|
|
|
qLog(Debug) << "Found invalid indicator" << indicator;
|
2013-12-03 10:21:52 +01:00
|
|
|
url_hop_ = false;
|
2010-10-10 18:09:20 +02:00
|
|
|
emit Finished(id);
|
|
|
|
return;
|
|
|
|
}
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
2013-12-03 10:21:52 +01:00
|
|
|
if (!url_hop_) {
|
|
|
|
// Apply extract rules
|
|
|
|
foreach (const Rule& rule, extract_rules_) {
|
|
|
|
// Modify the rule for this request's metadata
|
|
|
|
Rule rule_copy(rule);
|
|
|
|
for (Rule::iterator it = rule_copy.begin() ; it != rule_copy.end() ; ++it) {
|
|
|
|
ReplaceFields(metadata_, &it->first);
|
|
|
|
}
|
|
|
|
|
|
|
|
QString content = original_content;
|
|
|
|
if (ApplyExtractRule(rule_copy, &content)) {
|
|
|
|
url_hop_ = true;
|
|
|
|
QUrl url(content);
|
|
|
|
qLog(Debug) << "Next url hop: " << url;
|
|
|
|
QNetworkReply* reply = network_->get(QNetworkRequest(url));
|
|
|
|
requests_[reply] = id;
|
|
|
|
connect(reply, SIGNAL(finished()), SLOT(LyricsFetched()));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply exclude rules
|
|
|
|
foreach (const Rule& rule, exclude_rules_) {
|
2014-01-18 18:22:00 +01:00
|
|
|
ApplyExcludeRule(rule, &content);
|
2013-12-03 10:21:52 +01:00
|
|
|
}
|
|
|
|
|
2014-01-02 23:52:35 +01:00
|
|
|
if (!content.isEmpty() and HTMLHasAlphaNumeric(content)) {
|
2013-12-03 10:21:52 +01:00
|
|
|
lyrics = content;
|
|
|
|
break;
|
|
|
|
}
|
2012-02-12 16:17:18 +01:00
|
|
|
}
|
2013-12-03 10:21:52 +01:00
|
|
|
} else {
|
|
|
|
lyrics = original_content;
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
2014-01-02 23:52:35 +01:00
|
|
|
if (!lyrics.isEmpty() and HTMLHasAlphaNumeric(lyrics)) {
|
2010-10-10 18:09:20 +02:00
|
|
|
CollapsibleInfoPane::Data data;
|
2010-10-11 21:49:12 +02:00
|
|
|
data.id_ = "ultimatelyrics/" + name_;
|
2010-10-10 18:09:20 +02:00
|
|
|
data.title_ = tr("Lyrics from %1").arg(name_);
|
|
|
|
data.type_ = CollapsibleInfoPane::Data::Type_Lyrics;
|
2010-10-16 14:56:58 +02:00
|
|
|
data.relevance_ = relevance();
|
2010-10-10 18:09:20 +02:00
|
|
|
|
2013-10-09 19:33:59 +02:00
|
|
|
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
|
|
|
|
SongInfoTextView* editor = new SongInfoTextView;
|
|
|
|
editor->SetHtml(lyrics);
|
|
|
|
data.contents_ = editor;
|
|
|
|
} else {
|
|
|
|
UltimateLyricsLyric* editor = new UltimateLyricsLyric;
|
|
|
|
editor->SetHtml(lyrics);
|
|
|
|
data.content_object_ = editor;
|
|
|
|
}
|
2010-10-10 18:09:20 +02:00
|
|
|
|
|
|
|
emit InfoReady(id, data);
|
|
|
|
}
|
2013-12-03 10:21:52 +01:00
|
|
|
url_hop_ = false;
|
2010-10-10 18:09:20 +02:00
|
|
|
emit Finished(id);
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
2013-12-03 10:21:52 +01:00
|
|
|
bool UltimateLyricsProvider::ApplyExtractRule(const Rule& rule, QString* content) const {
|
2010-09-26 16:21:23 +02:00
|
|
|
foreach (const RuleItem& item, rule) {
|
|
|
|
if (item.second.isNull()) {
|
2013-12-03 10:21:52 +01:00
|
|
|
if (item.first.startsWith("http://") && item.second.isNull()) {
|
|
|
|
*content = ExtractUrl(*content, rule);
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
*content = ExtractXmlTag(*content, item.first);
|
|
|
|
}
|
2010-09-26 16:21:23 +02:00
|
|
|
} else {
|
|
|
|
*content = Extract(*content, item.first, item.second);
|
|
|
|
}
|
|
|
|
}
|
2013-12-03 10:21:52 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString UltimateLyricsProvider::ExtractUrl(const QString& source, const Rule& rule) {
|
|
|
|
QString url;
|
|
|
|
QString id;
|
|
|
|
|
|
|
|
foreach(const RuleItem& item, rule) {
|
|
|
|
if (item.first.startsWith("http://") && item.second.isNull())
|
|
|
|
url = item.first;
|
|
|
|
else
|
|
|
|
id = Extract(source, item.first,item.second);
|
|
|
|
}
|
|
|
|
|
|
|
|
url.replace("{id}", id);
|
|
|
|
|
|
|
|
return url;
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
QString UltimateLyricsProvider::ExtractXmlTag(const QString& source, const QString& tag) {
|
2010-09-26 19:30:17 +02:00
|
|
|
QRegExp re("<(\\w+).*>"); // ಠ_ಠ
|
2010-09-26 16:21:23 +02:00
|
|
|
if (re.indexIn(tag) == -1)
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
return Extract(source, tag, "</" + re.cap(1) + ">");
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
QString UltimateLyricsProvider::Extract(const QString& source, const QString& begin, const QString& end) {
|
2010-09-26 16:21:23 +02:00
|
|
|
int begin_idx = source.indexOf(begin);
|
|
|
|
if (begin_idx == -1)
|
|
|
|
return QString();
|
|
|
|
begin_idx += begin.length();
|
|
|
|
|
|
|
|
int end_idx = source.indexOf(end, begin_idx);
|
|
|
|
if (end_idx == -1)
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
return source.mid(begin_idx, end_idx - begin_idx - 1);
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
void UltimateLyricsProvider::ApplyExcludeRule(const Rule& rule, QString* content) const {
|
2010-09-26 16:21:23 +02:00
|
|
|
foreach (const RuleItem& item, rule) {
|
|
|
|
if (item.second.isNull()) {
|
|
|
|
*content = ExcludeXmlTag(*content, item.first);
|
|
|
|
} else {
|
|
|
|
*content = Exclude(*content, item.first, item.second);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
QString UltimateLyricsProvider::ExcludeXmlTag(const QString& source, const QString& tag) {
|
2010-09-26 19:30:17 +02:00
|
|
|
QRegExp re("<(\\w+).*>"); // ಠ_ಠ
|
2010-09-26 16:21:23 +02:00
|
|
|
if (re.indexIn(tag) == -1)
|
|
|
|
return source;
|
|
|
|
|
|
|
|
return Exclude(source, tag, "</" + re.cap(1) + ">");
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
QString UltimateLyricsProvider::Exclude(const QString& source, const QString& begin, const QString& end) {
|
2010-09-26 16:21:23 +02:00
|
|
|
int begin_idx = source.indexOf(begin);
|
|
|
|
if (begin_idx == -1)
|
|
|
|
return source;
|
|
|
|
|
|
|
|
int end_idx = source.indexOf(end, begin_idx + begin.length());
|
|
|
|
if (end_idx == -1)
|
|
|
|
return source;
|
|
|
|
|
|
|
|
return source.left(begin_idx) + source.right(source.length() - end_idx - end.length());
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
QString UltimateLyricsProvider::FirstChar(const QString& text) {
|
2010-09-26 16:21:23 +02:00
|
|
|
if (text.isEmpty())
|
|
|
|
return QString();
|
|
|
|
return text[0].toLower();
|
|
|
|
}
|
|
|
|
|
2010-10-10 18:09:20 +02:00
|
|
|
QString UltimateLyricsProvider::TitleCase(const QString& text) {
|
2010-09-26 16:21:23 +02:00
|
|
|
if (text.length() == 0)
|
|
|
|
return QString();
|
2012-02-12 16:17:18 +01:00
|
|
|
|
|
|
|
QString ret = text;
|
|
|
|
bool last_was_space = true;
|
|
|
|
|
|
|
|
for (QString::iterator it = ret.begin() ; it != ret.end() ; ++it) {
|
|
|
|
if (last_was_space) {
|
|
|
|
*it = it->toUpper();
|
|
|
|
last_was_space = false;
|
|
|
|
} else if (it->isSpace()) {
|
|
|
|
last_was_space = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
|
|
|
|
2012-02-12 16:17:18 +01:00
|
|
|
void UltimateLyricsProvider::ReplaceField(const QString& tag, const QString& value,
|
|
|
|
QString* text) const {
|
|
|
|
if (!text->contains(tag))
|
2010-09-26 16:21:23 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
// Apply URL character replacement
|
|
|
|
QString value_copy(value);
|
|
|
|
foreach (const UrlFormat& format, url_formats_) {
|
|
|
|
QRegExp re("[" + QRegExp::escape(format.first) + "]");
|
|
|
|
value_copy.replace(re, format.second);
|
|
|
|
}
|
|
|
|
|
2012-02-12 16:17:18 +01:00
|
|
|
text->replace(tag, value_copy, Qt::CaseInsensitive);
|
|
|
|
}
|
|
|
|
|
|
|
|
void UltimateLyricsProvider::ReplaceFields(const Song& metadata, QString* text) const {
|
|
|
|
ReplaceField("{artist}", metadata.artist().toLower(), text);
|
|
|
|
ReplaceField("{artist2}",NoSpace(metadata.artist().toLower()), text);
|
|
|
|
ReplaceField("{album}", metadata.album().toLower(), text);
|
|
|
|
ReplaceField("{album2}", NoSpace(metadata.album().toLower()), text);
|
|
|
|
ReplaceField("{title}", metadata.title().toLower(), text);
|
|
|
|
ReplaceField("{Artist}", metadata.artist(), text);
|
|
|
|
ReplaceField("{Album}", metadata.album(), text);
|
|
|
|
ReplaceField("{ARTIST}", metadata.artist().toUpper(), text);
|
|
|
|
ReplaceField("{year}", metadata.PrettyYear(), text);
|
|
|
|
ReplaceField("{Title}", metadata.title(), text);
|
|
|
|
ReplaceField("{Title2}", TitleCase(metadata.title()), text);
|
|
|
|
ReplaceField("{a}", FirstChar(metadata.artist()), text);
|
|
|
|
ReplaceField("{track}", QString::number(metadata.track()), text);
|
|
|
|
}
|
|
|
|
|
|
|
|
QString UltimateLyricsProvider::NoSpace(const QString& text) {
|
|
|
|
QString ret(text);
|
|
|
|
ret.remove(' ');
|
|
|
|
return ret;
|
2010-09-26 16:21:23 +02:00
|
|
|
}
|
2014-01-02 23:52:35 +01:00
|
|
|
|
|
|
|
// tells whether a html block has alphanumeric characters (skipping tags)
|
|
|
|
// TODO: handle special characters (e.g. ® á)
|
|
|
|
bool UltimateLyricsProvider::HTMLHasAlphaNumeric(const QString& html) {
|
2014-01-03 08:45:22 +01:00
|
|
|
bool in_tag = false;
|
|
|
|
foreach (const QChar& c, html) {
|
|
|
|
if (!in_tag and c.isLetterOrNumber())
|
|
|
|
return true;
|
|
|
|
else if (c == QChar('<'))
|
|
|
|
in_tag = true;
|
|
|
|
else if (c == QChar('>'))
|
|
|
|
in_tag = false;
|
|
|
|
}
|
|
|
|
qLog(Debug) << html;
|
|
|
|
return false;
|
2014-01-02 23:52:35 +01:00
|
|
|
}
|