diff options
author | Andrzej Rybczak <electricityispower@gmail.com> | 2013-07-10 17:20:09 +0200 |
---|---|---|
committer | Andrzej Rybczak <electricityispower@gmail.com> | 2013-07-10 19:25:32 +0200 |
commit | 1710f892c5942bfe09f06672aaedad3eab4cd538 (patch) | |
tree | a3d4ace8fec6f9bb7e7da5d974d06d71bf5f65cb | |
parent | 0092dfe044a40ae10c8a765d18f35bf6dd44e3e4 (diff) |
lastfm: rework service architecture a bit and implement getting full artist info
-rw-r--r-- | doc/bindings | 3 | ||||
-rw-r--r-- | src/actions.cpp | 24 | ||||
-rw-r--r-- | src/actions.h | 9 | ||||
-rw-r--r-- | src/curl_handle.h | 4 | ||||
-rw-r--r-- | src/lastfm.cpp | 155 | ||||
-rw-r--r-- | src/lastfm.h | 53 | ||||
-rw-r--r-- | src/lastfm_service.cpp | 202 | ||||
-rw-r--r-- | src/lastfm_service.h | 35 |
8 files changed, 187 insertions, 298 deletions
diff --git a/doc/bindings b/doc/bindings index 679206ef..f4b846cd 100644 --- a/doc/bindings +++ b/doc/bindings @@ -505,9 +505,6 @@ # refetch_lyrics # #def_key "`" -# refetch_artist_info -# -#def_key "`" # add_random_items # #def_key "ctrl_p" diff --git a/src/actions.cpp b/src/actions.cpp index daaf8004..27c51bc6 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -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()); diff --git a/src/actions.h b/src/actions.h index afb53823..42112685 100644 --- a/src/actions.h +++ b/src/actions.h @@ -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() diff --git a/src/curl_handle.h b/src/curl_handle.h index e1b3eea5..072a3509 100644 --- a/src/curl_handle.h +++ b/src/curl_handle.h @@ -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 diff --git a/src/lastfm.cpp b/src/lastfm.cpp index f6ff5004..e9baabca 100644 --- a/src/lastfm.cpp +++ b/src/lastfm.cpp @@ -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() -{ - 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) +void Lastfm::getResult() { - 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); - } - 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(); + m_service->beautifyOutput(w); } 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 - diff --git a/src/lastfm.h b/src/lastfm.h index 6f81257b..ceda5a70 100644 --- a/src/lastfm.h +++ b/src/lastfm.h @@ -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; - - std::string itsArtist; - std::string itsFilename; - - std::string itsFolder; - - std::auto_ptr<LastfmService> itsService; - LastfmService::Args itsArgs; - - void Load(); - void Save(const std::string &data); - void SetTitleAndFolder(); + void getResult(); - void Download(); - static void *DownloadWrapper(void *); + std::wstring m_title; - 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; diff --git a/src/lastfm_service.cpp b/src/lastfm_service.cpp index d0b11f52..87ddf729 100644 --- a/src/lastfm_service.cpp +++ b/src/lastfm_service.cpp @@ -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); - - postProcess(data); + // get artist we look for, it's the one before similar artists + rx.assign("<name>.*?</name>.*?<url>(.*?)</url>.*?<similar>"); - 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 diff --git a/src/lastfm_service.h b/src/lastfm_service.h index 3aa10b8e..a5c439b7 100644 --- a/src/lastfm_service.h +++ b/src/lastfm_service.h @@ -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; + Service(Arguments args) : m_arguments(args) { } + virtual const char *name() = 0; - virtual Result fetch(Args &args); + virtual Result fetch(); - virtual bool checkArgs(const Args &args) = 0; - virtual void colorizeOutput(NC::Scrollpad &w) = 0; + 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 |