Skip to content

Instantly share code, notes, and snippets.

@ITotalJustice
Created November 9, 2021 17:59
Show Gist options
  • Save ITotalJustice/5c1ad163b483f30c261a5ad5a04ee30f to your computer and use it in GitHub Desktop.
Save ITotalJustice/5c1ad163b483f30c261a5ad5a04ee30f to your computer and use it in GitHub Desktop.
fs
#include "crunchy.hpp"
#include "crapi/crapi.hpp"
#include "../utils/logger.hpp"
#include <algorithm>
#include <ranges>
namespace fs {
static auto CheckCrDataError(const auto& a) noexcept {
if (a.error == true) {
LOG("[ERROR] %s\n", a.code.c_str());
return false;
}
return true;
}
Crunchy::Crunchy() {
this->api = std::make_unique<cr::Crapi>();
if (!this->api->StartSession()) {
this->good = false;
LOG("[CRUNCHY] failed to start session\n");
} else {
this->good = true;
}
this->SetupFullpath();
}
Crunchy::~Crunchy() {
}
auto Crunchy::List(std::stop_token stk, FilterFunc filter_func) -> Result {
try {
switch (this->page) {
case Pages::HOME:
return this->GetHomeOptions(stk);
case Pages::SERIES_LIST:
return this->GetSeriesList(stk);
case Pages::SEARCH_LIST:
return this->GetSearchSeriesList(stk);
case Pages::COLLECTIONS:
return this->GetCollections(stk);
case Pages::EPISODES:
return this->GetEpisodes(stk);
}
} catch (const std::exception& e) {
LOG("[EXCEPTION] %s\n", e.what());
return e.what();
}
__builtin_unreachable();
}
auto Crunchy::Open(std::stop_token stk, const Entry& entry, OpenType type, FilterFunc filter_func) -> Result {
auto private_data = dynamic_cast<CrunchyPrivateData*>(entry.private_data.get());
switch (this->page) {
case Pages::HOME:
this->series_path = private_data->id;
this->search_term.reset();
this->page = Pages::SERIES_LIST;
break;
case Pages::SERIES_LIST: case Pages::SEARCH_LIST:
this->series_id = private_data->id;
this->page = Pages::COLLECTIONS;
break;
case Pages::COLLECTIONS:
this->collection_id = private_data->id;
this->page = Pages::EPISODES;
break;
case Pages::EPISODES:
// impossible!
throw;
return {"oof"};
}
this->SetupFullpath();
return this->List(stk, filter_func);
}
auto Crunchy::WalkUp(std::stop_token stk, FilterFunc filter_func) -> Result {
switch (this->page) {
case Pages::HOME:
break;
// return "Already Root!";
case Pages::SERIES_LIST:
case Pages::SEARCH_LIST:
this->search_term.reset();
this->page = Pages::HOME;
break;
case Pages::COLLECTIONS:
if (this->search_term.has_value()) {
this->page = Pages::SEARCH_LIST;
} else {
this->page = Pages::SERIES_LIST;
}
break;
case Pages::EPISODES:
this->page = Pages::COLLECTIONS;
break;
}
this->SetupFullpath();
return this->List(stk, filter_func);
}
// todo: rewrite page thing to be more like funime (for now).
// once that's done, add search
auto Crunchy::Search(std::stop_token stk, const std::string& path, FilterFunc filter_func) -> Result {
this->search_term = path;
this->page = Pages::SEARCH_LIST;
return this->List(stk, filter_func);
}
auto Crunchy::GetMediaUri(std::stop_token stk, const Entry& entry) const -> std::optional<std::string> {
try {
auto private_data = dynamic_cast<CrunchyPrivateData*>(entry.private_data.get());
auto it = std::ranges::find(this->media_data, private_data->id, &cr::MediaData::episode_number);
return cr::Crapi::GetStreamUrl(*it);
} catch (const std::exception& e) {
LOG("[EXCEPTION-GetMediaUri()] %s\n", e.what());
}
return {};
}
auto Crunchy::GetHomeOptions(std::stop_token token) const -> Result {
return std::vector{
Entry{"Recently Updated", EntryType::FOLDER, std::make_shared<CrunchyPrivateData>(cr::FILTER_UPDATED)},
Entry{"Popular Anime", EntryType::FOLDER, std::make_shared<CrunchyPrivateData>(cr::FILTER_POPULAR)},
Entry{"Featured", EntryType::FOLDER, std::make_shared<CrunchyPrivateData>(cr::FILTER_FEATURED)},
Entry{"Newest", EntryType::FOLDER, std::make_shared<CrunchyPrivateData>(cr::FILTER_NEWEST)},
Entry{"Alpha", EntryType::FOLDER, std::make_shared<CrunchyPrivateData>(cr::FILTER_ALPHA)},
Entry{"Simulcast", EntryType::FOLDER, std::make_shared<CrunchyPrivateData>(cr::FILTER_SIMULCAST)},
};
}
auto Crunchy::GetSearchSeriesList(std::stop_token stk) -> Result {
const auto series = this->api->SearchAsync(stk, *this->search_term).value();
if (!CheckCrDataError(series)) {
return series.code;
}
Entries entries;
this->series_data = std::move(series.data);
LOG("\nListing series that we got\n");
for (auto&p : this->series_data) {
entries.emplace_back(p.name,
EntryType::FOLDER,
std::make_shared<CrunchyPrivateData>(p.series_id)
);
LOG("SeriesID: %s Name: %s\n", p.series_id.c_str(), p.name.c_str());
}
return entries;
}
auto Crunchy::GetSeriesList(std::stop_token stk) -> Result {
const auto series = this->api->ListSeriesAsync(stk,
this->series_path, 0U, 25).value();
if (!CheckCrDataError(series)) {
return series.code;
}
Entries entries;
this->series_data = std::move(series.data);
LOG("\nListing series that we got\n");
for (auto&p : this->series_data) {
entries.emplace_back(p.name,
EntryType::FOLDER,
std::make_shared<CrunchyPrivateData>(p.series_id)
);
LOG("SeriesID: %s Name: %s\n", p.series_id.c_str(), p.name.c_str());
}
return entries;
}
auto Crunchy::GetCollections(std::stop_token stk) -> Result {
const auto collection = this->api->ListCollectionsAsync(stk,
this->series_id, 0U, -1).value();
if (!CheckCrDataError(collection)) {
return collection.code;
}
Entries entries;
this->collection_data = std::move(collection.data);
LOG("\nListing collections that we got\n");
for (auto&p : this->collection_data) {
entries.emplace_back("Season " + p.season + ": " + p.name,
EntryType::FOLDER,
std::make_shared<CrunchyPrivateData>(p.collection_id)
);
LOG("SeriesID: %s Season: %s Name: %s\n", p.series_id.c_str(), p.season.c_str(), p.name.c_str());
}
return entries;
}
auto Crunchy::GetEpisodes(std::stop_token stk) -> Result {
const auto media = this->api->ListMediaFromCollectionIDAsync(stk,
this->collection_id, 0U, -1).value();
if (!CheckCrDataError(media)) {
return media.code;
}
Entries entries;
this->media_data = std::move(media.data);
LOG("\nListing media episodes that we got\n");
for (auto&p : this->media_data) {
// entries.emplace_back(p.episode_number, EntryType::FILE);
entries.emplace_back("Episode " + p.episode_number + ": " + p.name,
EntryType::FILE,
std::make_shared<CrunchyPrivateData>(p.episode_number)
);
// LOG("Premium: %s Episode: %s Name: %s\n", p.premium_only ? "TRUE" : "FALSE", p.episode_number.c_str(), p.name.c_str());
}
return entries;
}
auto Crunchy::SetupFullpath() -> void {
// todo: rewrite this as this code was just to show something for current
// dir in filebrowser.
switch (this->page) {
case Pages::HOME:
this->fullpath = "Home";
break;
case Pages::SERIES_LIST:
case Pages::SEARCH_LIST:
this->fullpath = "Home/" + this->series_path;
break;
case Pages::COLLECTIONS:
this->fullpath = "Home/" + this->series_path + "/Seasons";
break;
case Pages::EPISODES:
this->fullpath = "Home/" + this->series_path + "/Seasons/Episodes";
break;
}
}
} // namespace fs
// currently the file borwser api wants files / folders.
// so this class has to abstract the crapi to all options being folders.
#pragma once
#include "fsbase.hpp"
#include "crapi/series.hpp"
#include "crapi/collection.hpp"
#include "crapi/media.hpp"
#include <memory>
// i think this should be the ordering.
// home -> series list -> collections / seasons -> episodes
namespace cr { class Crapi; }
namespace fs {
class Crunchy final : public FsBase {
public:
enum class Pages {
HOME,
SERIES_LIST,
SEARCH_LIST,
COLLECTIONS,
EPISODES,
};
public:
Crunchy();
~Crunchy();
auto List(std::stop_token token, FilterFunc filter_func = {}) -> Result override;
auto Open(std::stop_token token, const Entry& entry, OpenType type, FilterFunc filter_func = {}) -> Result override;
auto WalkUp(std::stop_token token, FilterFunc filter_func = {}) -> Result override;
auto Search(std::stop_token token, const std::string& path, FilterFunc filter_func = {}) -> Result override;
auto GetMediaUri(std::stop_token token, const Entry& entry) const -> std::optional<std::string> override;
auto CanSearch() const noexcept -> bool override { return true; }
private:
class CrunchyPrivateData final : public EntryPrivateData {
public:
CrunchyPrivateData(const std::string &_id) : id{_id} {}
std::string id;
};
private:
auto GetHomeOptions(std::stop_token token) const -> Result;
auto GetSeriesList(std::stop_token token) -> Result;
auto GetSearchSeriesList(std::stop_token token) -> Result;
auto GetCollections(std::stop_token token) -> Result;
auto GetEpisodes(std::stop_token token) -> Result;
auto SetupFullpath() -> void;
private:
std::unique_ptr<cr::Crapi> api{nullptr};
std::vector<cr::SeriesData> series_data; // popular, recent, ...
std::vector<cr::CollectionData> collection_data; // seasons
std::vector<cr::MediaData> media_data; // episodes
// todo: join this for fullpath.
std::string series_path; // popular, recent, ...
std::optional<std::string> search_term;
std::string series_id; // which season selected
std::string collection_id; // which episode
Pages page{Pages::HOME};
};
} // namespace fs
// simple fs base for read-only browsing.
// - List(): lists all entries in current path.
// - OpenRoot(): opens the root path.
// - Open(path, type): opens dir based on type.
// - append: is fullpath + path.
// - full: fullpath = path then opens.
// - WalkUp(): try and walk up
#pragma once
#include "../utils/thread.hpp"
#include <string>
#include <string_view>
#include <vector>
#include <functional>
#include <memory>
#include <optional>
namespace fs {
enum class EntryType {
FOLDER,
FILE,
// todo: file types
// MOVIE,
// MUSIC,
// IMAGE,
// ZIP,
};
// private data for each entry type.
// im pretty sure this is a bad idea, but the only other altnenitive
// is to have each entry in a vector be a pointer, which sounds a *lot*
// worse to me.
// another option is to make the private data be a variant.
// this is a much nicer approach than pointers but it means having to
// keep adding entries to the variant <> in the base class, and logically
// the base class should not know about the parent classes data types, it's
// just meant to be abstract.
// i would also have to include all fs headers here in the base class which
// makes no sense to do, so sadly i don't think varient is a better option.
class EntryPrivateData {
public:
virtual ~EntryPrivateData() = default;
};
struct Entry {
std::string name;
EntryType type;
std::shared_ptr<EntryPrivateData> private_data{nullptr};
};
using Entries = std::vector<Entry>;
enum class ResultType {
OK,
REQUESTED_EXIT,
ERROR,
};
struct Result {
Result(const std::string& err) : error_message{err} {
type = ResultType::ERROR;
}
Result(const char* err) : error_message{err} {
type = ResultType::ERROR;
}
Result(Entries&& _entries) : entries{std::forward<Entries>(_entries)} {
type = ResultType::OK;
}
Result(ResultType _type) : type{_type} { }
auto Good() const noexcept { return this->type == ResultType::OK; }
auto ExitedEarly() const noexcept { return this->type == ResultType::REQUESTED_EXIT; }
std::optional<std::string> error_message;
Entries entries;
ResultType type;
};
class FsBase {
public:
enum class OpenType { APPEND, FULL };
// might remove these and just make them bools as i don't really
// care why something fails, just need to know if it fails or not.
// return boolean instead.
enum class /*[[nodiscard]]*/ OpenError { OK, NO_HANDLE, UNK_ERROR };
enum class /*[[nodiscard]]*/ WalkError { OK, NO_HANDLE, ALREADY_ROOT, UNK_ERROR };
enum class /*[[nodiscard]]*/ FilterVal { WANT, SKIP };
using FilterFunc = std::function<FilterVal(std::string_view name, EntryType type)>;
using Then = std::function<void()>;
public:
virtual ~FsBase() = default;
// virtual auto connect(std::optional<std::string> user, std::optional<std::string> pass) -> bool { return true; }
virtual auto List(std::stop_token token, FilterFunc filter_func = {}) -> Result = 0;
virtual auto Open(std::stop_token token, const Entry& entry, OpenType type, FilterFunc filter_func = {}) -> Result = 0;
virtual auto WalkUp(std::stop_token token, FilterFunc filter_func = {}) -> Result = 0;
virtual auto Search(std::stop_token token, const std::string& path, FilterFunc filter_func = {}) -> Result { return {"Search Not Supported!"}; } // TODO: make = 0 soon tm
virtual auto GetMediaUri(std::stop_token token, const Entry& entry) const -> std::optional<std::string> = 0;
virtual auto CanSearch() const noexcept -> bool { return false; }
auto List(FilterFunc filter_func = {}) {
return this->List(std::stop_token{}, filter_func);
}
auto Open(const Entry& entry, OpenType type, FilterFunc filter_func = {}) {
return this->Open(std::stop_token{}, entry, type, filter_func);
}
auto WalkUp(FilterFunc filter_func = {}) {
return this->WalkUp(std::stop_token{}, filter_func);
}
auto Search(const std::string& path, FilterFunc filter_func = {}) {
return this->Search(std::stop_token{}, path, filter_func);
}
auto GetMediaUri(const Entry& entry) const {
return this->GetMediaUri(std::stop_token{}, entry);
}
// enable for testing non-async
#if 0
auto ListAsync(FilterFunc filter_func = {}, Then then = {}) {
auto r = util::Async([this, filter_func, then](std::stop_token token) {
const auto result = this->List(token, filter_func);
if (then) {
then();
}
return result;
});
r.Wait();
return r;
}
auto OpenAsync(const Entry& entry, OpenType type, FilterFunc filter_func = {}, Then then = {}) {
auto r = util::Async([this, entry, type, filter_func, then](std::stop_token token) {
const auto result = this->Open(token, entry, type, filter_func);
if (then) {
then();
}
return result;
});
r.Wait();
return r;
}
auto WalkUpAsync(FilterFunc filter_func = {}, Then then = {}) {
auto r = util::Async([this, filter_func, then](std::stop_token token) {
const auto result = this->WalkUp(token, filter_func);
if (then) {
then();
}
return result;
});
r.Wait();
return r;
}
auto SearchAsync(const std::string& path, FilterFunc filter_func = {}, Then then = {}) {
auto r = util::Async([this, path, filter_func, then](std::stop_token token) {
const auto result = this->Search(token, path, filter_func);
if (then) {
then();
}
return result;
});
r.Wait();
return r;
}
auto GetMediaUriAsync(const Entry& entry, Then then = {}) const {
auto r = util::Async([this, entry, then](std::stop_token token) {
const auto result = this->GetMediaUri(token, entry);
if (then) {
then();
}
return result;
});
r.Wait();
return r;
}
#else
auto ListAsync(FilterFunc filter_func = {}, Then then = {}) {
return util::Async([this, filter_func, then](std::stop_token token) {
const auto result = this->List(token, filter_func);
if (then) {
then();
}
return result;
});
}
auto OpenAsync(const Entry& entry, OpenType type, FilterFunc filter_func = {}, Then then = {}) {
return util::Async([this, entry, type, filter_func, then](std::stop_token token) {
const auto result = this->Open(token, entry, type, filter_func);
if (then) {
then();
}
return result;
});
}
auto WalkUpAsync(FilterFunc filter_func = {}, Then then = {}) {
return util::Async([this, filter_func, then](std::stop_token token) {
const auto result = this->WalkUp(token, filter_func);
if (then) {
then();
}
return result;
});
}
auto SearchAsync(const std::string& path, FilterFunc filter_func = {}, Then then = {}) {
return util::Async([this, path, filter_func, then](std::stop_token token) {
const auto result = this->Search(token, path, filter_func);
if (then) {
then();
}
return result;
});
}
auto GetMediaUriAsync(const Entry& entry, Then then = {}) const {
return util::Async([this, entry, then](std::stop_token token) {
const auto result = this->GetMediaUri(token, entry);
if (then) {
then();
}
return result;
});
}
#endif
auto FullPath() const { return this->fullpath; } // not noexcept because alloc can fail
auto IsGood() const noexcept { return this->good; }
protected:
std::string fullpath;
bool good{false};
private:
};
} // namespace fs
#include "funime.hpp"
#include "funapi/funapi.hpp"
#include "hlsparse/hlsparse.hpp"
#include "../utils/logger.hpp"
#include <algorithm>
#include <ranges>
#include <iostream>
namespace fs {
Funime::Funime() {
this->api = std::make_unique<funi::Funapi>();
this->SetupFullpath();
}
Funime::~Funime() {
}
auto Funime::List(std::stop_token stk, FilterFunc filter_func) -> Result {
try {
switch (this->page) {
case Pages::HOME:
return this->GetHomeOptions(stk);
case Pages::GENRE_SHOWS:
return this->GetGenreShows(stk);
case Pages::SEARCH_SHOWS:
return this->GetSearchShows(stk);
case Pages::SEASONS:
return this->GetSeasons(stk);
case Pages::EPISODES:
return this->GetEpisodes(stk);
}
} catch (const std::exception& e) {
LOG("[EXCEPTION] %s\n", e.what());
return {e.what()};
}
__builtin_unreachable();
}
auto Funime::Open(std::stop_token stk, const Entry& entry, OpenType type, FilterFunc filter_func) -> Result {
auto private_data = dynamic_cast<FunimePrivateData*>(entry.private_data.get());
switch (this->page) {
case Pages::HOME:
this->genre_id = private_data->id;
this->search_term.reset();
this->page = Pages::GENRE_SHOWS;
break;
case Pages::GENRE_SHOWS:
case Pages::SEARCH_SHOWS:
this->title_id = private_data->id;
this->page = Pages::SEASONS;
break;
case Pages::SEASONS:
this->season_id = private_data->id;
this->page = Pages::EPISODES;
break;
case Pages::EPISODES:
// impossible!
throw;
return {"oof"};
}
this->SetupFullpath();
return this->List(stk, filter_func);
}
auto Funime::WalkUp(std::stop_token stk, FilterFunc filter_func) -> Result {
switch (this->page) {
case Pages::HOME:
// return "Already Root!";
break;
case Pages::GENRE_SHOWS:
case Pages::SEARCH_SHOWS:
this->page = Pages::HOME;
break;
case Pages::SEASONS:
if (this->search_term.has_value()) {
this->page = Pages::SEARCH_SHOWS;
} else {
this->page = Pages::GENRE_SHOWS;
}
break;
case Pages::EPISODES:
this->page = Pages::SEASONS;
break;
}
this->SetupFullpath();
return this->List(stk, filter_func);
}
auto Funime::Search(std::stop_token stk, const std::string& path, FilterFunc filter_func) -> Result {
this->page = Pages::SEARCH_SHOWS;
this->search_term = path;
return this->List(stk, filter_func);
}
auto Funime::GetMediaUri(std::stop_token stk, const Entry& entry) const -> std::optional<std::string> {
const auto private_data = dynamic_cast<FunimePrivateData*>(entry.private_data.get());
// basically makes 1 api call and 1 download for the master.m3u8.
// the master m3u8 is then parsed for the actual url.
// this is because mpv (or ffmpeg, not sure) is slow as shit at parsing
// the master, and also randomly picks a stream.
// this way we have more control over the stream quality and speed.
// TODO: return struct of video url + vector<string> subs.
try {
const auto episode = this->api->GetEpisode(private_data->id).value();
if (const auto streams = episode.GetMediaStreams(); !streams.empty()) {
for (auto& stream : streams) {
std::cout << "StreamID: " << stream.experience.id << '\n';
}
auto& stream = streams[0];
const auto showex = this->api->GetShowExperience(stream.experience.id).value();
if (auto it = std::ranges::find(showex.items, "m3u8", &funi::ShowExperience::Item::videoType); it != showex.items.end()) {
curl::CurlWrapper curl;
if (const auto data = curl.Download(curl::Url{it->src})) {
if (const auto hls = hls::ParseMaster(*data); !hls.empty()) {
return hls[0].url;
}
}
}
}
} catch (const std::exception& e) {
LOG("[EXCEPTION] %s\n", e.what());
}
return {};
}
auto Funime::GetHomeOptions(std::stop_token stk) -> Result {
const auto data = this->api->ListGenresAsync(stk).value();
Entries entries;
for (auto& entry : data) {
entries.emplace_back(entry.name,
EntryType::FOLDER,
std::make_shared<FunimePrivateData>(entry.id)
);
}
return entries;
}
auto Funime::GetGenreShows(std::stop_token stk) -> Result {
// this one is really really slow
const auto data = this->api->ListTitlesFromGenreAsync(stk, this->genre_id).value();
// const auto data = this->api->ListTitlesFromGenre2(this->genre_id).value();
Entries entries;
for (auto& entry : data.items) {
entries.emplace_back(entry.title,
EntryType::FOLDER,
std::make_shared<FunimePrivateData>(entry.id)
);
}
return entries;
}
auto Funime::GetSearchShows(std::stop_token stk) -> Result {
const auto data = this->api->SearchAsync(stk, *this->search_term).value();
Entries entries;
for (auto& entry : data.items.hits) {
entries.emplace_back(entry.title,
EntryType::FOLDER,
std::make_shared<FunimePrivateData>(entry.id)
);
}
return entries;
}
auto Funime::GetSeasons(std::stop_token stk) -> Result {
const auto data = this->api->GetTitleInfoAsync(stk, this->title_id).value();
Entries entries;
for (auto& item : data.items) {
for (auto& child : item.children) {
entries.emplace_back(child.title,
EntryType::FOLDER,
std::make_shared<FunimePrivateData>(std::stoul(child.number))
);
}
}
return entries;
}
auto Funime::GetEpisodes(std::stop_token stk) -> Result {
const auto data = this->api->ListEpisodesAsync(stk, this->title_id, this->season_id).value();
Entries entries;
for (auto& entry : data.items) {
entries.emplace_back(
entry.mediaCategory + ' ' + entry.item.episodeNum + ": " + entry.item.episodeName,
EntryType::FILE,
std::make_shared<FunimePrivateData>(entry.GetEpisodeID())
);
}
return entries;
}
auto Funime::SetupFullpath() -> void {
// todo: rewrite this as this code was just to show something for current
// dir in filebrowser.
switch (this->page) {
case Pages::HOME:
this->fullpath = "Home/";
break;
case Pages::GENRE_SHOWS: case Pages::SEARCH_SHOWS:
this->fullpath = "Home/Shows/";
break;
case Pages::SEASONS:
this->fullpath = "Home/Shows/Seasons";
break;
case Pages::EPISODES:
this->fullpath = "Home/Shows/Seasons/Episodes";
break;
}
}
} // namespace fs
#pragma once
#include "fsbase.hpp"
#include <memory>
namespace funi { class Funapi; }
namespace fs {
class Funime final : public FsBase {
public:
enum class Pages {
HOME,
GENRE_SHOWS,
SEARCH_SHOWS,
SEASONS,
EPISODES,
};
public:
Funime();
~Funime();
auto List(std::stop_token token, FilterFunc filter_func = {}) -> Result override;
auto Open(std::stop_token token, const Entry& entry, OpenType type, FilterFunc filter_func = {}) -> Result override;
auto WalkUp(std::stop_token token, FilterFunc filter_func = {}) -> Result override;
auto Search(std::stop_token token, const std::string& path, FilterFunc filter_func = {}) -> Result override;
auto GetMediaUri(std::stop_token token, const Entry& entry) const -> std::optional<std::string> override;
auto CanSearch() const noexcept -> bool override { return true; }
private:
class FunimePrivateData final : public EntryPrivateData {
public:
FunimePrivateData(std::uint32_t _id) : id{_id} {}
std::uint32_t id;
};
private:
auto GetHomeOptions(std::stop_token token) -> Result;
auto GetGenreShows(std::stop_token token) -> Result;
auto GetSearchShows(std::stop_token token) -> Result;
auto GetSeasons(std::stop_token token) -> Result;
auto GetEpisodes(std::stop_token token) -> Result;
auto SetupFullpath() -> void;
private:
std::unique_ptr<funi::Funapi> api;
std::optional<std::string> search_term{std::nullopt};
std::uint32_t genre_id{};
std::uint32_t title_id{};
std::uint32_t season_id{};
Pages page{Pages::HOME};
};
} // namespace fs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment