aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--example-config.json3
-rw-r--r--include/Config.hpp1
-rw-r--r--plugins/EpisodeNameParser.hpp15
-rw-r--r--plugins/LocalAnime.hpp25
-rw-r--r--src/Config.cpp4
-rw-r--r--src/QuickMedia.cpp2
-rw-r--r--src/plugins/EpisodeNameParser.cpp112
-rw-r--r--src/plugins/LocalAnime.cpp278
-rw-r--r--tests/main.cpp58
-rw-r--r--tests/project.conf2
10 files changed, 418 insertions, 82 deletions
diff --git a/example-config.json b/example-config.json
index 2214571..ce52d52 100644
--- a/example-config.json
+++ b/example-config.json
@@ -28,7 +28,8 @@
},
"local_anime": {
"directory": "",
- "sort_by_name": false
+ "sort_by_name": false,
+ "auto_group_episodes": false
},
"use_system_fonts": false,
"use_system_mpv_config": false,
diff --git a/include/Config.hpp b/include/Config.hpp
index b191c96..efb7edd 100644
--- a/include/Config.hpp
+++ b/include/Config.hpp
@@ -39,6 +39,7 @@ namespace QuickMedia {
struct LocalAnimeConfig {
std::string directory;
bool sort_by_name = false;
+ bool auto_group_episodes = false;
};
struct Config {
diff --git a/plugins/EpisodeNameParser.hpp b/plugins/EpisodeNameParser.hpp
new file mode 100644
index 0000000..1ec847a
--- /dev/null
+++ b/plugins/EpisodeNameParser.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <string_view>
+#include <optional>
+
+namespace QuickMedia {
+ struct EpisodeNameParts {
+ std::string_view group; // optional
+ std::string_view anime; // required
+ std::string_view season; // optional
+ std::string_view episode; // required
+ };
+
+ std::optional<EpisodeNameParts> episode_name_extract_parts(std::string_view episode_name);
+} \ No newline at end of file
diff --git a/plugins/LocalAnime.hpp b/plugins/LocalAnime.hpp
index 7fe58d9..19b93e8 100644
--- a/plugins/LocalAnime.hpp
+++ b/plugins/LocalAnime.hpp
@@ -17,49 +17,42 @@ namespace QuickMedia {
};
struct LocalAnimeSeason {
- Path path;
+ std::string name;
std::vector<LocalAnimeItem> episodes;
time_t modified_time_seconds;
};
struct LocalAnime {
- Path path;
+ std::string name;
std::vector<LocalAnimeItem> items;
time_t modified_time_seconds;
};
- enum class LocalAnimeSearchPageType {
- DIRECTORY,
- ANIME,
- SEASON
- };
+ std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory);
class LocalAnimeSearchPage : public LazyFetchPage {
public:
- LocalAnimeSearchPage(Program *program, Path directory, LocalAnimeSearchPageType type)
- : LazyFetchPage(program), directory(std::move(directory)), type(type) {}
+ LocalAnimeSearchPage(Program *program, std::vector<LocalAnimeItem> anime_items)
+ : LazyFetchPage(program), anime_items(std::move(anime_items)) {}
const char* get_title() const override { return "Search"; }
bool search_is_filter() override { return true; }
PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
PluginResult lazy_fetch(BodyItems &result_items) override;
- bool reload_on_page_change() override { return true; }
- bool reseek_to_body_item_by_url() override { return true; }
void toggle_read(BodyItem *selected_item) override;
private:
- Path directory;
- LocalAnimeSearchPageType type;
+ std::vector<LocalAnimeItem> anime_items;
};
class LocalAnimeVideoPage : public VideoPage {
public:
- LocalAnimeVideoPage(Program *program, std::string filepath, WatchProgress watch_progress)
- : VideoPage(program, std::move(filepath)), watch_progress(std::move(watch_progress)) {}
+ LocalAnimeVideoPage(Program *program, std::string filepath, WatchProgress *watch_progress)
+ : VideoPage(program, std::move(filepath)), watch_progress(watch_progress) {}
const char* get_title() const override { return ""; }
std::string get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) override;
std::string get_url_timestamp() override;
bool is_local() const override { return true; }
void set_watch_progress(int64_t time_pos_sec) override;
private:
- WatchProgress watch_progress;
+ WatchProgress *watch_progress;
};
} \ No newline at end of file
diff --git a/src/Config.cpp b/src/Config.cpp
index d6f826b..7ddc1af 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -163,6 +163,10 @@ namespace QuickMedia {
const Json::Value &sort_by_name_json = local_anime_json["sort_by_name"];
if(sort_by_name_json.isBool())
config->local_anime.sort_by_name = sort_by_name_json.asBool();
+
+ const Json::Value &auto_group_episodes_json = local_anime_json["auto_group_episodes"];
+ if(auto_group_episodes_json.isBool())
+ config->local_anime.auto_group_episodes = auto_group_episodes_json.asBool();
}
const Json::Value &use_system_fonts_json = json_root["use_system_fonts"];
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 33bab93..5a94326 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -1100,7 +1100,7 @@ namespace QuickMedia {
exit(1);
}
- auto search_page = std::make_unique<LocalAnimeSearchPage>(this, get_config().local_anime.directory, LocalAnimeSearchPageType::DIRECTORY);
+ auto search_page = std::make_unique<LocalAnimeSearchPage>(this, get_anime_in_directory(get_config().local_anime.directory));
tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
start_tab_index = 0;
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
diff --git a/tests/main.cpp b/tests/main.cpp
index 32acde6..01f91a3 100644
--- a/tests/main.cpp
+++ b/tests/main.cpp
@@ -1,74 +1,106 @@
#include <stdio.h>
#include "../include/NetUtils.hpp"
+#include "../plugins/EpisodeNameParser.hpp"
#define assert_fail(str) do { fprintf(stderr, "Assert failed on line %d, reason: %s\n", __LINE__, (str)); exit(1); } while(0)
#define assert_equals(a, b) do { if((a) != (b)) { fprintf(stderr, "Assert failed on line %d, %s == %s\n", __LINE__, #a, #b); exit(1); } } while(0)
+using namespace QuickMedia;
+
int main() {
std::vector<std::string> urls;
const char *str;
str = "example.com";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "example.com");
str = "example.com, is where I like to go";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "example.com");
str = "The website I like to go to is example.com";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "example.com");
str = "example.com. Is also a website";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "example.com");
str = "example.com: the best test website";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "example.com");
str = "is it example.com? or not?";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "example.com");
str = "these. are. not. websites.";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 0);
str = "This is not an url: example.";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 0);
str = "the.se/~#423-_/2f.no/3df a.re considered sub.websit.es, this.is.not";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 3);
assert_equals(urls[0], "the.se/~#423-_/2f.no/3df");
assert_equals(urls[1], "a.re");
assert_equals(urls[2], "sub.websit.es");
str = "(see https://emojipedia.org/emoji/%23%EF%B8%8F%E2%83%A3/)";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "https://emojipedia.org/emoji/%23%EF%B8%8F%E2%83%A3/");
str = "[sneed](https://sneedville.com)";
- urls = QuickMedia::ranges_get_strings(str, QuickMedia::extract_urls(str));
+ urls = ranges_get_strings(str, extract_urls(str));
assert_equals(urls.size(), 1);
assert_equals(urls[0], "https://sneedville.com");
std::string html_unescaped_str = "hello &#039; world";
- QuickMedia::html_unescape_sequences(html_unescaped_str);
+ html_unescape_sequences(html_unescaped_str);
assert_equals(html_unescaped_str, "hello ' world");
html_unescaped_str = "hello &#x27; world";
- QuickMedia::html_unescape_sequences(html_unescaped_str);
+ html_unescape_sequences(html_unescaped_str);
assert_equals(html_unescaped_str, "hello ' world");
+
+ std::optional<EpisodeNameParts> name_parts1 = episode_name_extract_parts("[SubsPlease] Shikkakumon no Saikyou Kenja - 07 (1080p) [83EFD76A].mkv");
+ assert_equals(name_parts1.has_value(), true);
+ assert_equals(name_parts1->group, "SubsPlease");
+ assert_equals(name_parts1->anime, "Shikkakumon no Saikyou Kenja");
+ assert_equals(name_parts1->season.size(), 0);
+ assert_equals(name_parts1->episode, "07");
+
+ std::optional<EpisodeNameParts> name_parts2 = episode_name_extract_parts("[SubsPlease] Shingeki no Kyojin (The Final Season) - 81 (1080p) [601A33BD].mkv");
+ assert_equals(name_parts2.has_value(), true);
+ assert_equals(name_parts2->group, "SubsPlease");
+ assert_equals(name_parts2->anime, "Shingeki no Kyojin (The Final Season)");
+ assert_equals(name_parts2->season.size(), 0);
+ assert_equals(name_parts2->episode, "81");
+
+ std::optional<EpisodeNameParts> name_parts3 = episode_name_extract_parts("[SubsPlease] Lupin III - Part 6 - 18 (1080p) [98204042].mkv");
+ assert_equals(name_parts3.has_value(), true);
+ assert_equals(name_parts3->group, "SubsPlease");
+ assert_equals(name_parts3->anime, "Lupin III");
+ assert_equals(name_parts3->season, "Part 6");
+ assert_equals(name_parts3->episode, "18");
+
+ std::optional<EpisodeNameParts> name_parts4 = episode_name_extract_parts("[SubsPlease] Kimetsu no Yaiba - Yuukaku-hen - 11 (1080p) [BE15F231].mkv");
+ assert_equals(name_parts4.has_value(), true);
+ assert_equals(name_parts4->group, "SubsPlease");
+ assert_equals(name_parts4->anime, "Kimetsu no Yaiba");
+ assert_equals(name_parts4->season, "Yuukaku-hen");
+ assert_equals(name_parts4->episode, "11");
+
return 0;
}
diff --git a/tests/project.conf b/tests/project.conf
new file mode 100644
index 0000000..07f784f
--- /dev/null
+++ b/tests/project.conf
@@ -0,0 +1,2 @@
+[lang.cpp]
+version = "c++17" \ No newline at end of file