From ccb7d14a8b1ad6d80308c14a3f0b209d66c3c88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Ankarstr=C3=B6m?= Date: Wed, 24 Aug 2022 03:01:10 +0200 Subject: Add FetchScreenwriters. Improve thread communication. --- c/data.cpp | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 13 deletions(-) (limited to 'c/data.cpp') diff --git a/c/data.cpp b/c/data.cpp index 7722cd0..86da266 100644 --- a/c/data.cpp +++ b/c/data.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -5,8 +6,14 @@ #include #include "data.h" +#include "episodelistview.h" #include "win.h" +/* Signals for thread communication. */ +constexpr unsigned char SIG_READY = 1<<0; +constexpr unsigned char SIG_DONE = 1<<1; +constexpr unsigned char SIG_ABORT = 1<<2; + struct XmlError : public std::exception { const char* msg; @@ -20,14 +27,17 @@ struct XmlError : public std::exception } }; +static inline void XmlFree(void* p) { xmlFree(p); } + +/* RAII types for WinINet and libxml2. */ +using InternetHandle = Managed; using XmlXPathContextPtr = Managed; using XmlXPathObjectPtr = Managed; -static inline void XmlFree(void* p) { xmlFree(p); } using XmlCharPtr = Managed; -using InternetHandle = Managed; static InternetHandle s_hi = InternetOpen(L"Episode Browser", INTERNET_OPEN_TYPE_DIRECT, nullptr, nullptr, 0); +/* ParsedDoc downloads and parses an HTML document. */ struct ParsedDoc { using HtmlParserCtxtPtr = Managed; @@ -83,7 +93,7 @@ bool WcharsFromXmlchars(wchar_t (&dst)[N], XmlCharPtr utf8) noexcept return MultiByteToWideChar(CP_UTF8, 0, src, cchNarrow, dst, cchWide); } -void FetchData() +void FetchData(unsigned char* sig) { /* The remote data is retrieved using WinINet from the * Detective Conan World wiki. Using libxml2's "push parser", @@ -94,20 +104,23 @@ void FetchData() ParsedDoc doc(L"https://www.detectiveconanworld.com/wiki/Anime", "https://www.detectiveconanworld.com/wiki/Anime"); + XmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); XmlXPathObjectPtr xpathObj = xmlXPathEvalExpression( reinterpret_cast("//tr[./td[1] != '' and ./td[3][@style='background:#f2fde9;']]"), xpathCtx); xmlNodeSetPtr nodes = xpathObj->nodesetval; - int cNodes = nodes? nodes->nodeNr: 0; - if (!cNodes) + if (!nodes || !nodes->nodeNr) throw std::runtime_error("could not find remote episode information"); - for (int i = 0; i < cNodes; i++) { + for (int i = 0; i < nodes->nodeNr; i++) { extern FileView g_fvElv; extern FileView g_fvDlv; + if (*sig & SIG_ABORT) + return; + const xmlNodePtr node = nodes->nodeTab[i]; if (xmlChildElementCount(node) != 8) throw std::runtime_error("unexpected remote data format"); @@ -142,13 +155,105 @@ void FetchData() } } -void WaitFetchData(bool* bDone) noexcept +void FetchScreenwriters(unsigned char* sig) +{ + extern FileView g_fvDlv; + extern CfgA& g_cfg; + + static int iLast = -1; + int iMax = g_cfg.cEp-1; + + /* Find the last fetched screenwriter. */ + if (iLast == -1) + for (size_t i = 0; i < g_fvDlv.c; i++) + if (const DlvDataA& d = g_fvDlv[i]; !d.date[0]) { + iMax = i-1; + break; + } else if (d.screenwriter[0]) + iLast = i; + + FINALLY { Status(L""); }; + + /* Fetch screenwriters for the rest of the episodes. */ + const wchar_t prefix[] = L"https://www.detectiveconanworld.com"; + wchar_t url[256]; + Wcscpy(url, prefix); + for (iLast++; iLast < iMax; iLast++) { + if (*sig & SIG_ABORT) + return; + + wchar_t msg[48]; + Swprintf(msg, L"Fetching screenwriter for episode %d...", iLast+1); + Status(msg); + + /* Retrieve URL for episode's wiki page. */ + DlvDataA& d = g_fvDlv[iLast]; + Wcscpy(Buf(url)+Len(prefix), d.wiki); + + /* Retrieve screenwriter from HTML. */ + ParsedDoc doc(url, nullptr); + XmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); + XmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(reinterpret_cast( + "//th[contains(text(), 'Screenplay:')]/following-sibling::td"), + xpathCtx); + xmlNodeSetPtr nodes = xpathObj->nodesetval; + if (!nodes || !nodes->nodeNr) + continue; + xmlChar* s = xmlNodeGetContent(nodes->nodeTab[0]); + WcharsFromXmlchars(d.screenwriter, s); + } +} + +void WaitFor(void (*f)(unsigned char*)) { - try { - FetchData(); - *bDone = true; - } catch (...) { - *bDone = true; - ShowException(L"Remote data could not be fetched due to an error: %s", L"Error", MB_ICONWARNING); + static unsigned char sig = SIG_READY; + static UINT_PTR iTimer = 0; + + /* The timer procedure animates an ellipsis in the status bar + * while the thread is running. */ + static auto procTimer = [](HWND, UINT, UINT_PTR, DWORD) -> void + { + static int i = 0; + static const wchar_t* text[] = {L".", L"..", L"...", L""}; + + if (sig & SIG_DONE) { + extern EpisodeListView* const g_elv; + KillTimer(nullptr, iTimer); + i = 0; + sig |= SIG_READY; + g_elv->Update(); /* Reset status bar. */ + } else { + i = (i+1)%(sizeof(text)/sizeof(*text)); + Status(text[i], 1); + } + }; + + static auto procThread = [](void (*f)(unsigned char*)) noexcept -> void + { + while (!(sig & SIG_READY)) + Sleep(100); + sig &= ~SIG_READY; + try { + f(&sig); + sig |= SIG_DONE; + } catch (...) { + sig |= SIG_DONE; + ShowException(L"Remote data could not be fetched due to an error: %s", + L"Error", MB_ICONWARNING); + } + }; + + /* Ensure that only a single thread is waited on. */ + if (!(sig & SIG_READY)) { + if (EBMessageBox(L"Another task is active. " + L"Do you want to cancel the existing task and start a new one?", + L"Error", MB_YESNO|MB_ICONWARNING) == IDYES) + sig |= SIG_ABORT; + else + return; } + + std::thread(procThread, f).detach(); + Status(L".", 1); + Prefer(iTimer = SetTimer(nullptr, iTimer, 500, procTimer)); } -- cgit v1.2.3