Allow retrieving lyrics from LyricsWikia

This commit is contained in:
Martchus 2016-10-09 22:41:34 +02:00
parent 958cd9960c
commit 0062384bad
14 changed files with 800 additions and 356 deletions

View File

@ -265,7 +265,8 @@ KnownFieldModel &dbQueryFields()
<< KnownFieldModel::mkItem(KnownField::Album)
<< KnownFieldModel::mkItem(KnownField::Year)
<< KnownFieldModel::mkItem(KnownField::Genre)
<< KnownFieldModel::mkItem(KnownField::Cover, Qt::Unchecked));
<< KnownFieldModel::mkItem(KnownField::Cover, Qt::Unchecked)
<< KnownFieldModel::mkItem(KnownField::Lyrics, Qt::Unchecked));
return v;
}

View File

@ -78,6 +78,8 @@ TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
return tagValue;
}
break;
case KnownField::Lyrics:
returnValue(lyrics);
default:
;
}
@ -195,18 +197,51 @@ const QByteArray *QueryResultsModel::cover(const QModelIndex &index) const
/*!
* \brief Fetches the cover the specified \a index.
* \returns True if the cover is immidiately available; false is the cover is fetched asynchronously.
* \returns
* - true if the cover is immidiately available or an error occurs immidiately
* - and false if the cover will be fetched asynchronously.
*
* If the cover is fetched asynchronously the coverAvailable() signal is emitted, when the cover
* is available.
* becomes available.
*
* The resultsAvailable() signal is emitted if errors have been added to errorList().
*/
bool QueryResultsModel::fetchCover(const QModelIndex &)
bool QueryResultsModel::fetchCover(const QModelIndex &index)
{
m_errorList << tr("Fetching the cover is not implemented for the selected provider.");
Q_UNUSED(index)
m_errorList << tr("Fetching cover is not implemented for this provider");
emit resultsAvailable();
return false;
return true;
}
const QString *QueryResultsModel::lyrics(const QModelIndex &index) const
{
if(!index.parent().isValid() && index.row() < m_results.size()) {
const QString &lyrics = m_results.at(index.row()).lyrics;
if(!lyrics.isEmpty()) {
return &lyrics;
}
}
return nullptr;
}
/*!
* \brief Fetches the lyrics the specified \a index.
* \returns
* - true if the lyrics are immidiately available or an error occurs immidiately
* - and false if the lyrics will be fetched asynchronously.
*
* If the lyrics are fetched asynchronously the lyricsAvailable() signal is emitted, when the lyrics
* become available.
*
* The resultsAvailable() signal is emitted if errors have been added to errorList().
*/
bool QueryResultsModel::fetchLyrics(const QModelIndex &index)
{
Q_UNUSED(index)
m_errorList << tr("Fetching lyrics is not implemented for this provider");
emit resultsAvailable();
return true;
}
/*!
@ -216,7 +251,7 @@ bool QueryResultsModel::fetchCover(const QModelIndex &)
HttpResultsModel::HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) :
m_initialDescription(initialSongDescription)
{
addReply(reply);
addReply(reply, this, &HttpResultsModel::handleInitialReplyFinished);
}
/*!
@ -227,77 +262,61 @@ HttpResultsModel::~HttpResultsModel()
qDeleteAll(m_replies);
}
/*!
* \brief Helper to copy a property from one QObject to another.
* \remarks Used to transfer reply properties to new reply in case a second reply is required.
*/
void copyProperty(const char *property, const QObject *from, QObject *to)
{
to->setProperty(property, from->property(property));
}
/*!
* \brief Evaluates request results.
* \remarks Calls parseResults() if the requested data is available. Handles errors/redirections otherwise.
*/
void HttpResultsModel::handleReplyFinished()
void HttpResultsModel::handleInitialReplyFinished()
{
auto *reply = static_cast<QNetworkReply *>(sender());
QByteArray data;
if(auto *newReply = evaluateReplyResults(reply, data, false)) {
addReply(newReply, this, &HttpResultsModel::handleInitialReplyFinished);
} else {
if(!data.isEmpty()) {
parseInitialResults(data);
}
setResultsAvailable(true); // update status, emit resultsAvailable()
}
}
QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection)
{
// delete reply (later)
reply->deleteLater();
m_replies.removeAll(reply);
if(reply->error() == QNetworkReply::NoError) {
QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if(!redirectionTarget.isNull()) {
// there's a redirection available
// -> resolve new URL
const QUrl newUrl = reply->url().resolved(redirectionTarget.toUrl());
// -> always follow when retrieving cover art, otherwise ask user
bool follow = reply->property("coverArt").toBool();
if(!follow) {
// -> ask user whether to follow redirection unless alwaysFollowRedirection is true
if(!alwaysFollowRedirection) {
const QString message = tr("<p>Do you want to redirect form <i>%1</i> to <i>%2</i>?</p>").arg(
reply->url().toString(), newUrl.toString());
follow = QMessageBox::question(nullptr, tr("Search"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
alwaysFollowRedirection = QMessageBox::question(nullptr, tr("Search"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
}
if(follow) {
QNetworkReply *newReply = networkAccessManager().get(QNetworkRequest(newUrl));
// retain possible assigned dynamic properties (TODO: implement it in a better way)
copyProperty("coverArt", reply, newReply);
copyProperty("albumId", reply, newReply);
copyProperty("row", reply, newReply);
addReply(newReply);
return;
if(alwaysFollowRedirection) {
return networkAccessManager().get(QNetworkRequest(newUrl));
} else {
m_errorList << tr("Redirection to: ") + newUrl.toString();
return nullptr;
}
} else {
QByteArray data = reply->readAll();
if((data = reply->readAll()).isEmpty()) {
m_errorList << tr("Server replied no data.");
}
#ifdef DEBUG_BUILD
std::cerr << "Results from HTTP query:" << std::endl;
std::cerr << data.data() << std::endl;
cerr << "Results from HTTP query:" << endl;
cerr << data.data() << endl;
#endif
parseResults(reply, data);
}
} else {
m_errorList << reply->errorString();
}
// delete reply
reply->deleteLater();
m_replies.removeAll(reply);
// update status, emit resultsAvailable()
setResultsAvailable(true);
}
/*!
* \brief Adds a reply.
* \remarks Called within c'tor and handleReplyFinished() in case of redirection. Might be called when subclassing to do further requests.
*/
void HttpResultsModel::addReply(QNetworkReply *reply)
{
m_replies << reply;
#ifdef DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toLocal8Bit().data() << std::endl;
#endif
connect(reply, &QNetworkReply::finished, this, &HttpResultsModel::handleReplyFinished);
return nullptr;
}
/*!

View File

@ -5,6 +5,11 @@
#include <QAbstractTableModel>
#ifdef DEBUG_BUILD
# include <QNetworkReply>
# include <iostream>
#endif
QT_FORWARD_DECLARE_CLASS(QNetworkReply)
namespace Media {
@ -28,6 +33,7 @@ struct SongDescription
unsigned int totalTracks;
unsigned int disk;
QByteArray cover;
QString lyrics;
};
class QueryResultsModel : public QAbstractTableModel
@ -58,11 +64,14 @@ public:
int columnCount(const QModelIndex &parent) const;
const QByteArray *cover(const QModelIndex &index) const;
virtual bool fetchCover(const QModelIndex &index);
const QString *lyrics(const QModelIndex &index) const;
virtual bool fetchLyrics(const QModelIndex &index);
virtual void abort();
signals:
void resultsAvailable();
void coverAvailable(const QModelIndex &index);
void lyricsAvailable(const QModelIndex &index);
protected:
QueryResultsModel(QObject *parent = nullptr);
@ -104,17 +113,41 @@ public:
protected:
HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
void addReply(QNetworkReply *reply);
virtual void parseResults(QNetworkReply *reply, const QByteArray &data) = 0;
template<class Object, class Function> void addReply(QNetworkReply *reply, Object object, Function handler);
template<class Function> void addReply(QNetworkReply *reply, Function handler);
virtual void parseInitialResults(const QByteArray &data) = 0;
QNetworkReply *evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection = false);
private slots:
void handleReplyFinished();
void handleInitialReplyFinished();
protected:
QList<QNetworkReply *> m_replies;
const SongDescription m_initialDescription;
};
template<class Object, class Function>
inline void HttpResultsModel::addReply(QNetworkReply *reply, Object object, Function handler)
{
(m_replies << reply), connect(reply, &QNetworkReply::finished, object, handler);
#ifdef DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toLocal8Bit().data() << std::endl;
#endif
}
/*!
* \brief Adds a reply.
* \remarks Called within c'tor and handleReplyFinished() in case of redirection. Might be called when subclassing to do further requests.
*/
template<class Function>
inline void HttpResultsModel::addReply(QNetworkReply *reply, Function handler)
{
(m_replies << reply), connect(reply, &QNetworkReply::finished, handler);
#ifdef DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toLocal8Bit().data() << std::endl;
#endif
}
QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription);
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription);
QNetworkReply *queryCoverArtArchive(const QString &albumId);

View File

@ -6,27 +6,49 @@
#include <QUrlQuery>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QXmlStreamReader>
#include <QTextDocument>
#include <functional>
using namespace std;
using namespace std::placeholders;
using namespace Utility;
namespace QtGui {
static const QString defaultLyricsWikiaUrl(QStringLiteral("https://lyrics.wikia.com"));
LyricsWikiaResultsModel::LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply) :
HttpResultsModel(move(initialSongDescription), reply)
{}
void LyricsWikiaResultsModel::parseResults(QNetworkReply *reply, const QByteArray &data)
bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index)
{
Q_UNUSED(reply)
if(!index.parent().isValid() && index.row() < m_results.size()) {
SongDescription &desc = m_results[index.row()];
if(!desc.lyrics.isEmpty()) {
// lyrics already available -> nothing to do
} else if(!desc.artist.isEmpty() && !desc.title.isEmpty()) {
auto *reply = requestSongDetails(desc);
addReply(reply, bind(&LyricsWikiaResultsModel::handleSongDetailsFinished, this, reply, index.row()));
return false;
} else {
m_errorList << tr("Unable to fetch lyrics: Artist or title is unknown.");
emit resultsAvailable();
}
}
return true;
}
void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing MusicBrainz meta data
beginResetModel();
m_results.clear();
m_reader.addData(data);
QXmlStreamReader xmlReader(data);
// parse XML tree
#define xmlReader m_reader
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("getArtistResponse") {
@ -53,18 +75,18 @@ void LyricsWikiaResultsModel::parseResults(QNetworkReply *reply, const QByteArra
} else_skip
}
} else_skip
// need to filter results manually because the filtering provided by Lyrica Wiki API doesn't work
if((m_initialDescription.album.isEmpty() || m_initialDescription.album == album)
&& (m_initialDescription.year.isEmpty() || m_initialDescription.year == year)
&& (!m_initialDescription.totalTracks || m_initialDescription.totalTracks == static_cast<unsigned int>(songs.size()))) {
for(SongDescription &song : songs) {
if((m_initialDescription.title.isEmpty() || m_initialDescription.title == song.title)
&& (!m_initialDescription.track || m_initialDescription.track == static_cast<unsigned int>(songs.size()))) {
song.album = album;
song.year = year;
song.totalTracks = static_cast<unsigned int>(songs.size());
m_results << move(song);
}
}
// need to filter results manually because the filtering provided by Lyrica Wiki API doesn't work
if((m_initialDescription.album.isEmpty() || m_initialDescription.album == album)
&& (m_initialDescription.year.isEmpty() || m_initialDescription.year == year)
&& (!m_initialDescription.totalTracks || m_initialDescription.totalTracks == static_cast<unsigned int>(songs.size()))) {
for(SongDescription &song : songs) {
if((m_initialDescription.title.isEmpty() || m_initialDescription.title == song.title)
&& (!m_initialDescription.track || m_initialDescription.track == song.track)) {
song.album = album;
song.year = year;
song.totalTracks = static_cast<unsigned int>(songs.size());
m_results << move(song);
}
}
}
@ -80,24 +102,155 @@ void LyricsWikiaResultsModel::parseResults(QNetworkReply *reply, const QByteArra
#include <qtutilities/misc/undefxmlparsermacros.h>
// check for parsing errors
switch(m_reader.error()) {
switch(xmlReader.error()) {
case QXmlStreamReader::NoError:
case QXmlStreamReader::PrematureEndOfDocumentError:
break;
default:
m_errorList << m_reader.errorString();
m_errorList << xmlReader.errorString();
}
// promote changes
endResetModel();
}
QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription &songDescription)
{
// compose URL
QUrl url((Settings::lyricsWikiaUrl().isEmpty() ? defaultLyricsWikiaUrl : Settings::lyricsWikiaUrl()) + QStringLiteral("/api.php"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("func"), QStringLiteral("getSong"));
query.addQueryItem(QStringLiteral("action"), QStringLiteral("lyrics"));
query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
query.addQueryItem(QStringLiteral("fixXML"), QString());
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
query.addQueryItem(QStringLiteral("title"), songDescription.title);
if(!songDescription.album.isEmpty()) {
// specifying album seems to have no effect but also don't hurt
query.addQueryItem(QStringLiteral("album"), songDescription.album);
}
url.setQuery(query);
return Utility::networkAccessManager().get(QNetworkRequest(url));
}
void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, int row)
{
QByteArray data;
if(auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleSongDetailsFinished, this, newReply, row));
} else if(!data.isEmpty()) {
parseSongDetails(row, data);
}
}
void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
{
// find associated result/desc
if(row >= m_results.size()) {
m_errorList << tr("Internal error: context for song details reply invalid");
setResultsAvailable(true);
return;
}
const SongDescription &assocDesc = m_results.at(row);
QUrl parsedUrl;
// parse XML tree
QXmlStreamReader xmlReader(data);
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("LyricsResult") {
SongDescription parsedDesc;
children {
iftag("artist") {
parsedDesc.artist = text;
} eliftag("song") {
parsedDesc.title = text;
} eliftag("url") {
parsedUrl = text;
} else_skip
}
// verify whether parsed results match what was requested
if(!parsedUrl.isEmpty() && assocDesc.title == parsedDesc.title && assocDesc.artist == parsedDesc.artist) {
break;
} else {
parsedUrl.clear();
}
} else_skip
}
#include <qtutilities/misc/undefxmlparsermacros.h>
// check for parsing errors
switch(xmlReader.error()) {
case QXmlStreamReader::NoError:
case QXmlStreamReader::PrematureEndOfDocumentError:
break;
default:
m_errorList << tr("Unable to parse song details: ") + xmlReader.errorString();
setResultsAvailable(true);
if(parsedUrl.isEmpty()) {
return; // don't complain about missing URL when the XML isn't even valid
}
}
// requets lyrics (seem to be incomplete in XML response, so just get the regular Wiki page)
if(parsedUrl.isEmpty()) {
m_errorList << tr("Song details requested for %1/%2 do not contain URL for Wiki page").arg(assocDesc.artist, assocDesc.title);
setResultsAvailable(true);
return;
}
// do not use parsed URL directly to avoid unintended requests
QUrl requestUrl(Settings::lyricsWikiaUrl().isEmpty() ? defaultLyricsWikiaUrl : Settings::lyricsWikiaUrl());
requestUrl.setPath(parsedUrl.path());
auto *reply = Utility::networkAccessManager().get(QNetworkRequest(parsedUrl));
addReply(reply, bind(&LyricsWikiaResultsModel::handleLyricsReplyFinished, this, reply, row));
}
void LyricsWikiaResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, int row)
{
QByteArray data;
if(auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleLyricsReplyFinished, this, newReply, row));
} else {
if(!data.isEmpty()) {
parseLyricsResults(row, data);
}
setResultsAvailable(true);
}
}
void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data)
{
// find associated result/desc
if(row >= m_results.size()) {
m_errorList << tr("Internal error: context for LyricsWikia page reply invalid");
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
// parse lyrics from HTML
QString html(data);
int lyricsStart = html.indexOf("<div class='lyricbox'>");
if(!lyricsStart) {
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
setResultsAvailable(true);
return;
}
int lyricsEnd = html.indexOf("<div class='lyricsbreak'></div>", lyricsStart);
QTextDocument textDoc;
textDoc.setHtml(html.mid(lyricsStart, (lyricsEnd > lyricsStart) ? (lyricsEnd - lyricsStart) : -1));
assocDesc.lyrics = textDoc.toPlainText();
emit lyricsAvailable(index(row, 0));
}
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription)
{
static QString defaultUrl(QStringLiteral("https://lyrics.wikia.com/api.php"));
// compose URL
QUrl url(Settings::lyricsWikiaUrl().isEmpty() ? defaultUrl : Settings::lyricsWikiaUrl());
QUrl url((Settings::lyricsWikiaUrl().isEmpty() ? defaultLyricsWikiaUrl : Settings::lyricsWikiaUrl()) + QStringLiteral("/api.php"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("func"), QStringLiteral("getArtist"));
query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));

View File

@ -3,7 +3,7 @@
#include "./dbquery.h"
#include <QXmlStreamReader>
#include <map>
namespace QtGui {
@ -13,12 +13,17 @@ class LyricsWikiaResultsModel : public HttpResultsModel
public:
LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchLyrics(const QModelIndex &index);
protected:
void parseResults(QNetworkReply *reply, const QByteArray &data);
void parseInitialResults(const QByteArray &data);
private:
QXmlStreamReader m_reader;
QNetworkReply *requestSongDetails(const SongDescription &songDescription);
void handleSongDetailsFinished(QNetworkReply *reply, int row);
void parseSongDetails(int row, const QByteArray &data);
void handleLyricsReplyFinished(QNetworkReply *reply, int row);
void parseLyricsResults(int row, const QByteArray &data);
};

View File

@ -8,8 +8,12 @@
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrlQuery>
#include <QXmlStreamReader>
#include <functional>
using namespace std;
using namespace std::placeholders;
using namespace Utility;
namespace QtGui {
@ -32,147 +36,158 @@ bool MusicBrainzResultsModel::fetchCover(const QModelIndex &index)
desc.cover = m_coverData.at(desc.albumId);
} catch(const out_of_range &) {
// request the cover art
QNetworkReply *reply = queryCoverArtArchive(desc.albumId);
addReply(reply);
reply->setProperty("coverArt", true);
reply->setProperty("albumId", desc.albumId);
reply->setProperty("row", index.row());
auto *reply = queryCoverArtArchive(desc.albumId);
addReply(reply, bind(&MusicBrainzResultsModel::handleCoverReplyFinished, this, reply, desc.albumId, index.row()));
setFetchingCover(true);
return false;
}
} else {
m_errorList << tr("Unable to fetch cover: Album ID is unknown.");
m_errorList << tr("Unable to fetch cover: Album ID unknown");
emit resultsAvailable();
}
}
return true;
}
void MusicBrainzResultsModel::parseResults(QNetworkReply *reply, const QByteArray &data)
void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
{
if(reply->property("coverArt").toBool()) {
// add cover -> determine album ID and row
bool ok;
QString albumId = reply->property("albumId").toString();
int row = reply->property("row").toInt(&ok);
if(!albumId.isEmpty() && ok && row < m_results.size()) {
// prepare parsing MusicBrainz meta data
beginResetModel();
m_results.clear();
QXmlStreamReader xmlReader(data);
// parse XML tree
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("metadata") {
children {
iftag("recording-list") {
children {
iftag("recording") {
SongDescription currentDescription;
children {
iftag("title") {
currentDescription.title = text;
} eliftag("artist-credit") {
children {
iftag("name-credit") {
children {
iftag("artist") {
children {
iftag("name") {
currentDescription.artist = text;
} else_skip
}
} else_skip
}
} else_skip
}
} eliftag("release-list") {
children {
iftag("release") {
if(currentDescription.albumId.isEmpty()) {
currentDescription.albumId = attribute("id").toString();
}
children {
iftag("title") {
currentDescription.album = text;
} eliftag("date") {
currentDescription.year = text;
} eliftag("medium-list") {
children {
iftag("medium") {
children {
iftag("position") {
currentDescription.disk = text.toUInt();
} eliftag("track-list") {
currentDescription.totalTracks = attribute("count").toUInt();
children {
iftag("track") {
children {
iftag("number") {
currentDescription.track = text.toUInt();
} else_skip
}
} else_skip
}
} else_skip
}
} else_skip
}
} else_skip
}
} else_skip
}
} eliftag("tag-list") {
children {
iftag("tag") {
children {
iftag("name") {
if(!currentDescription.genre.isEmpty()) {
currentDescription.genre.append(QLatin1Char(' '));
}
currentDescription.genre.append(text);
} else_skip
}
} else_skip
}
} else_skip
}
m_results << currentDescription;
} else_skip
}
} else_skip
}
} else_skip
}
#include <qtutilities/misc/undefxmlparsermacros.h>
// check for parsing errors
switch(xmlReader.error()) {
case QXmlStreamReader::NoError:
case QXmlStreamReader::PrematureEndOfDocumentError:
break;
default:
m_errorList << xmlReader.errorString();
}
// promote changes
endResetModel();
}
void MusicBrainzResultsModel::handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row)
{
QByteArray data;
if(auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&MusicBrainzResultsModel::handleCoverReplyFinished, this, newReply, albumId, row));
} else {
if(!data.isEmpty()) {
parseCoverResults(albumId, row, data);
}
setResultsAvailable(true);
}
}
void MusicBrainzResultsModel::parseCoverResults(const QString &albumId, int row, const QByteArray &data)
{
// add cover -> determine album ID and row
bool ok;
if(!albumId.isEmpty() && ok && row < m_results.size()) {
if(!data.isEmpty()) {
m_coverData[albumId] = data;
m_results[row].cover = data;
emit coverAvailable(index(row, 0));
} else {
m_errorList << tr("Cover reply is invalid (internal error).");
}
setFetchingCover(false);
} else {
// prepare parsing MusicBrainz meta data
beginResetModel();
m_results.clear();
m_reader.addData(data);
// parse XML tree
#define xmlReader m_reader
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("metadata") {
children {
iftag("recording-list") {
children {
iftag("recording") {
SongDescription currentDescription;
children {
iftag("title") {
currentDescription.title = text;
} eliftag("artist-credit") {
children {
iftag("name-credit") {
children {
iftag("artist") {
children {
iftag("name") {
currentDescription.artist = text;
} else_skip
}
} else_skip
}
} else_skip
}
} eliftag("release-list") {
children {
iftag("release") {
if(currentDescription.albumId.isEmpty()) {
currentDescription.albumId = attribute("id").toString();
}
children {
iftag("title") {
currentDescription.album = text;
} eliftag("date") {
currentDescription.year = text;
} eliftag("medium-list") {
children {
iftag("medium") {
children {
iftag("position") {
currentDescription.disk = text.toUInt();
} eliftag("track-list") {
currentDescription.totalTracks = attribute("count").toUInt();
children {
iftag("track") {
children {
iftag("number") {
currentDescription.track = text.toUInt();
} else_skip
}
} else_skip
}
} else_skip
}
} else_skip
}
} else_skip
}
} else_skip
}
} eliftag("tag-list") {
children {
iftag("tag") {
children {
iftag("name") {
if(!currentDescription.genre.isEmpty()) {
currentDescription.genre.append(QLatin1Char(' '));
}
currentDescription.genre.append(text);
} else_skip
}
} else_skip
}
} else_skip
}
m_results << currentDescription;
} else_skip
}
} else_skip
}
} else_skip
}
#include <qtutilities/misc/undefxmlparsermacros.h>
// check for parsing errors
switch(m_reader.error()) {
case QXmlStreamReader::NoError:
case QXmlStreamReader::PrematureEndOfDocumentError:
break;
default:
m_errorList << m_reader.errorString();
}
// promote changes
endResetModel();
m_errorList << tr("Internal error: context for cover reply invalid");
setResultsAvailable(true);
}
setFetchingCover(false);
}
QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
{
static QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
static const QString defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
// compose parts
QStringList parts;
@ -202,7 +217,7 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
QNetworkReply *queryCoverArtArchive(const QString &albumId)
{
static QString defaultArchiveUrl(QStringLiteral("https://coverartarchive.org"));
static const QString defaultArchiveUrl(QStringLiteral("https://coverartarchive.org"));
return networkAccessManager().get(QNetworkRequest(QUrl((Settings::coverArtArchiveUrl().isEmpty() ? defaultArchiveUrl : Settings::coverArtArchiveUrl()) % QStringLiteral("/release/") % albumId % QStringLiteral("/front"))));
}

View File

@ -3,8 +3,6 @@
#include "./dbquery.h"
#include <QXmlStreamReader>
#include <map>
QT_FORWARD_DECLARE_CLASS(QNetworkRequest)
@ -23,14 +21,16 @@ private:
public:
MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchCover(const QModelIndex &index);
static QNetworkRequest coverRequest(const QString &albumId);
protected:
void parseResults(QNetworkReply *reply, const QByteArray &data);
void parseInitialResults(const QByteArray &data);
private:
void handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row);
void parseCoverResults(const QString &albumId, int row, const QByteArray &data);
private:
static std::map<QString, QByteArray> m_coverData;
QXmlStreamReader m_reader;
What m_what;
};

View File

@ -20,6 +20,7 @@
#include <QDialog>
#include <QGraphicsView>
#include <QGraphicsItem>
#include <QTextBrowser>
using namespace ConversionUtilities;
using namespace Dialogs;
@ -36,6 +37,7 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
m_tagEditorWidget(tagEditorWidget),
m_model(nullptr),
m_coverIndex(-1),
m_lyricsIndex(-1),
m_menu(new QMenu(parent))
{
m_ui->setupUi(this);
@ -117,12 +119,22 @@ void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit)
}
}
SongDescription DbQueryWidget::currentSongDescription() const
{
SongDescription desc;
desc.title = m_ui->titleLineEdit->text();
desc.album = m_ui->albumLineEdit->text();
desc.artist = m_ui->artistLineEdit->text();
desc.track = static_cast<unsigned int>(m_ui->trackSpinBox->value());
return desc;
}
void DbQueryWidget::searchMusicBrainz()
{
// check whether enough search terms are supplied
if(m_ui->titleLineEdit->text().isEmpty() && m_ui->albumLineEdit->text().isEmpty() && m_ui->artistLineEdit->text().isEmpty()) {
m_ui->notificationLabel->setNotificationType(NotificationType::Critical);
m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied"));
m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied - at least title, album or artist must be specified"));
return;
}
@ -132,18 +144,11 @@ void DbQueryWidget::searchMusicBrainz()
// show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->setText(tr("Retrieving meta data ..."));
m_ui->notificationLabel->setText(tr("Retrieving meta data from MusicBrainz ..."));
setStatus(false);
// get song description
SongDescription desc;
desc.title = m_ui->titleLineEdit->text();
desc.album = m_ui->albumLineEdit->text();
desc.artist = m_ui->artistLineEdit->text();
desc.track = m_ui->trackSpinBox->value();
// do actual query
m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(std::move(desc)));
m_ui->resultsTreeView->setModel(m_model = queryMusicBrainz(currentSongDescription()));
connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults);
connect(m_model, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex);
}
@ -153,7 +158,7 @@ void DbQueryWidget::searchLyricsWikia()
// check whether enough search terms are supplied
if(m_ui->artistLineEdit->text().isEmpty()) {
m_ui->notificationLabel->setNotificationType(NotificationType::Critical);
m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied"));
m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied - artist is mandatory"));
return;
}
@ -163,20 +168,13 @@ void DbQueryWidget::searchLyricsWikia()
// show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->setText(tr("Retrieving meta data ..."));
m_ui->notificationLabel->setText(tr("Retrieving meta data from LyricsWikia ..."));
setStatus(false);
// get song description
SongDescription desc;
desc.title = m_ui->titleLineEdit->text();
desc.album = m_ui->albumLineEdit->text();
desc.artist = m_ui->artistLineEdit->text();
desc.track = static_cast<unsigned int>(m_ui->trackSpinBox->value());
// do actual query
m_ui->resultsTreeView->setModel(m_model = queryLyricsWikia(std::move(desc)));
m_ui->resultsTreeView->setModel(m_model = queryLyricsWikia(currentSongDescription()));
connect(m_model, &QueryResultsModel::resultsAvailable, this, &DbQueryWidget::showResults);
connect(m_model, &QueryResultsModel::coverAvailable, this, &DbQueryWidget::showCoverFromIndex);
connect(m_model, &QueryResultsModel::lyricsAvailable, this, &DbQueryWidget::showLyricsFromIndex);
}
void DbQueryWidget::abortSearch()
@ -261,27 +259,56 @@ void DbQueryWidget::applyResults()
int row = selection.front().row();
TagValue value = m_model->fieldValue(row, field);
if(value.isEmpty() && field == KnownField::Cover) {
// cover value is empty -> cover still needs to be fetched
if(m_model->fetchCover(selection.front())) {
// cover is available now
tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling);
} else {
// cover is fetched asynchronously
// -> show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->setText(tr("Retrieving cover art to be applied ..."));
setStatus(false);
// -> apply cover when available
connect(m_model, &QueryResultsModel::coverAvailable, [this, row, previousValueHandling](const QModelIndex &index) {
if(row == index.row()) {
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling);
if(value.isEmpty()) {
// cover and lyrics might be fetched belated
switch(field) {
case KnownField::Cover:
if(m_model->fetchCover(selection.front())) {
// cover is available now
tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling);
} else {
// cover is fetched asynchronously
// -> show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->appendLine(tr("Retrieving cover art to be applied ..."));
setStatus(false);
// -> apply cover when available
connect(m_model, &QueryResultsModel::coverAvailable, [this, row, previousValueHandling](const QModelIndex &index) {
if(row == index.row()) {
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
tagEdit->setValue(KnownField::Cover, m_model->fieldValue(row, KnownField::Cover), previousValueHandling);
}
}
}
});
});
}
break;
case KnownField::Lyrics:
if(m_model->fetchLyrics(selection.front())) {
// lyrics are available now
tagEdit->setValue(KnownField::Lyrics, m_model->fieldValue(row, KnownField::Lyrics), previousValueHandling);
} else {
// lyrics are fetched asynchronously
// -> show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->appendLine(tr("Retrieving lyrics to be applied ..."));
setStatus(false);
// -> apply cover when available
connect(m_model, &QueryResultsModel::lyricsAvailable, [this, row, previousValueHandling](const QModelIndex &index) {
if(row == index.row()) {
if(TagEdit *tagEdit = m_tagEditorWidget->activeTagEdit()) {
tagEdit->setValue(KnownField::Lyrics, m_model->fieldValue(row, KnownField::Lyrics), previousValueHandling);
}
}
});
}
break;
default:
;
}
} else {
// any other fields are just set
tagEdit->setValue(field, value, previousValueHandling);
}
}
@ -308,6 +335,7 @@ void DbQueryWidget::showResultsContextMenu()
}
if(m_model && m_model->areResultsAvailable()) {
contextMenu.addAction(QIcon::fromTheme(QStringLiteral("view-preview")), tr("Show cover"), this, SLOT(fetchAndShowCoverForSelection()));
contextMenu.addAction(QIcon::fromTheme(QStringLiteral("view-media-lyrics")), tr("Show lyrics"), this, SLOT(fetchAndShowLyricsForSelection()));
}
contextMenu.exec(QCursor::pos());
}
@ -328,7 +356,7 @@ void DbQueryWidget::fetchAndShowCoverForSelection()
if(const QByteArray *cover = m_model->cover(selectedIndex)) {
showCover(*cover);
} else {
// cover couldn't be fetched
// cover couldn't be fetched, error tracks via resultsAvailable() signal so nothing to do
}
} else {
// cover is fetched asynchronously
@ -345,9 +373,39 @@ void DbQueryWidget::fetchAndShowCoverForSelection()
}
}
void DbQueryWidget::fetchAndShowLyricsForSelection()
{
if(m_model) {
if(const QItemSelectionModel *selectionModel = m_ui->resultsTreeView->selectionModel()) {
const QModelIndexList selection = selectionModel->selection().indexes();
if(!selection.isEmpty()) {
const QModelIndex &selectedIndex = selection.at(0);
if(const QString *lyrics = m_model->lyrics(selectedIndex)) {
showLyrics(*lyrics);
} else {
if(m_model->fetchLyrics(selectedIndex)) {
if(const QByteArray *cover = m_model->cover(selectedIndex)) {
showLyrics(*cover);
} else {
// lyrics couldn't be fetched, error tracks via resultsAvailable() signal so nothing to do
}
} else {
// lyrics are fetched asynchronously
// -> memorize index to be shown
m_lyricsIndex = selectedIndex.row();
// -> show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->setText(tr("Retrieving lyrics ..."));
setStatus(false);
}
}
}
}
}
}
void DbQueryWidget::showCover(const QByteArray &data)
{
// show cover
QDialog dlg;
dlg.setWindowFlags(Qt::Tool);
dlg.setWindowTitle(tr("Cover - %1").arg(QApplication::applicationName()));
@ -371,6 +429,29 @@ void DbQueryWidget::showCoverFromIndex(const QModelIndex &index)
}
}
void DbQueryWidget::showLyrics(const QString &data)
{
QDialog dlg;
dlg.setWindowFlags(Qt::Tool);
dlg.setWindowTitle(tr("Lyrics - %1").arg(QApplication::applicationName()));
QBoxLayout layout(QBoxLayout::Up);
layout.setMargin(0);
QTextBrowser textBrowser;
layout.addWidget(&textBrowser);
textBrowser.setText(data);
dlg.setLayout(&layout);
dlg.resize(400, 400);
dlg.exec();
}
void DbQueryWidget::showLyricsFromIndex(const QModelIndex &index)
{
if(m_lyricsIndex == index.row()) {
m_lyricsIndex = -1;
showLyrics(*m_model->lyrics(index));
}
}
void DbQueryWidget::clearSearchCriteria()
{
m_ui->titleLineEdit->clear();

View File

@ -22,6 +22,7 @@ class DbQueryWidget;
class QueryResultsModel;
class TagEditorWidget;
class TagEdit;
struct SongDescription;
class DbQueryWidget : public QWidget
{
@ -32,6 +33,7 @@ public:
~DbQueryWidget();
void insertSearchTermsFromTagEdit(TagEdit *tagEdit);
SongDescription currentSongDescription() const;
public slots:
void searchMusicBrainz();
@ -47,8 +49,11 @@ private slots:
void fileStatusChanged(bool opened, bool hasTags);
void showResultsContextMenu();
void fetchAndShowCoverForSelection();
void fetchAndShowLyricsForSelection();
void showCover(const QByteArray &data);
void showCoverFromIndex(const QModelIndex &index);
void showLyrics(const QString &data);
void showLyricsFromIndex(const QModelIndex &index);
protected:
bool eventFilter(QObject *obj, QEvent *event);
@ -57,7 +62,7 @@ private:
std::unique_ptr<Ui::DbQueryWidget> m_ui;
TagEditorWidget *m_tagEditorWidget;
QueryResultsModel *m_model;
int m_coverIndex;
int m_coverIndex, m_lyricsIndex;
QMenu *m_menu;
QAction *m_insertPresentDataAction;
};

View File

@ -2,14 +2,6 @@
<ui version="4.0">
<class>QtGui::DbQueryWidget</class>
<widget class="QWidget" name="QtGui::DbQueryWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>731</width>
<height>619</height>
</rect>
</property>
<property name="windowTitle">
<string>MusicBrainz/LyricsWikia search</string>
</property>
@ -218,7 +210,7 @@
<item>
<widget class="QCheckBox" name="overrideCheckBox">
<property name="text">
<string>override existing values</string>
<string>Override existing values</string>
</property>
<property name="checked">
<bool>true</bool>

View File

@ -110,10 +110,7 @@ MainWindow::MainWindow(QWidget *parent) :
// dbquery dock widget
if(Settings::dbQueryWidgetShown()) {
toggleDbQueryWidget();
} else {
// ensure the dock widget is invisible
m_ui->dbQueryDockWidget->setVisible(false);
m_ui->dbQueryDockWidget->setWidget(m_dbQueryWidget = new DbQueryWidget(m_ui->tagEditorWidget, this));
}
// restore locked

View File

@ -173,18 +173,19 @@ QSize NotificationLabel::minimumSizeHint() const
void NotificationLabel::setText(const QString &text)
{
const bool updateTooltip = toolTip().isEmpty() || toolTip() == m_text;
m_text = text;
updateGeometry();
update(textRect());
if(toolTip().isEmpty()) {
setToolTip(text);
if(updateTooltip) {
setToolTip(m_text);
}
}
void NotificationLabel::clearText()
{
if(toolTip() == m_text) {
toolTip().clear();
setToolTip(QString());
}
m_text.clear();
updateGeometry();

View File

@ -719,44 +719,52 @@ row</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="48"/>
<location filename="../gui/dbquerywidget.cpp" line="50"/>
<source>Search hasn&apos;t been started</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="66"/>
<location filename="../gui/dbquerywidget.cpp" line="68"/>
<source>Insert present data</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="70"/>
<location filename="../gui/dbquerywidget.cpp" line="72"/>
<source>Clear search criteria</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="125"/>
<location filename="../gui/dbquerywidget.cpp" line="156"/>
<source>Insufficient search criteria supplied</source>
<location filename="../gui/dbquerywidget.cpp" line="137"/>
<source>Insufficient search criteria supplied - at least title, album or artist must be specified</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="135"/>
<location filename="../gui/dbquerywidget.cpp" line="166"/>
<source>Retrieving meta data ...</source>
<location filename="../gui/dbquerywidget.cpp" line="147"/>
<source>Retrieving meta data from MusicBrainz ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="196"/>
<location filename="../gui/dbquerywidget.cpp" line="161"/>
<source>Insufficient search criteria supplied - artist is mandatory</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="171"/>
<source>Retrieving meta data from LyricsWikia ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="194"/>
<source>Aborted</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="208"/>
<location filename="../gui/dbquerywidget.cpp" line="206"/>
<source>No results available</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<location filename="../gui/dbquerywidget.cpp" line="210"/>
<location filename="../gui/dbquerywidget.cpp" line="208"/>
<source>%1 result(s) available</source>
<translation type="unfinished">
<numerusform></numerusform>
@ -769,25 +777,45 @@ row</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="307"/>
<location filename="../gui/dbquerywidget.cpp" line="294"/>
<source>Retrieving lyrics to be applied ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="334"/>
<source>Use selected row</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="310"/>
<location filename="../gui/dbquerywidget.cpp" line="337"/>
<source>Show cover</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="339"/>
<location filename="../gui/dbquerywidget.cpp" line="338"/>
<source>Show lyrics</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="367"/>
<source>Retrieving cover art ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="353"/>
<location filename="../gui/dbquerywidget.cpp" line="398"/>
<source>Retrieving lyrics ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="411"/>
<source>Cover - %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="436"/>
<source>Lyrics - %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::EditorAutoCorrectionOptionPage</name>
@ -1578,22 +1606,27 @@ another position would prevent rewriting the entire file</source>
<context>
<name>QtGui::HttpResultsModel</name>
<message>
<location filename="../dbquery/dbquery.cpp" line="255"/>
<location filename="../dbquery/dbquery.cpp" line="297"/>
<source>&lt;p&gt;Do you want to redirect form &lt;i&gt;%1&lt;/i&gt; to &lt;i&gt;%2&lt;/i&gt;?&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="257"/>
<location filename="../dbquery/dbquery.cpp" line="299"/>
<source>Search</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="268"/>
<location filename="../dbquery/dbquery.cpp" line="304"/>
<source>Redirection to: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="312"/>
<location filename="../dbquery/dbquery.cpp" line="309"/>
<source>Server replied no data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="331"/>
<source>Aborted by user.</source>
<translation type="unfinished"></translation>
</message>
@ -1723,6 +1756,39 @@ another position would prevent rewriting the entire file</source>
<translation></translation>
</message>
</context>
<context>
<name>QtGui::LyricsWikiaResultsModel</name>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="37"/>
<source>Unable to fetch lyrics: Artist or title is unknown.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="151"/>
<source>Internal error: context for song details reply invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="191"/>
<source>Unable to parse song details: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="200"/>
<source>Song details requested for %1/%2 do not contain URL for Wiki page</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="228"/>
<source>Internal error: context for LyricsWikia page reply invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="238"/>
<source>Song details requested for %1/%2 do not contain lyrics</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::MainWindow</name>
<message>
@ -1846,7 +1912,7 @@ another position would prevent rewriting the entire file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="202"/>
<location filename="../gui/mainwindow.cpp" line="199"/>
<source>Lock layout</source>
<translation type="unfinished"></translation>
</message>
@ -1922,63 +1988,63 @@ another position would prevent rewriting the entire file</source>
<translation></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="335"/>
<location filename="../gui/mainwindow.cpp" line="332"/>
<source>No file opened.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="356"/>
<location filename="../gui/mainwindow.cpp" line="353"/>
<source>A tag editing utility supporting ID3, MP4 (iTunes style), Vorbis and Matroska tags.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="473"/>
<location filename="../gui/mainwindow.cpp" line="470"/>
<source>Unable to show the next file because it can&apos;t be found anymore.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="483"/>
<location filename="../gui/mainwindow.cpp" line="480"/>
<source>Open file - </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="494"/>
<location filename="../gui/mainwindow.cpp" line="491"/>
<source>Save changes as - </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="516"/>
<location filename="../gui/mainwindow.cpp" line="513"/>
<source>Save file information - </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="535"/>
<location filename="../gui/mainwindow.cpp" line="532"/>
<source>No file is opened.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="538"/>
<location filename="../gui/mainwindow.cpp" line="535"/>
<source>Unable to save file information because the current process hasn&apos;t been finished yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="525"/>
<location filename="../gui/mainwindow.cpp" line="522"/>
<source>Unable to write to file.
%1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="197"/>
<location filename="../gui/mainwindow.cpp" line="194"/>
<source>Unlock layout</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="528"/>
<location filename="../gui/mainwindow.cpp" line="525"/>
<source>Unable to open file.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="532"/>
<location filename="../gui/mainwindow.cpp" line="529"/>
<source>No file information available.</source>
<translation type="unfinished"></translation>
</message>
@ -1986,13 +2052,13 @@ another position would prevent rewriting the entire file</source>
<context>
<name>QtGui::MusicBrainzResultsModel</name>
<message>
<location filename="../dbquery/musicbrainz.cpp" line="44"/>
<source>Unable to fetch cover: Album ID is unknown.</source>
<location filename="../dbquery/musicbrainz.cpp" line="45"/>
<source>Unable to fetch cover: Album ID unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/musicbrainz.cpp" line="63"/>
<source>Cover reply is invalid (internal error).</source>
<location filename="../dbquery/musicbrainz.cpp" line="182"/>
<source>Internal error: context for cover reply invalid</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -2315,43 +2381,48 @@ another position would prevent rewriting the entire file</source>
<context>
<name>QtGui::QueryResultsModel</name>
<message>
<location filename="../dbquery/dbquery.cpp" line="148"/>
<location filename="../dbquery/dbquery.cpp" line="150"/>
<source>Song title</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="150"/>
<location filename="../dbquery/dbquery.cpp" line="152"/>
<source>Album</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="152"/>
<location filename="../dbquery/dbquery.cpp" line="154"/>
<source>Artist</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="154"/>
<location filename="../dbquery/dbquery.cpp" line="156"/>
<source>Year</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="156"/>
<location filename="../dbquery/dbquery.cpp" line="158"/>
<source>Track</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="158"/>
<location filename="../dbquery/dbquery.cpp" line="160"/>
<source>Total tracks</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="160"/>
<location filename="../dbquery/dbquery.cpp" line="162"/>
<source>Genre</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="207"/>
<source>Fetching the cover is not implemented for the selected provider.</source>
<location filename="../dbquery/dbquery.cpp" line="212"/>
<source>Fetching cover is not implemented for this provider</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="242"/>
<source>Fetching lyrics is not implemented for this provider</source>
<translation type="unfinished"></translation>
</message>
</context>

View File

@ -648,44 +648,52 @@
<context>
<name>QtGui::DbQueryWidget</name>
<message>
<location filename="../gui/dbquerywidget.cpp" line="48"/>
<location filename="../gui/dbquerywidget.cpp" line="50"/>
<source>Search hasn&apos;t been started</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="66"/>
<location filename="../gui/dbquerywidget.cpp" line="68"/>
<source>Insert present data</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="70"/>
<location filename="../gui/dbquerywidget.cpp" line="72"/>
<source>Clear search criteria</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="125"/>
<location filename="../gui/dbquerywidget.cpp" line="156"/>
<source>Insufficient search criteria supplied</source>
<location filename="../gui/dbquerywidget.cpp" line="137"/>
<source>Insufficient search criteria supplied - at least title, album or artist must be specified</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="135"/>
<location filename="../gui/dbquerywidget.cpp" line="166"/>
<source>Retrieving meta data ...</source>
<location filename="../gui/dbquerywidget.cpp" line="147"/>
<source>Retrieving meta data from MusicBrainz ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="196"/>
<location filename="../gui/dbquerywidget.cpp" line="161"/>
<source>Insufficient search criteria supplied - artist is mandatory</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="171"/>
<source>Retrieving meta data from LyricsWikia ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="194"/>
<source>Aborted</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="208"/>
<location filename="../gui/dbquerywidget.cpp" line="206"/>
<source>No results available</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<location filename="../gui/dbquerywidget.cpp" line="210"/>
<location filename="../gui/dbquerywidget.cpp" line="208"/>
<source>%1 result(s) available</source>
<translation type="unfinished">
<numerusform>%1 result available</numerusform>
@ -698,25 +706,45 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="307"/>
<location filename="../gui/dbquerywidget.cpp" line="294"/>
<source>Retrieving lyrics to be applied ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="334"/>
<source>Use selected row</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="310"/>
<location filename="../gui/dbquerywidget.cpp" line="337"/>
<source>Show cover</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="339"/>
<location filename="../gui/dbquerywidget.cpp" line="338"/>
<source>Show lyrics</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="367"/>
<source>Retrieving cover art ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="353"/>
<location filename="../gui/dbquerywidget.cpp" line="398"/>
<source>Retrieving lyrics ...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="411"/>
<source>Cover - %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.cpp" line="436"/>
<source>Lyrics - %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/dbquerywidget.ui" line="14"/>
<source>MusicBrainz/LyricsWikia search</source>
@ -1578,22 +1606,27 @@ another position would prevent rewriting the entire file</source>
<context>
<name>QtGui::HttpResultsModel</name>
<message>
<location filename="../dbquery/dbquery.cpp" line="255"/>
<location filename="../dbquery/dbquery.cpp" line="297"/>
<source>&lt;p&gt;Do you want to redirect form &lt;i&gt;%1&lt;/i&gt; to &lt;i&gt;%2&lt;/i&gt;?&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="257"/>
<location filename="../dbquery/dbquery.cpp" line="299"/>
<source>Search</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="268"/>
<location filename="../dbquery/dbquery.cpp" line="304"/>
<source>Redirection to: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="312"/>
<location filename="../dbquery/dbquery.cpp" line="309"/>
<source>Server replied no data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="331"/>
<source>Aborted by user.</source>
<translation type="unfinished"></translation>
</message>
@ -1723,6 +1756,39 @@ another position would prevent rewriting the entire file</source>
<translation></translation>
</message>
</context>
<context>
<name>QtGui::LyricsWikiaResultsModel</name>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="37"/>
<source>Unable to fetch lyrics: Artist or title is unknown.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="151"/>
<source>Internal error: context for song details reply invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="191"/>
<source>Unable to parse song details: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="200"/>
<source>Song details requested for %1/%2 do not contain URL for Wiki page</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="228"/>
<source>Internal error: context for LyricsWikia page reply invalid</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/lyricswikia.cpp" line="238"/>
<source>Song details requested for %1/%2 do not contain lyrics</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtGui::MainWindow</name>
<message>
@ -1912,7 +1978,7 @@ another position would prevent rewriting the entire file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="202"/>
<location filename="../gui/mainwindow.cpp" line="199"/>
<source>Lock layout</source>
<translation type="unfinished"></translation>
</message>
@ -1922,63 +1988,63 @@ another position would prevent rewriting the entire file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="197"/>
<location filename="../gui/mainwindow.cpp" line="194"/>
<source>Unlock layout</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="335"/>
<location filename="../gui/mainwindow.cpp" line="332"/>
<source>No file opened.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="356"/>
<location filename="../gui/mainwindow.cpp" line="353"/>
<source>A tag editing utility supporting ID3, MP4 (iTunes style), Vorbis and Matroska tags.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="473"/>
<location filename="../gui/mainwindow.cpp" line="470"/>
<source>Unable to show the next file because it can&apos;t be found anymore.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="483"/>
<location filename="../gui/mainwindow.cpp" line="480"/>
<source>Open file - </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="494"/>
<location filename="../gui/mainwindow.cpp" line="491"/>
<source>Save changes as - </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="516"/>
<location filename="../gui/mainwindow.cpp" line="513"/>
<source>Save file information - </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="525"/>
<location filename="../gui/mainwindow.cpp" line="522"/>
<source>Unable to write to file.
%1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="528"/>
<location filename="../gui/mainwindow.cpp" line="525"/>
<source>Unable to open file.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="532"/>
<location filename="../gui/mainwindow.cpp" line="529"/>
<source>No file information available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="535"/>
<location filename="../gui/mainwindow.cpp" line="532"/>
<source>No file is opened.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/mainwindow.cpp" line="538"/>
<location filename="../gui/mainwindow.cpp" line="535"/>
<source>Unable to save file information because the current process hasn&apos;t been finished yet.</source>
<translation type="unfinished"></translation>
</message>
@ -1986,13 +2052,13 @@ another position would prevent rewriting the entire file</source>
<context>
<name>QtGui::MusicBrainzResultsModel</name>
<message>
<location filename="../dbquery/musicbrainz.cpp" line="44"/>
<source>Unable to fetch cover: Album ID is unknown.</source>
<location filename="../dbquery/musicbrainz.cpp" line="45"/>
<source>Unable to fetch cover: Album ID unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/musicbrainz.cpp" line="63"/>
<source>Cover reply is invalid (internal error).</source>
<location filename="../dbquery/musicbrainz.cpp" line="182"/>
<source>Internal error: context for cover reply invalid</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -2315,43 +2381,48 @@ another position would prevent rewriting the entire file</source>
<context>
<name>QtGui::QueryResultsModel</name>
<message>
<location filename="../dbquery/dbquery.cpp" line="148"/>
<location filename="../dbquery/dbquery.cpp" line="150"/>
<source>Song title</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="150"/>
<location filename="../dbquery/dbquery.cpp" line="152"/>
<source>Album</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="152"/>
<location filename="../dbquery/dbquery.cpp" line="154"/>
<source>Artist</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="154"/>
<location filename="../dbquery/dbquery.cpp" line="156"/>
<source>Year</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="156"/>
<location filename="../dbquery/dbquery.cpp" line="158"/>
<source>Track</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="158"/>
<location filename="../dbquery/dbquery.cpp" line="160"/>
<source>Total tracks</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="160"/>
<location filename="../dbquery/dbquery.cpp" line="162"/>
<source>Genre</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="207"/>
<source>Fetching the cover is not implemented for the selected provider.</source>
<location filename="../dbquery/dbquery.cpp" line="212"/>
<source>Fetching cover is not implemented for this provider</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dbquery/dbquery.cpp" line="242"/>
<source>Fetching lyrics is not implemented for this provider</source>
<translation type="unfinished"></translation>
</message>
</context>