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 +++++++++++++++++++++++++++++++++++++++++++++----- c/data.h | 22 ++++++--- c/episodelistview.cpp | 7 +-- c/main.cpp | 55 ++------------------- c/test.cpp | 2 +- c/util.h | 53 +++++++++++++------- c/win.cpp | 1 - c/win.h | 12 +++-- 8 files changed, 184 insertions(+), 99 deletions(-) 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)); } diff --git a/c/data.h b/c/data.h index 14e8920..bf45511 100644 --- a/c/data.h +++ b/c/data.h @@ -10,8 +10,11 @@ #include "win.h" /* Fetch data from the web. */ -void FetchData(); -void WaitFetchData(bool* bDone) noexcept; +void FetchData(unsigned char* sig); +void FetchScreenwriters(unsigned char* sig); + +/* Wait for thread. */ +void WaitFor(void (*f)(unsigned char*)); /* The structs ending with A are written as-is to disk. As such, they * should be regarded as immutable. If the format needs to be changed @@ -54,7 +57,7 @@ struct CfgA unsigned short cEp = 4096; unsigned short iFocus = 0; unsigned short heightDlv = 0; - wchar_t limitScreenwriter[64] = {0}; + wchar_t limitScreenwriter[64] = {0}; /* TODO: 64 -> 48 */ wchar_t root[260] = {0}; wchar_t glob[64] = {0}; wchar_t url[192] = {0}; @@ -76,6 +79,11 @@ constexpr inline bool HasVersion = true; template struct FileView { + HANDLE hf; + HANDLE hm; + T* view; + size_t c; + static FileView Initialized(const wchar_t* filename, size_t c, size_t cInit = 0) { bool bExisting = GetFileAttributes(filename) != INVALID_FILE_ATTRIBUTES; @@ -150,6 +158,9 @@ struct FileView return t; } + T* begin() { return *view; } + T* end() { return view[c-1]; } + inline operator T*() noexcept { return view; @@ -159,11 +170,6 @@ struct FileView { return view; } - - HANDLE hf; - HANDLE hm; - T* view; - size_t c; }; inline int FromWeb(const int iEp, ElvDataA& e, DlvDataA& d) noexcept diff --git a/c/episodelistview.cpp b/c/episodelistview.cpp index 645c11f..cadd751 100644 --- a/c/episodelistview.cpp +++ b/c/episodelistview.cpp @@ -281,7 +281,9 @@ void EpisodeListView::SelectUnwatched(int dir) iEpNew = lvfi.lParam; for (;;) { iEpNew += dir; - if (iEpNew == 0 || iEpNew == g_cfg.cEp || !g_fvElv[iEpNew-1].siEp[0]) + if (iEpNew == 0 + || static_cast(iEpNew) > g_fvElv.c + || !g_fvElv[iEpNew-1].siEp[0]) return; if (!g_fvElv[iEpNew-1].bWatched) break; @@ -421,10 +423,9 @@ void EpisodeListView::Update() } /* Show number of displayed items in status bar. */ - extern HWND g_hWndStatus; wchar_t disp[16]; Swprintf(disp, L"%d", cItem); - SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), reinterpret_cast(disp)); + Status(disp, 1); } Sort(); diff --git a/c/main.cpp b/c/main.cpp index 771b323..8916860 100644 --- a/c/main.cpp +++ b/c/main.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -77,8 +76,6 @@ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); static LRESULT CALLBACK HandleMsg(HWND, UINT, WPARAM, LPARAM); /* Process main menu commands. */ static void HandleMainMenu(HWND, WORD); -/* Wait for thread. */ -void WaitFor(void (*f)(bool*)); /* Handle messages to Help > About dialog. */ static INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM); /* Try to style application according to current Windows theme. */ @@ -353,7 +350,7 @@ LRESULT CALLBACK HandleMsg(const HWND hWnd, const UINT uMsg, const WPARAM wParam const wchar_t** const vTip = group == IDG_MENU? vTipMenu: vTipCtx; tip = vTip[ID_INDEX(command)]; } - SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(0,0), reinterpret_cast(tip)); + Status(tip, 0); return 0; } @@ -390,12 +387,12 @@ void HandleMainMenu(const HWND hWnd, const WORD command) case IDM_FILE_FETCH_DATA: { - WaitFor(WaitFetchData); + WaitFor(FetchData); break; } case IDM_FILE_FETCH_SCREENWRITERS: - //WaitFor("episode_data","update_screenwriters"); + WaitFor(FetchScreenwriters); break; case IDM_FILE_ABOUT: @@ -442,52 +439,6 @@ void HandleMainMenu(const HWND hWnd, const WORD command) } } -void WaitFor(void (*f)(bool*)) -{ - static bool bActive = false; - static bool bDone = false; - static UINT_PTR iTimer; - - /* Ensure that only a single thread is waited on. */ - if (bActive) { - 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) { - KillTimer(nullptr, iTimer); - bActive = false; - g_elv->Update(); - } else - return; - } - - /* The timer procedure animates an ellipsis in the status bar - * while the thread is running. */ - static auto proc = [](HWND, UINT, UINT_PTR, DWORD) -> void - { - static int i = 0; - static const wchar_t* text[] = {L".", L"..", L"...", L""}; - - if (bDone) { - KillTimer(nullptr, iTimer); - i = 0; - bActive = 0; - g_elv->Update(); - } else { - i = (i+1)%(sizeof(text)/sizeof(*text)); - SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), - reinterpret_cast(text[i])); - } - }; - - /* The waited-on function signals its completion by setting a - * shared boolean value to true. */ - bDone = false; - bActive = true; - std::thread(f, &bDone).detach(); - SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), reinterpret_cast(L".")); - Prefer(iTimer = SetTimer(nullptr, -1, 500, proc)); -} - INT_PTR CALLBACK AboutDlgProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM) { switch (uMsg) { diff --git a/c/test.cpp b/c/test.cpp index e56d7aa..4c7c7ea 100644 --- a/c/test.cpp +++ b/c/test.cpp @@ -173,7 +173,7 @@ int RunTests() //IO(), //MigrateElvDataFromPrologToDisk(), //MigrateDlvDataFromPrologToDisk(), - //MigrateCfg() + //MigrateCfg(), }; printf("Results (%llu tests):\n", sizeof(tests)/sizeof(*tests)); diff --git a/c/util.h b/c/util.h index 7c2b293..7f704df 100644 --- a/c/util.h +++ b/c/util.h @@ -11,17 +11,14 @@ #define CONCAT(a, b) CONCAT_IMPL(a, b) #define _ CONCAT(unused_, __LINE__) -inline size_t Min(size_t a, size_t b) +template +struct Finally { - return a < b? a: b; -} - -inline int Cmp(const int a, const int b) -{ - if (a == b) return 0; - if (a > b) return 1; - return -1; -} + F f; + inline Finally(F f) : f(f) {} + inline ~Finally() { f(); } +}; +#define FINALLY Finally _ = [=]() /* Generic RAII type. */ template @@ -45,33 +42,53 @@ template struct Buf { T* data; - size_t size; - Buf(T* data, size_t size) : data(data), size(size) {} - Buf(std::basic_string& s) : data(s.data()), size(s.capacity()) {} - template Buf(T (&data)[N]) : data(data), size(N) {} + size_t c; + Buf(T* data, size_t c) : data(data), c(c) {} + Buf(std::basic_string& s) : data(s.data()), c(s.capacity()) {} + template Buf(T (&data)[N]) : data(data), c(N) {} operator T*() { return data; } T& operator *() { return *data; } T& operator [](size_t i) { return data[i]; } + Buf operator +(size_t i) { return {data+i, c-i}; } + //T operator -(size_t i) { return {data-i, c+i}; } }; +inline int Cmp(const int a, const int b) +{ + if (a == b) return 0; + if (a > b) return 1; + return -1; +} + +inline size_t Min(size_t a, size_t b) +{ + return a < b? a: b; +} + +template +inline size_t Len(T (&)[N]) +{ + return N-1; +} + /* Format wide string. */ template inline int Swprintf(Buf buf, const wchar_t* const fmt, T... xs) { - return _snwprintf_s(buf, buf.size, _TRUNCATE, fmt, xs...); + return _snwprintf_s(buf, buf.c, _TRUNCATE, fmt, xs...); } /* Format static narrow string. */ template inline int Sprintf(Buf buf, const char* const fmt, T... xs) { - return _snprintf_s(buf, buf.size, _TRUNCATE, fmt, xs...); + return _snprintf_s(buf, buf.c, _TRUNCATE, fmt, xs...); } /* Copy to static wide string buffer. */ inline wchar_t* Wcscpy(Buf dst, const wchar_t* const src) { - const size_t len = Min(dst.size, wcslen(src)+1); + const size_t len = Min(dst.c, wcslen(src)+1); memcpy(dst, src, len*sizeof(wchar_t)); dst[len-1] = 0; return dst; @@ -80,7 +97,7 @@ inline wchar_t* Wcscpy(Buf dst, const wchar_t* const src) /* Copy to static narrow string buffer. */ inline char* Strcpy(Buf dst, const char* const src) { - const size_t len = Min(dst.size, strlen(src)+1); + const size_t len = Min(dst.c, strlen(src)+1); memcpy(dst, src, len); dst[len-1] = 0; return dst; diff --git a/c/win.cpp b/c/win.cpp index e8c429d..ba42508 100644 --- a/c/win.cpp +++ b/c/win.cpp @@ -107,7 +107,6 @@ void ShowException(const wchar_t* const fmt, const wchar_t* const title, const U Swprintf(msg, fmt, what); EBMessageBox(msg, title, uType); } - } int GetRelativeCursorPos(const HWND hWnd, POINT* const pt) noexcept diff --git a/c/win.h b/c/win.h index 62cb2ac..834aaec 100644 --- a/c/win.h +++ b/c/win.h @@ -4,6 +4,7 @@ #include #include #include +#include /* Run given procedure at creation of next window. */ void WithNextWindow(void (*proc)(HWND)); @@ -125,9 +126,8 @@ inline BOOL SetWindowRect(const HWND hWnd, const RECT r) return SetWindowRect(hWnd, r.left, r.top, r.right, r.bottom); } -/* The following functions call uxtheme.dll functions, if uxtheme.dll - * exists, and otherwise do nothing. */ - +/* The following two functions call uxtheme.dll functions, if + * uxtheme.dll exists, and otherwise do nothing. */ inline BOOL EBIsThemeActive() { extern BOOL (*IsThemeActive)(); @@ -141,4 +141,10 @@ inline BOOL EBSetWindowTheme(const HWND hWnd, const wchar_t* const pszSubAppName return SetWindowTheme? SetWindowTheme(hWnd, pszSubAppName, pszSubIdList): 0; } +inline void Status(const wchar_t* msg, unsigned short i = 0) +{ + extern HWND g_hWndStatus; + SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(i,0), reinterpret_cast(msg)); +} + #endif -- cgit v1.2.3