changeset 382:0265e125f680 default tip

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 :)
author Paper <paper@tflc.us>
date Thu, 06 Nov 2025 03:16:55 -0500
parents 5beae59cf042
children
files include/core/filesystem.h include/library/library.h src/core/filesystem.cc src/library/library.cc
diffstat 4 files changed, 227 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- 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 <filesystem>
 #include <string>
+#include <functional>
 
 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<void(void *opaque, const std::filesystem::path &path, Event event)>;
+
+	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_
--- 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 <filesystem>
 #include <optional>
@@ -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<std::filesystem::path> GetAnimeFolder(int id);
 	void Refresh();
 	void Refresh(int id);
@@ -20,6 +31,11 @@
 
 private:
 	void Refresh(std::optional<int> find_id);
+
+	std::unordered_map<std::filesystem::path, std::unique_ptr<Filesystem::IWatcher>> watchers_;
+
+	/* ID we're looking for */
+	std::optional<int> find_id_;
 };
 
 extern Database db;
--- 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<typename T>
+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<PathStatus> paths_;
+};
+
+IWatcher *GetRecursiveFilesystemWatcher(void *opaque,
+	const std::filesystem::path &path, IWatcher::EventHandler handler)
+{
+	/* .... :) */
+	return new StdFilesystemWatcher<std::filesystem::recursive_directory_iterator>(opaque, path, handler);
+}
+
+IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler)
+{
+	return new StdFilesystemWatcher<std::filesystem::directory_iterator>(opaque, path, handler);
+}
+
 } // namespace Filesystem
--- 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<void *>(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<int>(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<Database *>(opaque)->EventHandler(path, event);
+}
+
 std::optional<std::filesystem::path> 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<int> 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<int>(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()