/* This file is part of Clementine. Copyright 2010, David Sansome 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 . */ #include "ultimatelyricsprovider.h" #include #include #include #include #include "core/logging.h" #include "core/network.h" #include "songinfotextview.h" #include "ultimatelyricslyric.h" const int UltimateLyricsProvider::kRedirectLimit = 5; UltimateLyricsProvider::UltimateLyricsProvider() : network_(new NetworkAccessManager(this)), timeouts_(new NetworkTimeouts(30000, this)), // 30s relevance_(0), redirect_count_(0), url_hop_(false) {} void UltimateLyricsProvider::FetchInfo(int id, const Song& metadata) { // Get the text codec const QTextCodec* codec = QTextCodec::codecForName(charset_.toLatin1().constData()); if (!codec) { qLog(Warning) << "Invalid codec" << charset_; emit Finished(id); return; } // Fill in fields in the URL QString url_text(url_); ReplaceFields(metadata, &url_text); QUrl url(url_text); qLog(Debug) << "Fetching lyrics from" << url_text; // Fetch the URL, follow redirects metadata_ = metadata; redirect_count_ = 0; QNetworkReply* reply = network_->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, [=] { this->RequestFinished(reply, url_text, id); }); timeouts_->AddReply(reply); } void UltimateLyricsProvider::RequestFinished(QNetworkReply* reply, const QString& orig_url, int id) { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qLog(Debug) << "Reply error" << reply->errorString(); url_hop_ = false; emit Finished(id); return; } // Handle redirects QVariant redirect_target = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); if (redirect_target.isValid()) { if (redirect_count_ >= kRedirectLimit) { qLog(Debug) << "Too many redirects from" << orig_url << "to" << reply->url().toString(); url_hop_ = false; emit Finished(id); return; } QUrl target = redirect_target.toUrl(); if (target.scheme().isEmpty() || target.host().isEmpty()) { QString path = target.path(); target = reply->url(); target.setPath(path); } redirect_count_++; QNetworkReply* reply = network_->get(QNetworkRequest(target)); connect(reply, &QNetworkReply::finished, [=] { this->RequestFinished(reply, orig_url, id); }); timeouts_->AddReply(reply); return; } const QTextCodec* codec = QTextCodec::codecForName(charset_.toLatin1().constData()); const QString original_content = codec->toUnicode(reply->readAll()); QString lyrics; // Check for invalid indicators for (const QString& indicator : invalid_indicators_) { if (original_content.contains(indicator)) { qLog(Debug) << "Found invalid indicator" << indicator; url_hop_ = false; emit Finished(id); return; } } if (!url_hop_) { // Apply extract rules for (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)); connect(reply, &QNetworkReply::finished, [=] { this->RequestFinished(reply, orig_url, id); }); timeouts_->AddReply(reply); return; } // Apply exclude rules for (const Rule& rule : exclude_rules_) { ApplyExcludeRule(rule, &content); } if (!content.isEmpty() and HTMLHasAlphaNumeric(content)) { lyrics = content; break; } } } else { lyrics = original_content; } if (!lyrics.isEmpty() and HTMLHasAlphaNumeric(lyrics)) { CollapsibleInfoPane::Data data; data.id_ = "ultimatelyrics/" + name_; data.title_ = tr("Lyrics from %1").arg(name_); data.type_ = CollapsibleInfoPane::Data::Type_Lyrics; data.relevance_ = relevance(); 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; } emit InfoReady(id, data); } url_hop_ = false; emit Finished(id); } bool UltimateLyricsProvider::ApplyExtractRule(const Rule& rule, QString* content) const { for (const RuleItem& item : rule) { if (item.second.isNull()) { if (item.first.startsWith("http://") && item.second.isNull()) { *content = ExtractUrl(*content, rule); return true; } else { *content = ExtractXmlTag(*content, item.first); } } else { *content = Extract(*content, item.first, item.second); } } return false; } QString UltimateLyricsProvider::ExtractUrl(const QString& source, const Rule& rule) { QString url; QString id; for (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; } QString UltimateLyricsProvider::ExtractXmlTag(const QString& source, const QString& tag) { QRegExp re("<(\\w+).*>"); // ಠ_ಠ if (re.indexIn(tag) == -1) return QString(); return Extract(source, tag, ""); } QString UltimateLyricsProvider::Extract(const QString& source, const QString& begin, const QString& end) { 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); } void UltimateLyricsProvider::ApplyExcludeRule(const Rule& rule, QString* content) const { for (const RuleItem& item : rule) { if (item.second.isNull()) { *content = ExcludeXmlTag(*content, item.first); } else { *content = Exclude(*content, item.first, item.second); } } } QString UltimateLyricsProvider::ExcludeXmlTag(const QString& source, const QString& tag) { QRegExp re("<(\\w+).*>"); // ಠ_ಠ if (re.indexIn(tag) == -1) return source; return Exclude(source, tag, ""); } QString UltimateLyricsProvider::Exclude(const QString& source, const QString& begin, const QString& end) { 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()); } QString UltimateLyricsProvider::FirstChar(const QString& text) { if (text.isEmpty()) return QString(); return text[0].toLower(); } QString UltimateLyricsProvider::TitleCase(const QString& text) { if (text.length() == 0) return QString(); 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; } void UltimateLyricsProvider::ReplaceField(const QString& tag, const QString& value, QString* text) const { if (!text->contains(tag)) return; // Apply URL character replacement QString value_copy(value); for (const UrlFormat& format : url_formats_) { QRegExp re("[" + QRegExp::escape(format.first) + "]"); value_copy.replace(re, format.second); } 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; } // tells whether a html block has alphanumeric characters (skipping tags) // TODO: handle special characters (e.g. ® á) bool UltimateLyricsProvider::HTMLHasAlphaNumeric(const QString& html) { bool in_tag = false; for (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; }