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
#
#def_key "`"
# refetch_artist_info
#
#def_key "`"
# add_random_items
#
#def_key "ctrl_p"

View File

@@ -2072,22 +2072,6 @@ void RefetchLyrics::run()
# 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
{
if (Mpd.Version() < 17)
@@ -2161,7 +2145,7 @@ bool ShowArtistInfo::canBeRun() const
void ShowArtistInfo::run()
{
# ifdef HAVE_CURL_CURL_H
if (myScreen == myLastfm || myLastfm->isDownloading())
if (myScreen == myLastfm)
{
myLastfm->switchTo();
return;
@@ -2181,8 +2165,11 @@ void ShowArtistInfo::run()
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();
}
# endif // HAVE_CURL_CURL_H
}
@@ -2540,7 +2527,6 @@ void populateActions()
insert_action(new Actions::ToggleLibraryTagType());
insert_action(new Actions::ToggleMediaLibrarySortMode());
insert_action(new Actions::RefetchLyrics());
insert_action(new Actions::RefetchArtistInfo());
insert_action(new Actions::SetSelectedItemsPriority());
insert_action(new Actions::FilterPlaylistOnPriorities());
insert_action(new Actions::ShowSongInfo());

View File

@@ -942,15 +942,6 @@ protected:
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
{
SetSelectedItemsPriority()

View File

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

View File

@@ -22,27 +22,12 @@
#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 "charset.h"
#include "global.h"
#include "statusbar.h"
#include "title.h"
#include "screen_switcher.h"
#include "utility/string.h"
using Global::MainHeight;
using Global::MainStartY;
@@ -51,7 +36,6 @@ Lastfm *myLastfm;
Lastfm::Lastfm()
: Screen(NC::Scrollpad(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border::None))
, isReadyToTake(0), isDownloadInProgress(0)
{ }
void Lastfm::resize()
@@ -65,23 +49,13 @@ void Lastfm::resize()
std::wstring Lastfm::title()
{
return itsTitle;
return m_title;
}
void Lastfm::update()
{
if (isReadyToTake)
Take();
}
void Lastfm::Take()
{
assert(isReadyToTake);
pthread_join(itsDownloader, 0);
w.flush();
w.refresh();
isDownloadInProgress = 0;
isReadyToTake = 0;
if (m_worker.valid() && m_worker.is_ready())
getResult();
}
void Lastfm::switchTo()
@@ -90,134 +64,27 @@ void Lastfm::switchTo()
if (myScreen != this)
{
SwitchTo::execute(this);
// get an old info if it waits
if (isReadyToTake)
Take();
Load();
drawHeader();
}
else
switchToPreviousScreen();
}
void Lastfm::Load()
void Lastfm::getResult()
{
if (isDownloadInProgress)
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);
auto result = m_worker.get();
if (result.first)
{
Save(result.second);
w.clear();
w << Charset::utf8ToLocale(result.second);
itsService->colorizeOutput(w);
m_service->beautifyOutput(w);
}
else
w << NC::Color::Red << result.second << NC::Color::End;
isReadyToTake = 1;
}
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;
w << " " << NC::Color::Red << result.second << NC::Color::End;
w.flush();
w.refresh();
// reset m_worker so it's no longer valid
m_worker = boost::unique_future<LastFm::Service::Result>();
}
#endif // HVAE_CURL_CURL_H

View File

@@ -26,11 +26,12 @@
#ifdef HAVE_CURL_CURL_H
#include <memory>
#include <pthread.h>
#include <boost/thread/future.hpp>
#include "interfaces.h"
#include "lastfm_service.h"
#include "screen.h"
#include "utility/wide_string.h"
struct Lastfm: Screen<NC::Scrollpad>, Tabbable
{
@@ -49,37 +50,39 @@ struct Lastfm: Screen<NC::Scrollpad>, Tabbable
virtual bool isMergable() OVERRIDE { return true; }
// private members
void Refetch();
bool isDownloading() { return isDownloadInProgress && !isReadyToTake; }
bool SetArtistInfoArgs(const std::string &artist, const std::string &lang = "");
template <typename ServiceT>
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;
// download in progress
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:
virtual bool isLockable() OVERRIDE { return false; }
private:
std::wstring itsTitle;
void getResult();
std::string itsArtist;
std::string itsFilename;
std::wstring m_title;
std::string itsFolder;
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;
std::shared_ptr<LastFm::Service> m_service;
boost::unique_future<LastFm::Service::Result> m_worker;
};
extern Lastfm *myLastfm;

View File

@@ -23,152 +23,190 @@
#ifdef HAVE_CURL_CURL_H
#include <boost/algorithm/string/trim.hpp>
#include <boost/locale/conversion.hpp>
#include <fstream>
#include "charset.h"
#include "curl_handle.h"
#include "settings.h"
#include "utility/html.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.first = false;
std::string url = baseURL;
std::string url = apiUrl;
url += methodName();
for (Args::const_iterator it = args.begin(); it != args.end(); ++it)
for (auto &arg : m_arguments)
{
url += "&";
url += it->first;
url += arg.first;
url += "=";
url += Curl::escape(it->second);
url += Curl::escape(arg.second);
}
std::string data;
CURLcode code = Curl::perform(data, url);
if (code != CURLE_OK)
{
result.second = curl_easy_strerror(code);
return result;
}
if (actionFailed(data))
{
stripHtmlTags(data);
result.second = data;
return result;
}
if (!parse(data))
else if (actionFailed(data))
result.second = msgInvalidResponse;
else
{
result = processData(data);
// if relevant part of data was not found and one of arguments
// was language, try to fetch it again without that parameter.
// otherwise just report failure.
Args::iterator lang = args.find("lang");
if (lang != args.end())
if (!result.first && !m_arguments["lang"].empty())
{
args.erase(lang);
return fetch(args);
}
else
{
// parse should change data to error msg, if it fails
result.second = data;
return result;
m_arguments.erase("lang");
result = fetch();
}
}
result.first = true;
result.second = data;
return result;
}
bool LastfmService::actionFailed(const std::string &data)
bool Service::actionFailed(const std::string &data)
{
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);
}
bool ArtistInfo::parse(std::string &data)
Service::Result ArtistInfo::processData(const std::string &data)
{
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>");
if ((b = data.find("</content>")) == std::string::npos)
parse_failed = true;
std::string desc = what[1];
// if there is a description...
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
parse_failed = true;
if (parse_failed)
{
data = msgParseFailed;
return false;
result.second = msgInvalidResponse;
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.";
return false;
rx.assign("<artist>.*?<name>(.*?)</name>.*?<url>(.*?)</url>.*?</artist>");
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;
for (size_t i = data.find("<name>"), j, k = data.find("<url>"), l;
i != std::string::npos; i = data.find("<name>", i), k = data.find("<url>", k))
a = data.find("<tags>");
b = data.find("</tags>");
if (a != std::string::npos && b != std::string::npos)
{
j = data.find("</name>", i);
i += const_strlen("<name>");
l = data.find("</url>", k);
k += const_strlen("<url>");
similars.push_back(std::make_pair(data.substr(i, j-i), data.substr(k, l-k)));
stripHtmlTags(similars.back().first);
rx.assign("<tag>.*?<name>(.*?)</name>.*?<url>(.*?)</url>.*?</tag>");
auto it = boost::sregex_iterator(data.begin()+a, data.begin()+b, rx);
auto last = boost::sregex_iterator();
if (it != last)
result.second += "\n\nSimilar tags:\n";
add_similars(it, last);
}
a += const_strlen("<![CDATA[");
b -= const_strlen("]]>");
data = data.substr(a, b-a);
// get artist we look for, it's the one before similar artists
rx.assign("<name>.*?</name>.*?<url>(.*?)</url>.*?<similar>");
postProcess(data);
data += "\n\nSimilar artists:\n";
for (size_t i = 1; i < similars.size(); ++i)
if (boost::regex_search(data, what, rx))
{
data += "\n * ";
data += similars[i].first;
data += " (";
data += similars[i].second;
data += ")";
std::string url = what[1];
stripHtmlTags(url);
result.second += "\n\n";
// add only url
result.second += url;
}
data += "\n\n";
data += similars.front().second;
return true;
result.first = true;
return result;
}
}
#endif

View File

@@ -30,42 +30,51 @@
#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;
virtual const char *name() = 0;
virtual Result fetch(Args &args);
Service(Arguments args) : m_arguments(args) { }
virtual bool checkArgs(const Args &args) = 0;
virtual void colorizeOutput(NC::Scrollpad &w) = 0;
virtual const char *name() = 0;
virtual Result fetch();
virtual void beautifyOutput(NC::Scrollpad &w) = 0;
protected:
virtual bool argumentsOk() = 0;
virtual bool actionFailed(const std::string &data);
virtual bool parse(std::string &data) = 0;
virtual void postProcess(std::string &data);
virtual Result processData(const std::string &data) = 0;
virtual const char *methodName() = 0;
static const char *baseURL;
static const char *msgParseFailed;
Arguments m_arguments;
};
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 bool checkArgs(const Args &args);
virtual void colorizeOutput(NC::Scrollpad &w);
virtual void beautifyOutput(NC::Scrollpad &w);
bool operator==(const ArtistInfo &ai) const { return m_arguments == ai.m_arguments; }
protected:
virtual bool parse(std::string &data);
virtual bool argumentsOk();
virtual Result processData(const std::string &data);
virtual const char *methodName() { return "artist.getinfo"; }
};
}
#endif // HAVE_CURL_CURL_H
#endif // NCMPCPP_LASTFM_SERVICE_H