aboutsummaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-02-20 22:52:28 +0100
committerdec05eba <dec05eba@protonmail.com>2022-02-20 22:52:28 +0100
commit2beeddb325ecbc03ddd6c741449fabd527a3c8cc (patch)
tree545866dddda5f31aff0fc17713f19963e1517757 /src/plugins
parent5d999a9c97b986ff513aa6df71719914a41cb3eb (diff)
Local-anime: add option to group episodes into anime groups from the name of the anime by using the local_manga.auto_group_episodes config
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/EpisodeNameParser.cpp112
-rw-r--r--src/plugins/LocalAnime.cpp278
2 files changed, 339 insertions, 51 deletions
diff --git a/src/plugins/EpisodeNameParser.cpp b/src/plugins/EpisodeNameParser.cpp
new file mode 100644
index 0000000..cbaae50
--- /dev/null
+++ b/src/plugins/EpisodeNameParser.cpp
@@ -0,0 +1,112 @@
+#include "../../plugins/EpisodeNameParser.hpp"
+
+namespace QuickMedia {
+ static bool has_season_in_name(std::string_view episode_name) {
+ size_t sep_count = 0;
+ size_t index = 0;
+ while(true) {
+ size_t next_index = episode_name.find(" - ", index);
+ if(next_index == std::string_view::npos)
+ break;
+
+ index = next_index + 3;
+ ++sep_count;
+ }
+ return sep_count >= 2;
+ }
+
+ static bool is_whitespace(char c) {
+ return c == ' ' || c == '\n' || c == '\t' || c == '\v';
+ }
+
+ static std::string_view strip_left(std::string_view str) {
+ size_t i = 0;
+ for(; i < str.size(); ++i) {
+ if(!is_whitespace(str[i]))
+ break;
+ }
+ return str.substr(i);
+ }
+
+ static std::string_view strip_right(std::string_view str) {
+ long i = (long)str.size() - 1;
+ for(; i >= 0; --i) {
+ if(!is_whitespace(str[i]))
+ break;
+ }
+ return str.substr(0, i + 1);
+ }
+
+ static std::string_view episode_name_extract_group(std::string_view &episode_name) {
+ episode_name = strip_left(episode_name);
+ if(episode_name[0] == '[') {
+ size_t group_end_index = episode_name.find(']', 1);
+ if(group_end_index == std::string_view::npos)
+ return {};
+
+ std::string_view group = episode_name.substr(1, group_end_index - 1);
+ episode_name.remove_prefix(group_end_index + 1);
+ return group;
+ }
+ return {};
+ }
+
+ static std::string_view episode_name_extract_anime(std::string_view &episode_name) {
+ episode_name = strip_left(episode_name);
+ size_t episode_or_season_sep_index = episode_name.find(" - ");
+ if(episode_or_season_sep_index == std::string_view::npos)
+ episode_or_season_sep_index = episode_name.size();
+
+ std::string_view anime = episode_name.substr(0, episode_or_season_sep_index);
+ anime = strip_right(anime);
+ if(episode_or_season_sep_index + 3 > episode_name.size())
+ episode_name = {};
+ else
+ episode_name.remove_prefix(episode_or_season_sep_index + 3);
+
+ return anime;
+ }
+
+ static std::string_view episode_name_extract_season(std::string_view &episode_name) {
+ return episode_name_extract_anime(episode_name);
+ }
+
+ static bool is_num_real_char(char c) {
+ return (c >= '0' && c <= '9') || c == '.';
+ }
+
+ static std::string_view episode_name_extract_episode(std::string_view &episode_name) {
+ episode_name = strip_left(episode_name);
+ size_t i = 0;
+ for(; i < episode_name.size(); ++i) {
+ if(!is_num_real_char(episode_name[i]))
+ break;
+ }
+
+ if(i == 0)
+ return {};
+
+ std::string_view episode = episode_name.substr(0, i);
+ episode_name.remove_prefix(i + 1);
+ return episode;
+ }
+
+ std::optional<EpisodeNameParts> episode_name_extract_parts(std::string_view episode_name) {
+ EpisodeNameParts name_parts;
+ const bool has_season = has_season_in_name(episode_name);
+
+ name_parts.group = episode_name_extract_group(episode_name);
+ name_parts.anime = episode_name_extract_anime(episode_name);
+ if(name_parts.anime.empty())
+ return std::nullopt;
+
+ if(has_season)
+ name_parts.season = episode_name_extract_season(episode_name);
+
+ name_parts.episode = episode_name_extract_episode(episode_name);
+ if(name_parts.episode.empty())
+ return std::nullopt;
+
+ return name_parts;
+ }
+} \ No newline at end of file
diff --git a/src/plugins/LocalAnime.cpp b/src/plugins/LocalAnime.cpp
index 53b26f5..8434810 100644
--- a/src/plugins/LocalAnime.cpp
+++ b/src/plugins/LocalAnime.cpp
@@ -6,6 +6,7 @@
#include "../../include/FileAnalyzer.hpp"
#include "../../include/ResourceLoader.hpp"
#include "../../include/StringUtils.hpp"
+#include "../../plugins/EpisodeNameParser.hpp"
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/window/Window.hpp>
#include <json/value.h>
@@ -21,7 +22,7 @@ namespace QuickMedia {
class LocalAnimeBodyItemData : public BodyItemExtra {
public:
void draw_overlay(mgl::Window &render_target, const Widgets &widgets) override {
- if(!std::holds_alternative<LocalAnimeEpisode>(anime_item) || !widgets.thumbnail)
+ if(!std::holds_alternative<LocalAnimeEpisode>(*anime_item) || !widgets.thumbnail)
return;
const int rect_height = 5;
@@ -40,10 +41,149 @@ namespace QuickMedia {
render_target.draw(unwatch_rect);
}
- LocalAnimeItem anime_item;
+ const LocalAnimeItem *anime_item = nullptr;
WatchProgress watch_progress;
};
+ static std::string_view anime_item_get_filename(const LocalAnimeItem &item) {
+ if(std::holds_alternative<LocalAnime>(item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(item);
+ return anime.name.c_str();
+ } else if(std::holds_alternative<LocalAnimeSeason>(item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item);
+ return season.name.c_str();
+ } else if(std::holds_alternative<LocalAnimeEpisode>(item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ return episode.path.filename();
+ } else {
+ return {};
+ }
+ }
+
+ static time_t anime_item_get_modified_timestamp(const LocalAnimeItem &item) {
+ if(std::holds_alternative<LocalAnime>(item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(item);
+ return anime.modified_time_seconds;
+ } else if(std::holds_alternative<LocalAnimeSeason>(item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item);
+ return season.modified_time_seconds;
+ } else if(std::holds_alternative<LocalAnimeEpisode>(item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ return episode.modified_time_seconds;
+ } else {
+ return 0;
+ }
+ }
+
+ static void sort_anime_items_by_name_desc(std::vector<LocalAnimeItem> &items) {
+ std::sort(items.begin(), items.end(), [](const LocalAnimeItem &item1, const LocalAnimeItem &item2) {
+ return anime_item_get_filename(item1) > anime_item_get_filename(item2);
+ });
+ }
+
+ static void sort_anime_items_by_timestamp_asc(std::vector<LocalAnimeItem> &items) {
+ std::sort(items.begin(), items.end(), [](const LocalAnimeItem &item1, const LocalAnimeItem &item2) {
+ return anime_item_get_modified_timestamp(item1) > anime_item_get_modified_timestamp(item2);
+ });
+ }
+
+ static time_t update_modified_time_from_episodes(LocalAnimeItem &item) {
+ if(std::holds_alternative<LocalAnime>(item)) {
+ LocalAnime &anime = std::get<LocalAnime>(item);
+ time_t last_modified = 0;
+
+ for(LocalAnimeItem &item : anime.items) {
+ last_modified = std::max(last_modified, update_modified_time_from_episodes(item));
+ }
+
+ anime.modified_time_seconds = last_modified;
+ return last_modified;
+ } else if(std::holds_alternative<LocalAnimeSeason>(item)) {
+ LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item);
+ time_t last_modified = 0;
+
+ for(LocalAnimeItem &item : season.episodes) {
+ last_modified = std::max(last_modified, update_modified_time_from_episodes(item));
+ }
+
+ season.modified_time_seconds = last_modified;
+ return last_modified;
+ } else if(std::holds_alternative<LocalAnimeEpisode>(item)) {
+ LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ return episode.modified_time_seconds;
+ } else {
+ return 0;
+ }
+ }
+
+ static time_t update_modified_time_from_episodes(LocalAnime &anime) {
+ time_t last_modified = 0;
+
+ for(LocalAnimeItem &item : anime.items) {
+ last_modified = std::max(last_modified, update_modified_time_from_episodes(item));
+ }
+
+ anime.modified_time_seconds = last_modified;
+ return last_modified;
+ }
+
+ struct GroupedAnime {
+ LocalAnime anime;
+ std::unordered_map<std::string_view, LocalAnimeSeason> seasons_by_name;
+ };
+
+ static std::vector<LocalAnimeItem> group_episodes_by_anime_name(std::vector<LocalAnimeItem> items) {
+ std::unordered_map<std::string_view, GroupedAnime> anime_by_name;
+
+ std::vector<LocalAnimeItem> grouped_items;
+ for(LocalAnimeItem &item : items) {
+ if(std::holds_alternative<LocalAnimeEpisode>(item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ std::optional<EpisodeNameParts> name_parts = episode_name_extract_parts(episode.path.filename());
+ if(!name_parts) {
+ grouped_items.push_back(std::move(item));
+ continue;
+ }
+
+ GroupedAnime &grouped_anime = anime_by_name[name_parts->anime];
+ if(grouped_anime.anime.name.empty())
+ grouped_anime.anime.name = name_parts->anime;
+
+ if(name_parts->season.empty()) {
+ grouped_anime.anime.items.push_back(std::move(item));
+ continue;
+ }
+
+ LocalAnimeSeason &season = grouped_anime.seasons_by_name[name_parts->season];
+ if(season.name.empty())
+ season.name = name_parts->season;
+ season.episodes.push_back(std::move(item));
+ } else {
+ grouped_items.push_back(std::move(item));
+ }
+ }
+
+ for(auto anime_it : anime_by_name) {
+ GroupedAnime &grouped_anime = anime_it.second;
+ for(auto season_it : grouped_anime.seasons_by_name) {
+ LocalAnimeSeason &season = season_it.second;
+ sort_anime_items_by_name_desc(season.episodes);
+ grouped_anime.anime.items.push_back(std::move(season));
+ }
+
+ update_modified_time_from_episodes(grouped_anime.anime);
+ sort_anime_items_by_name_desc(grouped_anime.anime.items);
+ grouped_items.push_back(std::move(grouped_anime.anime));
+ }
+
+ if(get_config().local_anime.sort_by_name)
+ sort_anime_items_by_name_desc(grouped_items);
+ else
+ sort_anime_items_by_timestamp_asc(grouped_items);
+
+ return grouped_items;
+ }
+
static std::vector<LocalAnimeItem> get_episodes_in_directory(const Path &directory) {
std::vector<LocalAnimeItem> episodes;
for_files_in_dir_sort_name(directory, [&episodes](const Path &filepath, FileType file_type) -> bool {
@@ -74,7 +214,7 @@ namespace QuickMedia {
}
LocalAnimeSeason season;
- season.path = filepath;
+ season.name = filepath.filename();
season.episodes = get_episodes_in_directory(filepath);
season.modified_time_seconds = modified_time_seconds;
if(season.episodes.empty())
@@ -86,7 +226,7 @@ namespace QuickMedia {
return anime_items;
}
- static std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory) {
+ std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory) {
std::vector<LocalAnimeItem> anime_items;
auto callback = [&anime_items](const Path &filepath, FileType file_type) -> bool {
time_t modified_time_seconds;
@@ -100,7 +240,7 @@ namespace QuickMedia {
}
LocalAnime anime;
- anime.path = filepath;
+ anime.name = filepath.filename();
anime.items = get_episodes_or_seasons_in_directory(filepath);
anime.modified_time_seconds = modified_time_seconds;
if(anime.items.empty())
@@ -115,6 +255,9 @@ namespace QuickMedia {
else
for_files_in_dir_sort_last_modified(directory, std::move(callback));
+ if(get_config().local_anime.auto_group_episodes)
+ anime_items = group_episodes_by_anime_name(std::move(anime_items));
+
return anime_items;
}
@@ -133,19 +276,60 @@ namespace QuickMedia {
}
}
+ static std::string get_accumulated_name_to_latest_anime_item(const LocalAnimeItem &item, std::string name) {
+ if(std::holds_alternative<LocalAnime>(item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(item);
+ return get_accumulated_name_to_latest_anime_item(anime.items.front(), "");
+ } else if(std::holds_alternative<LocalAnimeSeason>(item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item);
+ if(!name.empty())
+ name += '/';
+ name += season.name;
+ return get_accumulated_name_to_latest_anime_item(season.episodes.front(), name);
+ } else if(std::holds_alternative<LocalAnimeEpisode>(item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ if(!name.empty())
+ name += '/';
+
+ name += "Episode ";
+ std::optional<EpisodeNameParts> name_parts = episode_name_extract_parts(episode.path.filename());
+ if(!name_parts) {
+ name += episode.path.filename_no_ext();
+ return name;
+ }
+
+ name += name_parts->episode;
+ return name;
+ } else {
+ return "";
+ }
+ }
+
+ static std::string get_accumulated_name_to_latest_anime_item(const LocalAnime &anime) {
+ return get_accumulated_name_to_latest_anime_item(anime.items.front(), "");
+ }
+
+ static std::string get_accumulated_name_to_latest_anime_item(const LocalAnimeSeason &season) {
+ return get_accumulated_name_to_latest_anime_item(season.episodes.front(), "");
+ }
+
+ static std::string anime_path_to_item_name(const std::string &path) {
+ return path.substr(get_config().local_anime.directory.size() + 1);
+ }
+
PluginResult LocalAnimeSearchPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
LocalAnimeBodyItemData *item_data = static_cast<LocalAnimeBodyItemData*>(args.extra.get());
- if(std::holds_alternative<LocalAnime>(item_data->anime_item)) {
- const LocalAnime &anime = std::get<LocalAnime>(item_data->anime_item);
- result_tabs.push_back(Tab{ create_body(), std::make_unique<LocalAnimeSearchPage>(program, anime.path.data, LocalAnimeSearchPageType::ANIME), create_search_bar("Search...", SEARCH_DELAY_FILTER) });
+ if(std::holds_alternative<LocalAnime>(*item_data->anime_item)) {
+ const LocalAnime &anime = std::get<LocalAnime>(*item_data->anime_item);
+ result_tabs.push_back(Tab{ create_body(), std::make_unique<LocalAnimeSearchPage>(program, anime.items), create_search_bar("Search...", SEARCH_DELAY_FILTER) });
return PluginResult::OK;
- } else if(std::holds_alternative<LocalAnimeSeason>(item_data->anime_item)) {
- const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(item_data->anime_item);
- result_tabs.push_back(Tab{ create_body(), std::make_unique<LocalAnimeSearchPage>(program, season.path.data, LocalAnimeSearchPageType::SEASON), create_search_bar("Search...", SEARCH_DELAY_FILTER) });
+ } else if(std::holds_alternative<LocalAnimeSeason>(*item_data->anime_item)) {
+ const LocalAnimeSeason &season = std::get<LocalAnimeSeason>(*item_data->anime_item);
+ result_tabs.push_back(Tab{ create_body(), std::make_unique<LocalAnimeSearchPage>(program, season.episodes), create_search_bar("Search...", SEARCH_DELAY_FILTER) });
return PluginResult::OK;
- } else if(std::holds_alternative<LocalAnimeEpisode>(item_data->anime_item)) {
- const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item_data->anime_item);
- result_tabs.push_back(Tab{ nullptr, std::make_unique<LocalAnimeVideoPage>(program, episode.path.data, item_data->watch_progress), nullptr });
+ } else if(std::holds_alternative<LocalAnimeEpisode>(*item_data->anime_item)) {
+ const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(*item_data->anime_item);
+ result_tabs.push_back(Tab{ nullptr, std::make_unique<LocalAnimeVideoPage>(program, episode.path.data, &item_data->watch_progress), nullptr });
return PluginResult::OK;
}
return PluginResult::ERR;
@@ -154,52 +338,38 @@ namespace QuickMedia {
PluginResult LocalAnimeSearchPage::lazy_fetch(BodyItems &result_items) {
std::unordered_map<std::string, WatchProgress> watch_progress = get_watch_progress_for_plugin("local-anime");
- std::vector<LocalAnimeItem> anime_items;
- switch(type) {
- case LocalAnimeSearchPageType::DIRECTORY:
- anime_items = get_anime_in_directory(directory);
- break;
- case LocalAnimeSearchPageType::ANIME:
- anime_items = get_episodes_or_seasons_in_directory(directory);
- break;
- case LocalAnimeSearchPageType::SEASON:
- anime_items = get_episodes_in_directory(directory);
- break;
- }
-
- fprintf(stderr, "num watched animu: %zu\n", watch_progress.size());
- for(auto &it : watch_progress) {
- fprintf(stderr, "watch progress: %s, time: %d, duration: %d\n", it.first.c_str(), (int)it.second.time_pos_sec, (int)it.second.duration_sec);
- }
-
const time_t time_now = time(nullptr);
- for(LocalAnimeItem &anime_item : anime_items) {
- std::string filename_relative_to_anime_dir = get_latest_anime_item(anime_item)->path.data.substr(get_config().local_anime.directory.size() + 1);
+ for(const LocalAnimeItem &anime_item : anime_items) {
+ const LocalAnimeEpisode *latest_anime_episode = get_latest_anime_item(anime_item);
+ std::string filename_relative_to_anime_dir = anime_path_to_item_name(latest_anime_episode->path.data);
auto body_item_data = std::make_shared<LocalAnimeBodyItemData>();
body_item_data->watch_progress = watch_progress[filename_relative_to_anime_dir];
const bool has_finished_watching = body_item_data->watch_progress.has_finished_watching();
- fprintf(stderr, "watch progress %s: time: %d, duration: %d\n", filename_relative_to_anime_dir.c_str(), (int)body_item_data->watch_progress.time_pos_sec, (int)body_item_data->watch_progress.duration_sec);
-
if(std::holds_alternative<LocalAnime>(anime_item)) {
const LocalAnime &anime = std::get<LocalAnime>(anime_item);
std::string title;
if(has_finished_watching)
title = "[Finished watching] ";
- title += anime.path.filename();
+ title += anime.name;
auto body_item = BodyItem::create(std::move(title));
if(has_finished_watching)
body_item->set_title_color(finished_watching_color);
- body_item->set_description("Updated " + seconds_to_relative_time_str(time_now - anime.modified_time_seconds));
+ body_item->set_description("Latest episode: "
+ + get_accumulated_name_to_latest_anime_item(anime)
+ + "\nUpdated: "
+ + seconds_to_relative_time_str(time_now - anime.modified_time_seconds));
body_item->set_description_color(get_theme().faded_text_color);
- body_item->url = anime.path.data;
+ body_item->url = anime.name;
+ body_item->thumbnail_is_local = true;
+ body_item->thumbnail_url = latest_anime_episode->path.data;
- body_item_data->anime_item = std::move(anime_item);
+ body_item_data->anime_item = &anime_item;
body_item->extra = std::move(body_item_data);
result_items.push_back(std::move(body_item));
} else if(std::holds_alternative<LocalAnimeSeason>(anime_item)) {
@@ -208,18 +378,23 @@ namespace QuickMedia {
std::string title;
if(has_finished_watching)
title = "[Finished watching] ";
- title += season.path.filename();
+ title += season.name;
auto body_item = BodyItem::create(std::move(title));
if(has_finished_watching)
body_item->set_title_color(finished_watching_color);
- body_item->set_description("Updated " + seconds_to_relative_time_str(time_now - season.modified_time_seconds));
+ body_item->set_description("Latest episode: "
+ + get_accumulated_name_to_latest_anime_item(season)
+ + "\nUpdated: "
+ + seconds_to_relative_time_str(time_now - season.modified_time_seconds));
body_item->set_description_color(get_theme().faded_text_color);
- body_item->url = season.path.data;
+ body_item->url = season.name;
+ body_item->thumbnail_is_local = true;
+ body_item->thumbnail_url = latest_anime_episode->path.data;
- body_item_data->anime_item = std::move(anime_item);
+ body_item_data->anime_item = &anime_item;
body_item->extra = std::move(body_item_data);
result_items.push_back(std::move(body_item));
} else if(std::holds_alternative<LocalAnimeEpisode>(anime_item)) {
@@ -241,7 +416,7 @@ namespace QuickMedia {
body_item->thumbnail_is_local = true;
body_item->thumbnail_url = episode.path.data;
- body_item_data->anime_item = std::move(anime_item);
+ body_item_data->anime_item = &anime_item;
body_item->extra = std::move(body_item_data);
result_items.push_back(std::move(body_item));
}
@@ -256,8 +431,8 @@ namespace QuickMedia {
LocalAnimeBodyItemData *item_data = static_cast<LocalAnimeBodyItemData*>(selected_item->extra.get());
- Path latest_anime_path = get_latest_anime_item(item_data->anime_item)->path;
- std::string filename_relative_to_anime_dir = latest_anime_path.data.substr(get_config().local_anime.directory.size() + 1);
+ Path latest_anime_path = get_latest_anime_item(*item_data->anime_item)->path;
+ std::string filename_relative_to_anime_dir = anime_path_to_item_name(latest_anime_path.data);
FileAnalyzer file_analyzer;
if(!file_analyzer.load_file(latest_anime_path.data.c_str(), true)) {
@@ -302,14 +477,14 @@ namespace QuickMedia {
// If we are very close to the end then start from the beginning.
// This is the same behavior as mpv.
// This is better because we dont want the video player to stop immediately after we start playing and we dont get any chance to seek.
- if(watch_progress.time_pos_sec + 10.0 >= watch_progress.duration_sec)
+ if(watch_progress->time_pos_sec + 10.0 >= watch_progress->duration_sec)
return "0";
else
- return std::to_string(watch_progress.time_pos_sec);
+ return std::to_string(watch_progress->time_pos_sec);
}
void LocalAnimeVideoPage::set_watch_progress(int64_t time_pos_sec) {
- std::string filename_relative_to_anime_dir = url.substr(get_config().local_anime.directory.size() + 1);
+ std::string filename_relative_to_anime_dir = anime_path_to_item_name(url);
FileAnalyzer file_analyzer;
if(!file_analyzer.load_file(url.c_str(), true)) {
@@ -322,7 +497,8 @@ namespace QuickMedia {
return;
}
- watch_progress.duration_sec = *file_analyzer.get_duration_seconds();
+ watch_progress->time_pos_sec = time_pos_sec;
+ watch_progress->duration_sec = *file_analyzer.get_duration_seconds();
set_watch_progress_for_plugin("local-anime", filename_relative_to_anime_dir, time_pos_sec, *file_analyzer.get_duration_seconds(), url);
}
} \ No newline at end of file