view src/library/library.cc @ 410:eb554255ea5f default tip

*: no crash
author Paper <paper@tflc.us>
date Thu, 02 Apr 2026 01:09:09 -0400
parents 8d06825d96d1
children
line wrap: on
line source

#include "library/library.h"
#include "core/anime_db.h"
#include "core/session.h"
#include "core/strings.h"

#include "anitomy/anitomy.h"

#include <filesystem>
#include <string>
#include <unordered_map>

#include <iostream>

namespace Library {

Database::Database()
{
	/* Do this immediately :) */
	/* Nevermind, this causes an immediate segfault on Windows
	UpdateWatchers();
	*/
	/* Also, this would be completely useless as the paths in
	 * the config are not filled until after main() is
	 * called. */
}

Database::~Database()
{
	/* Wait */
	if (refresh_thread_.joinable())
		refresh_thread_.join();
}

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;

	if (!GetPathAnimeAndEpisode(bname, &aid, &ep))
		return;

	/* Lock the items mutex */
	const std::lock_guard<std::recursive_mutex> lock(items_mutex_);

	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.
	//
	// in the future the Refresh() function should look for directories
	// as well that fit the anime name and *also* have episodes in them.
	// it should give each of these directories a rating by how many
	// episodes are contained in them. whichever directory has more episodes
	// wins, or the first found if there is an equal amount.

	/* TODO use a shared lock instead */
	const std::lock_guard<std::recursive_mutex> lock(items_mutex_);

	for (const auto &[anime_id, episodes] : items) {
		if (id != anime_id)
			continue;

		for (const auto &[episode, path] : episodes)
			return path.parent_path();

		break;
	}

	return std::nullopt;
}

void Database::RefreshThread()
{
	for (const auto &w : watchers_)
		w.second->Process();
}

/* TODO the threading code for this could be better */
void Database::Refresh(std::optional<int> find_id)
{
	/* Join the thread if it was already run */
	if (refresh_thread_.joinable())
		refresh_thread_.join();

	/* This is terrible */
	find_id_ = find_id;

	UpdateWatchers();

	refresh_thread_ = std::thread(&Database::RefreshThread, this);
}

void Database::Refresh()
{
	Refresh(std::nullopt);
}

void Database::Refresh(int id)
{
	Refresh(std::optional<int>(id));
}

std::optional<std::filesystem::path> Database::GetAnimeEpisodePath(int anime, int episode)
{
	const std::lock_guard<std::recursive_mutex> lock(items_mutex_);

	if (items.find(anime) == items.end() || items[anime].find(episode) == items[anime].end())
		return std::nullopt;

	return items[anime][episode];
}

// TODO export to JSON on exit

Database db;

} // namespace Library