# HG changeset patch # User Paper # Date 1762417015 18000 # Node ID 0265e125f68010fffac39ad5884660dfadb1241f # Parent 5beae59cf042a8119788c6b2adfdb58522e92d75 filesystem: implement filesystem watcher I also ported the library code to use it as well. Once we implement proper directory watching on Windows (and maybe others) this will be fairly useful :) diff -r 5beae59cf042 -r 0265e125f680 include/core/filesystem.h --- a/include/core/filesystem.h Thu Nov 06 01:17:24 2025 -0500 +++ b/include/core/filesystem.h Thu Nov 06 03:16:55 2025 -0500 @@ -2,6 +2,7 @@ #define MINORI_CORE_FILESYSTEM_H_ #include #include +#include namespace Filesystem { @@ -12,6 +13,29 @@ std::filesystem::path GetTorrentsPath(); // (dotpath)/torrents/... std::filesystem::path GetAnimePostersPath(); // (dotpath)/anime/posters/ +/* ------------------------------------------------------------------------ */ +/* Filesystem watcher interface. This is implemented differently on + * different platforms :) */ + +struct IWatcher { + enum Event { + /* File/directory 'path' was created */ + Created, + /* File/directory 'path' was deleted */ + Deleted, + }; + + using EventHandler = std::function; + + virtual ~IWatcher() = default; + virtual void Process() = 0; +}; + +/* Constructor functions. Yes, I'm doing this the C way :) */ +IWatcher *GetRecursiveFilesystemWatcher(void *opaque, + const std::filesystem::path &path, IWatcher::EventHandler handler); +IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler); + } // namespace Filesystem #endif // MINORI_CORE_FILESYSTEM_H_ diff -r 5beae59cf042 -r 0265e125f680 include/library/library.h --- a/include/library/library.h Thu Nov 06 01:17:24 2025 -0500 +++ b/include/library/library.h Thu Nov 06 03:16:55 2025 -0500 @@ -2,6 +2,7 @@ #define MINORI_LIBRARY_LIBRARY_H_ #include "library/library.h" +#include "core/filesystem.h" #include #include @@ -11,6 +12,16 @@ class Database final { public: + Database(); + + /* Update watchers from current library paths */ + void UpdateWatchers(); + + bool GetPathAnimeAndEpisode(const std::string &basename, int *aid, int *ep); + + void EventHandler(const std::filesystem::path &path, Filesystem::IWatcher::Event event); + static void StaticEventHandler(void *opaque, const std::filesystem::path &path, Filesystem::IWatcher::Event event); + std::optional GetAnimeFolder(int id); void Refresh(); void Refresh(int id); @@ -20,6 +31,11 @@ private: void Refresh(std::optional find_id); + + std::unordered_map> watchers_; + + /* ID we're looking for */ + std::optional find_id_; }; extern Database db; diff -r 5beae59cf042 -r 0265e125f680 src/core/filesystem.cc --- a/src/core/filesystem.cc Thu Nov 06 01:17:24 2025 -0500 +++ b/src/core/filesystem.cc Thu Nov 06 03:16:55 2025 -0500 @@ -56,4 +56,118 @@ return GetDotPath() / "anime" / "posters"; } +/* ------------------------------------------------------------------------ */ +/* Ehhhhh */ + +class Watcher : public IWatcher { +public: + Watcher(void *opaque, const std::filesystem::path &path, EventHandler handler) + : path_(path) + , handler_(handler) + , opaque_(opaque) + { + } + + virtual ~Watcher() override + { + } + +protected: + std::filesystem::path path_; + EventHandler handler_; + void *opaque_; +}; + +/* ------------------------------------------------------------------------ */ +/* Non-recursive filesystem watcher. + * This is the portable version for non-Windows */ + +template +class StdFilesystemWatcher : public Watcher { +public: + StdFilesystemWatcher(void *opaque, const std::filesystem::path &path, EventHandler handler) + : Watcher(opaque, path, handler) + { + } + + virtual ~StdFilesystemWatcher() override + { + } + + virtual void Process() override + { + /* Untoggle all paths. This allows us to only ever + * iterate over the directory ONCE. */ + UntoggleAllPaths(); + + for (const auto &item : T(path_)) { + std::filesystem::path p = item.path(); + + if (FindAndTogglePath(p)) + continue; + + /* Hand the path off to the listener */ + handler_(opaque_, p, Event::Created); + paths_.push_back({true, p}); + } + + DeleteUntoggledPaths(); + } + +protected: + bool FindAndTogglePath(const std::filesystem::path &p) { + for (auto &pp : paths_) { + if (pp.path == p) { + pp.found = true; + return true; + } + } + + return false; + } + + void UntoggleAllPaths() + { + for (auto &pp : paths_) + pp.found = false; + } + + void DeleteUntoggledPaths() + { + for (const auto &path : paths_) + if (!path.found) + handler_(opaque_, path.path, Event::Deleted); + + auto it = paths_.begin(); + + while (it != paths_.end()) { + if (!it->found) { + it = paths_.erase(it); + } else { + it++; + } + } + } + + struct PathStatus { + bool found; + std::filesystem::path path; + }; + + /* TODO this is probably DAMN slow */ + std::vector paths_; +}; + +IWatcher *GetRecursiveFilesystemWatcher(void *opaque, + const std::filesystem::path &path, IWatcher::EventHandler handler) +{ + /* .... :) */ + return new StdFilesystemWatcher(opaque, path, handler); +} + +IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) +{ + return new StdFilesystemWatcher(opaque, path, handler); +} + } // namespace Filesystem diff -r 5beae59cf042 -r 0265e125f680 src/library/library.cc --- a/src/library/library.cc Thu Nov 06 01:17:24 2025 -0500 +++ b/src/library/library.cc Thu Nov 06 03:16:55 2025 -0500 @@ -13,6 +13,73 @@ namespace Library { +Database::Database() +{ + /* Do this immediately :) */ + UpdateWatchers(); +} + +void Database::UpdateWatchers() +{ + /* TODO also need to remove unused watchers */ + for (const auto &p : session.config.library.paths) { + if (watchers_.count(p)) + continue; + + watchers_[p].reset(Filesystem::GetRecursiveFilesystemWatcher(reinterpret_cast(this), p, Database::StaticEventHandler)); + } +} + +bool Database::GetPathAnimeAndEpisode(const std::string &basename, int *aid, int *ep) +{ + anitomy::Anitomy anitomy; + anitomy.Parse(basename); + + const auto &elements = anitomy.elements(); + + const std::string title = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle)); + + const int id = Anime::db.LookupAnimeTitle(title); + if (id <= 0 || (find_id_ && find_id_.value() != id)) + return false; + + const int episode = + Strings::ToInt(Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber))); + + *aid = id; + *ep = episode; + return true; +} + +void Database::EventHandler(const std::filesystem::path &path, Filesystem::IWatcher::Event event) +{ + std::string bname = path.filename().u8string(); + int aid, ep; + + std::cout << path << '\n'; + + if (!GetPathAnimeAndEpisode(bname, &aid, &ep)) + return; + + switch (event) { + case Filesystem::IWatcher::Created: + items[aid][ep] = path; + break; + case Filesystem::IWatcher::Deleted: + /* kill it off */ + items[aid].erase(ep); + if (items[aid].empty()) + items.erase(aid); + break; + } +} + +void Database::StaticEventHandler(void *opaque, const std::filesystem::path &path, Filesystem::IWatcher::Event event) +{ + /* Forward to class function */ + reinterpret_cast(opaque)->EventHandler(path, event); +} + std::optional Database::GetAnimeFolder(int id) { // this function sucks, but it's the most I can really do for now. @@ -27,10 +94,8 @@ if (id != anime_id) continue; - for (const auto &[episode, path] : episodes) { + for (const auto &[episode, path] : episodes) return path.parent_path(); - break; - } break; } @@ -38,36 +103,15 @@ return std::nullopt; } +/* TODO shove this into a separate thread; currently it blocks */ void Database::Refresh(std::optional find_id) { - items.clear(); - - for (const auto &folder : session.config.library.paths) { - for (const auto &entry : std::filesystem::recursive_directory_iterator(folder)) { - const std::filesystem::path path = entry.path(); - if (!std::filesystem::is_regular_file(path)) - continue; - - const std::string basename = path.filename().u8string(); - - anitomy::Anitomy anitomy; - anitomy.Parse(basename); + find_id_ = find_id; - const auto &elements = anitomy.elements(); - - const std::string title = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle)); - - const int id = Anime::db.LookupAnimeTitle(title); - if (id <= 0 || (find_id && find_id.value() != id)) - continue; + UpdateWatchers(); - const int episode = - Strings::ToInt(Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber))); - - // we have an ID now! - items[id][episode] = path; - } - } + for (const auto &w : watchers_) + w.second->Process(); } void Database::Refresh()