lastfm: rework service architecture a bit and implement getting full artist info

This commit is contained in:
Andrzej Rybczak
2013-07-10 17:20:09 +02:00
parent 0092dfe044
commit 1710f892c5
8 changed files with 188 additions and 299 deletions

View File

@@ -505,9 +505,6 @@
# refetch_lyrics # refetch_lyrics
# #
#def_key "`" #def_key "`"
# refetch_artist_info
#
#def_key "`"
# add_random_items # add_random_items
# #
#def_key "ctrl_p" #def_key "ctrl_p"

View File

@@ -2072,22 +2072,6 @@ void RefetchLyrics::run()
# endif // HAVE_CURL_CURL_H # endif // HAVE_CURL_CURL_H
} }
bool RefetchArtistInfo::canBeRun() const
{
# ifdef HAVE_CURL_CURL_H
return myScreen == myLastfm;
# else
return false;
# endif // HAVE_CURL_CURL_H
}
void RefetchArtistInfo::run()
{
# ifdef HAVE_CURL_CURL_H
myLastfm->Refetch();
# endif // HAVE_CURL_CURL_H
}
bool SetSelectedItemsPriority::canBeRun() const bool SetSelectedItemsPriority::canBeRun() const
{ {
if (Mpd.Version() < 17) if (Mpd.Version() < 17)
@@ -2161,7 +2145,7 @@ bool ShowArtistInfo::canBeRun() const
void ShowArtistInfo::run() void ShowArtistInfo::run()
{ {
# ifdef HAVE_CURL_CURL_H # ifdef HAVE_CURL_CURL_H
if (myScreen == myLastfm || myLastfm->isDownloading()) if (myScreen == myLastfm)
{ {
myLastfm->switchTo(); myLastfm->switchTo();
return; return;
@@ -2181,8 +2165,11 @@ void ShowArtistInfo::run()
artist = s->getArtist(); artist = s->getArtist();
} }
if (!artist.empty() && myLastfm->SetArtistInfoArgs(artist, Config.lastfm_preferred_language)) if (!artist.empty())
{
myLastfm->queueJob(LastFm::ArtistInfo(artist, Config.lastfm_preferred_language));
myLastfm->switchTo(); myLastfm->switchTo();
}
# endif // HAVE_CURL_CURL_H # endif // HAVE_CURL_CURL_H
} }
@@ -2540,7 +2527,6 @@ void populateActions()
insert_action(new Actions::ToggleLibraryTagType()); insert_action(new Actions::ToggleLibraryTagType());
insert_action(new Actions::ToggleMediaLibrarySortMode()); insert_action(new Actions::ToggleMediaLibrarySortMode());
insert_action(new Actions::RefetchLyrics()); insert_action(new Actions::RefetchLyrics());
insert_action(new Actions::RefetchArtistInfo());
insert_action(new Actions::SetSelectedItemsPriority()); insert_action(new Actions::SetSelectedItemsPriority());
insert_action(new Actions::FilterPlaylistOnPriorities()); insert_action(new Actions::FilterPlaylistOnPriorities());
insert_action(new Actions::ShowSongInfo()); insert_action(new Actions::ShowSongInfo());

View File

@@ -942,15 +942,6 @@ protected:
virtual void run(); virtual void run();
}; };
struct RefetchArtistInfo : public BaseAction
{
RefetchArtistInfo() : BaseAction(Type::RefetchArtistInfo, "refetch_artist_info") { }
protected:
virtual bool canBeRun() const;
virtual void run();
};
struct SetSelectedItemsPriority : public BaseAction struct SetSelectedItemsPriority : public BaseAction
{ {
SetSelectedItemsPriority() SetSelectedItemsPriority()

View File

@@ -21,9 +21,7 @@
#ifndef NCMPCPP_CURL_HANDLE_H #ifndef NCMPCPP_CURL_HANDLE_H
#define NCMPCPP_CURL_HANDLE_H #define NCMPCPP_CURL_HANDLE_H
#ifdef HAVE_CONFIG_H #include "config.h"
# include "config.h"
#endif
#ifdef HAVE_CURL_CURL_H #ifdef HAVE_CURL_CURL_H

View File

@@ -22,27 +22,12 @@
#ifdef HAVE_CURL_CURL_H #ifdef HAVE_CURL_CURL_H
#ifdef WIN32
# include <io.h>
#else
# include <sys/stat.h>
#endif // WIN32
#include <cassert>
#include <cerrno>
#include <cstring>
#include <boost/locale/conversion.hpp>
#include <fstream>
#include <iostream>
#include "error.h"
#include "helpers.h" #include "helpers.h"
#include "charset.h" #include "charset.h"
#include "global.h" #include "global.h"
#include "statusbar.h" #include "statusbar.h"
#include "title.h" #include "title.h"
#include "screen_switcher.h" #include "screen_switcher.h"
#include "utility/string.h"
using Global::MainHeight; using Global::MainHeight;
using Global::MainStartY; using Global::MainStartY;
@@ -51,7 +36,6 @@ Lastfm *myLastfm;
Lastfm::Lastfm() Lastfm::Lastfm()
: Screen(NC::Scrollpad(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border::None)) : Screen(NC::Scrollpad(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border::None))
, isReadyToTake(0), isDownloadInProgress(0)
{ } { }
void Lastfm::resize() void Lastfm::resize()
@@ -65,23 +49,13 @@ void Lastfm::resize()
std::wstring Lastfm::title() std::wstring Lastfm::title()
{ {
return itsTitle; return m_title;
} }
void Lastfm::update() void Lastfm::update()
{ {
if (isReadyToTake) if (m_worker.valid() && m_worker.is_ready())
Take(); getResult();
}
void Lastfm::Take()
{
assert(isReadyToTake);
pthread_join(itsDownloader, 0);
w.flush();
w.refresh();
isDownloadInProgress = 0;
isReadyToTake = 0;
} }
void Lastfm::switchTo() void Lastfm::switchTo()
@@ -90,134 +64,27 @@ void Lastfm::switchTo()
if (myScreen != this) if (myScreen != this)
{ {
SwitchTo::execute(this); SwitchTo::execute(this);
// get an old info if it waits
if (isReadyToTake)
Take();
Load();
drawHeader(); drawHeader();
} }
else else
switchToPreviousScreen(); switchToPreviousScreen();
} }
void Lastfm::Load() void Lastfm::getResult()
{ {
if (isDownloadInProgress) auto result = m_worker.get();
return;
assert(itsService.get());
assert(itsService->checkArgs(itsArgs));
SetTitleAndFolder();
w.clear();
w.reset();
std::string artist = itsArgs.find("artist")->second;
std::string file = boost::locale::to_lower(artist + ".txt");
removeInvalidCharsFromFilename(file, Config.generate_win32_compatible_filenames);
itsFilename = itsFolder + "/" + file;
mkdir(itsFolder.c_str()
# ifndef WIN32
, 0755
# endif // !WIN32
);
std::ifstream input(itsFilename.c_str());
if (input.is_open())
{
bool first = 1;
std::string line;
while (std::getline(input, line))
{
if (!first)
w << '\n';
w << Charset::utf8ToLocale(line);
first = 0;
}
input.close();
itsService->colorizeOutput(w);
}
else
{
w << "Fetching informations... ";
pthread_create(&itsDownloader, 0, DownloadWrapper, this);
isDownloadInProgress = 1;
}
w.flush();
}
void Lastfm::SetTitleAndFolder()
{
if (dynamic_cast<ArtistInfo *>(itsService.get()))
{
itsTitle = L"Artist info - ";
itsTitle += ToWString(itsArgs.find("artist")->second);
itsFolder = Config.ncmpcpp_directory + "artists";
}
}
void *Lastfm::DownloadWrapper(void *this_ptr)
{
static_cast<Lastfm *>(this_ptr)->Download();
pthread_exit(0);
}
void Lastfm::Download()
{
LastfmService::Result result = itsService->fetch(itsArgs);
if (result.first) if (result.first)
{ {
Save(result.second);
w.clear(); w.clear();
w << Charset::utf8ToLocale(result.second); w << Charset::utf8ToLocale(result.second);
itsService->colorizeOutput(w); m_service->beautifyOutput(w);
} }
else else
w << NC::Color::Red << result.second << NC::Color::End; w << " " << NC::Color::Red << result.second << NC::Color::End;
w.flush();
isReadyToTake = 1; w.refresh();
} // reset m_worker so it's no longer valid
m_worker = boost::unique_future<LastFm::Service::Result>();
void Lastfm::Save(const std::string &data)
{
std::ofstream output(itsFilename.c_str());
if (output.is_open())
{
output << data;
output.close();
}
else
std::cerr << "ncmpcpp: couldn't save file \"" << itsFilename << "\"\n";
}
void Lastfm::Refetch()
{
if (remove(itsFilename.c_str()) && errno != ENOENT)
{
const char msg[] = "Couldn't remove \"%ls\": %s";
Statusbar::msg(msg, wideShorten(ToWString(itsFilename), COLS-const_strlen(msg)-25).c_str(), strerror(errno));
return;
}
Load();
}
bool Lastfm::SetArtistInfoArgs(const std::string &artist, const std::string &lang)
{
if (isDownloading())
return false;
itsService.reset(new ArtistInfo);
itsArgs.clear();
itsArgs["artist"] = artist;
if (!lang.empty())
itsArgs["lang"] = lang;
return true;
} }
#endif // HVAE_CURL_CURL_H #endif // HVAE_CURL_CURL_H

View File

@@ -26,11 +26,12 @@
#ifdef HAVE_CURL_CURL_H #ifdef HAVE_CURL_CURL_H
#include <memory> #include <memory>
#include <pthread.h> #include <boost/thread/future.hpp>
#include "interfaces.h" #include "interfaces.h"
#include "lastfm_service.h" #include "lastfm_service.h"
#include "screen.h" #include "screen.h"
#include "utility/wide_string.h"
struct Lastfm: Screen<NC::Scrollpad>, Tabbable struct Lastfm: Screen<NC::Scrollpad>, Tabbable
{ {
@@ -49,37 +50,39 @@ struct Lastfm: Screen<NC::Scrollpad>, Tabbable
virtual bool isMergable() OVERRIDE { return true; } virtual bool isMergable() OVERRIDE { return true; }
// private members template <typename ServiceT>
void Refetch(); bool queueJob(ServiceT service)
{
auto old_service = dynamic_cast<ServiceT *>(m_service.get());
// if the same service and arguments were used, leave old info
if (old_service != nullptr && *old_service == service)
return true;
bool isDownloading() { return isDownloadInProgress && !isReadyToTake; } // download in progress
bool SetArtistInfoArgs(const std::string &artist, const std::string &lang = ""); if (m_worker.valid() && !m_worker.is_ready())
return false;
m_service = std::make_shared<ServiceT>(std::forward<ServiceT>(service));
m_worker = boost::async(boost::launch::async, boost::bind(&LastFm::Service::fetch, m_service.get()));
w.clear();
w << "Fetching information...";
w.flush();
m_title = ToWString(m_service->name());
return true;
}
protected: protected:
virtual bool isLockable() OVERRIDE { return false; } virtual bool isLockable() OVERRIDE { return false; }
private: private:
std::wstring itsTitle; void getResult();
std::string itsArtist; std::wstring m_title;
std::string itsFilename;
std::string itsFolder; std::shared_ptr<LastFm::Service> m_service;
boost::unique_future<LastFm::Service::Result> m_worker;
std::auto_ptr<LastfmService> itsService;
LastfmService::Args itsArgs;
void Load();
void Save(const std::string &data);
void SetTitleAndFolder();
void Download();
static void *DownloadWrapper(void *);
void Take();
bool isReadyToTake;
bool isDownloadInProgress;
pthread_t itsDownloader;
}; };
extern Lastfm *myLastfm; extern Lastfm *myLastfm;

View File

@@ -23,152 +23,190 @@
#ifdef HAVE_CURL_CURL_H #ifdef HAVE_CURL_CURL_H
#include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/trim.hpp>
#include <boost/locale/conversion.hpp>
#include <fstream>
#include "charset.h"
#include "curl_handle.h" #include "curl_handle.h"
#include "settings.h" #include "settings.h"
#include "utility/html.h" #include "utility/html.h"
#include "utility/string.h" #include "utility/string.h"
const char *LastfmService::baseURL = "http://ws.audioscrobbler.com/2.0/?api_key=d94e5b6e26469a2d1ffae8ef20131b79&method="; namespace {
const char *LastfmService::msgParseFailed = "Fetched data could not be parsed"; const char *apiUrl = "http://ws.audioscrobbler.com/2.0/?api_key=d94e5b6e26469a2d1ffae8ef20131b79&method=";
const char *msgInvalidResponse = "Invalid response";
LastfmService::Result LastfmService::fetch(Args &args) }
namespace LastFm {
Service::Result Service::fetch()
{ {
Result result; Result result;
result.first = false;
std::string url = baseURL; std::string url = apiUrl;
url += methodName(); url += methodName();
for (Args::const_iterator it = args.begin(); it != args.end(); ++it) for (auto &arg : m_arguments)
{ {
url += "&"; url += "&";
url += it->first; url += arg.first;
url += "="; url += "=";
url += Curl::escape(it->second); url += Curl::escape(arg.second);
} }
std::string data; std::string data;
CURLcode code = Curl::perform(data, url); CURLcode code = Curl::perform(data, url);
if (code != CURLE_OK) if (code != CURLE_OK)
{
result.second = curl_easy_strerror(code); result.second = curl_easy_strerror(code);
return result; else if (actionFailed(data))
} result.second = msgInvalidResponse;
else
if (actionFailed(data))
{ {
stripHtmlTags(data); result = processData(data);
result.second = data;
return result;
}
if (!parse(data))
{
// if relevant part of data was not found and one of arguments // if relevant part of data was not found and one of arguments
// was language, try to fetch it again without that parameter. // was language, try to fetch it again without that parameter.
// otherwise just report failure. // otherwise just report failure.
Args::iterator lang = args.find("lang"); if (!result.first && !m_arguments["lang"].empty())
if (lang != args.end())
{ {
args.erase(lang); m_arguments.erase("lang");
return fetch(args); result = fetch();
}
else
{
// parse should change data to error msg, if it fails
result.second = data;
return result;
} }
} }
result.first = true;
result.second = data;
return result; return result;
} }
bool LastfmService::actionFailed(const std::string &data) bool Service::actionFailed(const std::string &data)
{ {
return data.find("status=\"failed\"") != std::string::npos; return data.find("status=\"failed\"") != std::string::npos;
} }
void LastfmService::postProcess(std::string &data)
{
stripHtmlTags(data);
boost::trim(data);
}
/***********************************************************************/ /***********************************************************************/
bool ArtistInfo::checkArgs(const Args &args) bool ArtistInfo::argumentsOk()
{ {
return args.find("artist") != args.end(); return !m_arguments["artist"].empty();
} }
void ArtistInfo::colorizeOutput(NC::Scrollpad &w) void ArtistInfo::beautifyOutput(NC::Scrollpad &w)
{ {
w.setProperties(NC::Format::Bold, "\n\nSimilar artists:\n", NC::Format::NoBold, 0, boost::regex::literal); w.setProperties(NC::Format::Bold, "\n\nSimilar artists:\n", NC::Format::NoBold, 0);
w.setProperties(NC::Format::Bold, "\n\nSimilar tags:\n", NC::Format::NoBold, 0);
w.setProperties(Config.color2, "\n * ", NC::Color::End, 0, boost::regex::literal); w.setProperties(Config.color2, "\n * ", NC::Color::End, 0, boost::regex::literal);
} }
bool ArtistInfo::parse(std::string &data) Service::Result ArtistInfo::processData(const std::string &data)
{ {
size_t a, b; size_t a, b;
bool parse_failed = false; Service::Result result;
result.first = false;
if ((a = data.find("<content>")) != std::string::npos) boost::regex rx("<content>(.*?)</content>");
boost::smatch what;
if (boost::regex_search(data, what, rx))
{ {
a += const_strlen("<content>"); std::string desc = what[1];
if ((b = data.find("</content>")) == std::string::npos) // if there is a description...
parse_failed = true; if (desc.length() > 0)
{
// ...locate the link to wiki on last.fm...
rx.assign("<link rel=\"original\" href=\"(.*?)\"");
if (boost::regex_search(data, what, rx))
{
// ...try to get the content of it...
std::string wiki;
CURLcode code = Curl::perform(wiki, what[1]);
if (code != CURLE_OK)
{
result.second = curl_easy_strerror(code);
return result;
}
else
{
// ...and filter it to get the whole description.
rx.assign("<div id=\"wiki\">(.*?)</div>");
if (boost::regex_search(wiki, what, rx))
desc = unescapeHtmlUtf8(what[1]);
}
}
else
{
// otherwise, get rid of CDATA wrapper.
rx.assign("<!\\[CDATA\\[(.*)\\]\\]>");
desc = boost::regex_replace(desc, rx, "\\1");
}
stripHtmlTags(desc);
boost::trim(desc);
result.second += desc;
}
else
result.second += "No description available for this artist.";
} }
else else
parse_failed = true;
if (parse_failed)
{ {
data = msgParseFailed; result.second = msgInvalidResponse;
return false; return result;
} }
if (a == b) auto add_similars = [&result](boost::sregex_iterator &it, const boost::sregex_iterator &last) {
for (; it != last; ++it)
{
std::string name = it->str(1);
std::string url = it->str(2);
stripHtmlTags(name);
stripHtmlTags(url);
result.second += "\n * ";
result.second += name;
result.second += " (";
result.second += url;
result.second += ")";
}
};
a = data.find("<similar>");
b = data.find("</similar>");
if (a != std::string::npos && b != std::string::npos)
{ {
data = "No description available for this artist."; rx.assign("<artist>.*?<name>(.*?)</name>.*?<url>(.*?)</url>.*?</artist>");
return false; auto it = boost::sregex_iterator(data.begin()+a, data.begin()+b, rx);
auto last = boost::sregex_iterator();
if (it != last)
result.second += "\n\nSimilar artists:\n";
add_similars(it, last);
} }
std::vector< std::pair<std::string, std::string> > similars; a = data.find("<tags>");
for (size_t i = data.find("<name>"), j, k = data.find("<url>"), l; b = data.find("</tags>");
i != std::string::npos; i = data.find("<name>", i), k = data.find("<url>", k)) if (a != std::string::npos && b != std::string::npos)
{ {
j = data.find("</name>", i); rx.assign("<tag>.*?<name>(.*?)</name>.*?<url>(.*?)</url>.*?</tag>");
i += const_strlen("<name>"); auto it = boost::sregex_iterator(data.begin()+a, data.begin()+b, rx);
auto last = boost::sregex_iterator();
l = data.find("</url>", k); if (it != last)
k += const_strlen("<url>"); result.second += "\n\nSimilar tags:\n";
add_similars(it, last);
similars.push_back(std::make_pair(data.substr(i, j-i), data.substr(k, l-k)));
stripHtmlTags(similars.back().first);
} }
a += const_strlen("<![CDATA["); // get artist we look for, it's the one before similar artists
b -= const_strlen("]]>"); rx.assign("<name>.*?</name>.*?<url>(.*?)</url>.*?<similar>");
data = data.substr(a, b-a);
postProcess(data); if (boost::regex_search(data, what, rx))
data += "\n\nSimilar artists:\n";
for (size_t i = 1; i < similars.size(); ++i)
{ {
data += "\n * "; std::string url = what[1];
data += similars[i].first; stripHtmlTags(url);
data += " ("; result.second += "\n\n";
data += similars[i].second; // add only url
data += ")"; result.second += url;
} }
data += "\n\n";
data += similars.front().second;
return true; result.first = true;
return result;
}
} }
#endif #endif

View File

@@ -30,42 +30,51 @@
#include "scrollpad.h" #include "scrollpad.h"
struct LastfmService namespace LastFm {
struct Service
{ {
typedef std::map<std::string, std::string> Args; typedef std::map<std::string, std::string> Arguments;
typedef std::pair<bool, std::string> Result; typedef std::pair<bool, std::string> Result;
virtual const char *name() = 0; Service(Arguments args) : m_arguments(args) { }
virtual Result fetch(Args &args);
virtual bool checkArgs(const Args &args) = 0; virtual const char *name() = 0;
virtual void colorizeOutput(NC::Scrollpad &w) = 0; virtual Result fetch();
virtual void beautifyOutput(NC::Scrollpad &w) = 0;
protected: protected:
virtual bool argumentsOk() = 0;
virtual bool actionFailed(const std::string &data); virtual bool actionFailed(const std::string &data);
virtual bool parse(std::string &data) = 0; virtual Result processData(const std::string &data) = 0;
virtual void postProcess(std::string &data);
virtual const char *methodName() = 0; virtual const char *methodName() = 0;
static const char *baseURL; Arguments m_arguments;
static const char *msgParseFailed;
}; };
struct ArtistInfo : public LastfmService struct ArtistInfo : public Service
{ {
ArtistInfo(std::string artist, std::string lang)
: Service({{"artist", artist}, {"lang", lang}}) { }
virtual const char *name() { return "Artist info"; } virtual const char *name() { return "Artist info"; }
virtual bool checkArgs(const Args &args); virtual void beautifyOutput(NC::Scrollpad &w);
virtual void colorizeOutput(NC::Scrollpad &w);
bool operator==(const ArtistInfo &ai) const { return m_arguments == ai.m_arguments; }
protected: protected:
virtual bool parse(std::string &data); virtual bool argumentsOk();
virtual Result processData(const std::string &data);
virtual const char *methodName() { return "artist.getinfo"; } virtual const char *methodName() { return "artist.getinfo"; }
}; };
}
#endif // HAVE_CURL_CURL_H #endif // HAVE_CURL_CURL_H
#endif // NCMPCPP_LASTFM_SERVICE_H #endif // NCMPCPP_LASTFM_SERVICE_H