aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/QuickMedia.cpp298
-rw-r--r--src/VideoPlayer.cpp40
-rw-r--r--src/plugins/Youtube.cpp250
3 files changed, 296 insertions, 292 deletions
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index f94601c..594b93a 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -248,182 +248,36 @@ static sf::Color interpolate_colors(sf::Color source, sf::Color target, double p
}
namespace QuickMedia {
- // Returns index to item or -1 if not found
- static int get_body_item_by_url(Body *body, const std::string &url) {
- if(url.empty()) return -1;
- for(size_t i = 0; i < body->items.size(); ++i) {
- auto &body_item = body->items[i];
- if(body_item->url == url)
- return i;
- }
- return -1;
- }
-
- static Path get_recommended_filepath(const char *plugin_name) {
- Path video_history_dir = get_storage_dir().join("recommended");
- if(create_directory_recursive(video_history_dir) != 0) {
- std::string err_msg = "Failed to create recommended directory ";
- err_msg += video_history_dir.data;
- show_notification("QuickMedia", err_msg.c_str(), Urgency::CRITICAL);
- exit(1);
- }
-
- Path video_history_filepath = video_history_dir;
- return video_history_filepath.join(plugin_name).append(".json");
- }
-
- // TODO: Make asynchronous
- static void fill_recommended_items_from_json(const char *plugin_name, const Json::Value &recommended_json, BodyItems &body_items) {
- assert(recommended_json.isObject());
-
- const int64_t recommendations_autodelete_period = 60*60*24*20; // 20 days
- time_t time_now = time(NULL);
- int num_items_deleted = 0;
-
- std::vector<std::pair<std::string, Json::Value>> recommended_items(recommended_json.size());
- /* TODO: Optimize member access */
- for(auto &member_name : recommended_json.getMemberNames()) {
- Json::Value recommended_item = recommended_json[member_name];
- if(recommended_item.isObject()) {
- Json::Value recommended_timestamp_json = recommended_item.get("recommended_timestamp", Json::Value::nullSingleton());
- Json::Value watched_timestamp_json = recommended_item.get("watched_timestamp", Json::Value::nullSingleton());
- if(watched_timestamp_json.isNumeric() && time_now - watched_timestamp_json.asInt64() >= recommendations_autodelete_period) {
- ++num_items_deleted;
- } else if(recommended_timestamp_json.isNumeric() && time_now - recommended_timestamp_json.asInt64() >= recommendations_autodelete_period) {
- ++num_items_deleted;
- } else if(recommended_timestamp_json.isNull() && watched_timestamp_json.isNull()) {
- ++num_items_deleted;
- } else {
- recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item)));
- }
- }
- }
-
- if(num_items_deleted > 0) {
- // TODO: Is there a better way?
- Json::Value new_recommendations(Json::objectValue);
- for(auto &recommended : recommended_items) {
- new_recommendations[recommended.first] = recommended.second;
- }
- fprintf(stderr, "Number of old recommendations to delete: %d\n", num_items_deleted);
- save_json_to_file_atomic(get_recommended_filepath(plugin_name), new_recommendations);
- }
-
- /* TODO: Better algorithm for recommendations */
- std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &b) {
- Json::Value &a_timestamp_json = a.second["recommended_timestamp"];
- Json::Value &b_timestamp_json = b.second["recommended_timestamp"];
- int64_t a_timestamp = 0;
- int64_t b_timestamp = 0;
- if(a_timestamp_json.isNumeric())
- a_timestamp = a_timestamp_json.asInt64();
- if(b_timestamp_json.isNumeric())
- b_timestamp = b_timestamp_json.asInt64();
-
- Json::Value &a_recommended_count_json = a.second["recommended_count"];
- Json::Value &b_recommended_count_json = b.second["recommended_count"];
- int64_t a_recommended_count = 0;
- int64_t b_recommended_count = 0;
- if(a_recommended_count_json.isNumeric())
- a_recommended_count = a_recommended_count_json.asInt64();
- if(b_recommended_count_json.isNumeric())
- b_recommended_count = b_recommended_count_json.asInt64();
-
- /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */
- a_timestamp += (300 * a_recommended_count);
- b_timestamp += (300 * b_recommended_count);
-
- return a_timestamp > b_timestamp;
- });
-
- for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) {
- const std::string &recommended_item_id = it->first;
- Json::Value &recommended_item = it->second;
-
- int64_t watched_count = 0;
- const Json::Value &watched_count_json = recommended_item["watched_count"];
- if(watched_count_json.isNumeric())
- watched_count = watched_count_json.asInt64();
-
- /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */
- if(watched_count != 0)
- continue;
-
- const Json::Value &recommended_title_json = recommended_item["title"];
- if(!recommended_title_json.isString())
- continue;
-
- auto body_item = BodyItem::create(recommended_title_json.asString());
- body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id;
- body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/mqdefault.jpg";
- body_item->thumbnail_size = sf::Vector2i(192, 108);
- body_items.push_back(std::move(body_item));
-
- // We dont want more than 150 recommendations
- if(body_items.size() == 150)
- break;
- }
-
- std::random_shuffle(body_items.begin(), body_items.end());
- }
-
enum class HistoryType {
YOUTUBE,
MANGA
};
- class HistoryPage : public Page {
+ class HistoryPage : public LazyFetchPage {
public:
- HistoryPage(Program *program, Page *search_page, SearchBar *search_bar, HistoryType history_type) :
- Page(program), search_page(search_page), search_bar(search_bar), history_type(history_type) {}
+ HistoryPage(Program *program, Page *search_page, HistoryType history_type) :
+ LazyFetchPage(program), search_page(search_page), history_type(history_type) {}
const char* get_title() const override { return "History"; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override {
return search_page->submit(title, url, result_tabs);
}
- void on_navigate_to_page(Body *body) override {
- std::string selected_item_url = body->get_selected() ? body->get_selected()->url : "";
- body->clear_items();
+ PluginResult lazy_fetch(BodyItems &result_items) override {
switch(history_type) {
case HistoryType::YOUTUBE:
- program->youtube_get_watch_history(body->items);
+ program->youtube_get_watch_history(result_items);
break;
case HistoryType::MANGA:
- program->manga_get_watch_history(program->get_plugin_name(), body->items);
+ program->manga_get_watch_history(program->get_plugin_name(), result_items);
break;
}
- body->filter_search_fuzzy(search_bar->get_text());
- int item_to_revert_selection_to = get_body_item_by_url(body, selected_item_url);
- if(item_to_revert_selection_to != -1)
- body->set_selected_item(item_to_revert_selection_to, false);
+ return PluginResult::OK;
}
+ bool reload_on_page_change() override { return true; }
private:
Page *search_page;
- SearchBar *search_bar;
HistoryType history_type;
};
- class RecommendedPage : public Page {
- public:
- RecommendedPage(Program *program, Page *search_page, SearchBar *search_bar, const char *plugin_name) : Page(program), search_page(search_page), search_bar(search_bar), plugin_name(plugin_name) {}
- const char* get_title() const override { return "Recommended"; }
- PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override {
- return search_page->submit(title, url, result_tabs);
- }
- void on_navigate_to_page(Body *body) override {
- std::string selected_item_url = body->get_selected() ? body->get_selected()->url : "";
- body->clear_items();
- fill_recommended_items_from_json(plugin_name, program->load_recommended_json(), body->items);
- body->filter_search_fuzzy(search_bar->get_text());
- int item_to_revert_selection_to = get_body_item_by_url(body, selected_item_url);
- if(item_to_revert_selection_to != -1)
- body->set_selected_item(item_to_revert_selection_to, false);
- }
- private:
- Page *search_page;
- SearchBar *search_bar;
- const char *plugin_name;
- };
-
Program::Program() :
disp(nullptr),
window_size(1280, 720),
@@ -978,47 +832,41 @@ namespace QuickMedia {
} else if(strcmp(plugin_name, "manganelo") == 0) {
tabs.push_back(Tab{create_body(), std::make_unique<ManganeloSearchPage>(this), create_search_bar("Search...", 400)});
- auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA);
- tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "manganelos") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "http://manganelos.com/");
add_manganelos_handlers(search_page.get());
tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
- auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA);
- tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "mangatown") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://www.mangatown.com/");
add_mangatown_handlers(search_page.get());
tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
- auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA);
- tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "mangakatana") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://mangakatana.com/", false);
add_mangakatana_handlers(search_page.get());
tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
- auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA);
- tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "mangadex") == 0) {
tabs.push_back(Tab{create_body(), std::make_unique<MangadexSearchPage>(this), create_search_bar("Search...", 400)});
- auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA);
- tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "readm") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://readm.org/");
add_readm_handlers(search_page.get());
tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
- auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA);
- tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "manga") == 0) {
auto manganelo = std::make_unique<ManganeloSearchPage>(this);
auto manganelos = std::make_unique<MangaGenericSearchPage>(this, "manganelos", "http://manganelos.com/");
@@ -1069,13 +917,11 @@ namespace QuickMedia {
tabs.push_back(Tab{create_body(), std::make_unique<YoutubeSearchPage>(this), create_search_bar("Search...", 350)});
auto history_body = create_body();
- auto history_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), history_search_bar.get(), HistoryType::YOUTUBE);
- tabs.push_back(Tab{std::move(history_body), std::move(history_page), std::move(history_search_bar)});
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::YOUTUBE);
+ tabs.push_back(Tab{std::move(history_body), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
- auto recommended_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER);
- auto recommended_page = std::make_unique<RecommendedPage>(this, tabs.front().page.get(), recommended_search_bar.get(), plugin_name);
- tabs.push_back(Tab{create_body(), std::move(recommended_page), std::move(recommended_search_bar)});
+ auto recommended_page = std::make_unique<YoutubeRecommendedPage>(this);
+ tabs.push_back(Tab{create_body(), std::move(recommended_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "pornhub") == 0) {
auto search_page = std::make_unique<MediaGenericSearchPage>(this, "https://www.pornhub.com/", sf::Vector2i(320/1.5f, 180/1.5f));
add_pornhub_handlers(search_page.get());
@@ -1267,16 +1113,6 @@ namespace QuickMedia {
return json_result;
}
- // This is not cached because we could have multiple instances of QuickMedia running the same plugin!
- // TODO: Find a way to optimize this
- Json::Value Program::load_recommended_json() {
- Path recommended_filepath = get_recommended_filepath(plugin_name);
- Json::Value json_result;
- if(!read_file_as_json(recommended_filepath, json_result) || !json_result.isObject())
- json_result = Json::Value(Json::objectValue);
- return json_result;
- }
-
void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items) {
// TOOD: Make generic, instead of checking for plugin
Path content_storage_dir = get_storage_dir().join(plugin_name);
@@ -1559,13 +1395,16 @@ namespace QuickMedia {
if(after_submit_handler)
after_submit_handler(new_tabs);
- for(Tab &tab : tabs) {
- tab.body->clear_cache();
+ for(size_t i = 0; i < tabs.size(); ++i) {
+ tabs[i].body->clear_cache();
+ if(tabs[i].page->is_lazy_fetch_page() && static_cast<LazyFetchPage*>(tabs[i].page.get())->reload_on_page_change())
+ tab_associated_data[i].lazy_fetch_finished = false;
}
if(tabs[selected_tab].page->allow_submit_no_selection()) {
page_loop(new_tabs, 0, after_submit_handler);
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) {
+ page_stack.push(current_page);
select_episode(selected_item.get(), false);
Body *chapters_body = tabs[selected_tab].body.get();
chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter
@@ -1608,9 +1447,11 @@ namespace QuickMedia {
window.setKeyRepeatEnabled(true);
malloc_trim(0);
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::IMAGE_BOARD_THREAD) {
+ page_stack.push(current_page);
current_page = PageType::IMAGE_BOARD_THREAD;
image_board_thread_page(static_cast<ImageBoardThreadPage*>(new_tabs[0].page.get()), new_tabs[0].body.get());
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) {
+ page_stack.push(current_page);
current_page = PageType::VIDEO_CONTENT;
int selected_index = tabs[selected_tab].body->get_selected_item();
video_content_page(tabs[selected_tab].page.get(), static_cast<VideoPage*>(new_tabs[0].page.get()), selected_item->get_title(), false, tabs[selected_tab].body->items, selected_index, &tab_associated_data[selected_tab].fetched_page, tab_associated_data[selected_tab].update_search_text);
@@ -2004,68 +1845,6 @@ namespace QuickMedia {
return true;
}
- void Program::save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const BodyItems &related_media_body_items) {
- std::string video_id;
- if(!youtube_url_extract_id(video_url, video_id)) {
- std::string err_msg = "Failed to extract id of youtube url ";
- err_msg += video_url;
- err_msg + ", video wont be saved in recommendations";
- show_notification("QuickMedia", err_msg.c_str(), Urgency::LOW);
- return;
- }
-
- Json::Value recommended_json = load_recommended_json();
- time_t time_now = time(NULL);
-
- Json::Value &existing_recommended_json = recommended_json[video_id];
- if(existing_recommended_json.isObject()) {
- int64_t watched_count = 0;
- Json::Value &watched_count_json = existing_recommended_json["watched_count"];
- if(watched_count_json.isNumeric())
- watched_count = watched_count_json.asInt64();
- existing_recommended_json["watched_count"] = watched_count + 1;
- existing_recommended_json["watched_timestamp"] = time_now;
- } else {
- Json::Value new_content_object(Json::objectValue);
- new_content_object["title"] = video_title;
- new_content_object["recommended_timestamp"] = time_now;
- new_content_object["recommended_count"] = 1;
- new_content_object["watched_count"] = 1;
- new_content_object["watched_timestamp"] = time_now;
- recommended_json[video_id] = std::move(new_content_object);
- }
-
- int saved_recommendation_count = 0;
- for(const auto &body_item : related_media_body_items) {
- std::string recommended_video_id;
- if(youtube_url_extract_id(body_item->url, recommended_video_id)) {
- Json::Value &existing_recommendation = recommended_json[recommended_video_id];
- if(existing_recommendation.isObject()) {
- int64_t recommended_count = 0;
- Json::Value &count_json = existing_recommendation["recommended_count"];
- if(count_json.isNumeric())
- recommended_count = count_json.asInt64();
- existing_recommendation["recommended_count"] = recommended_count + 1;
- existing_recommendation["recommended_timestamp"] = time_now;
- } else {
- Json::Value new_content_object(Json::objectValue);
- new_content_object["title"] = body_item->get_title();
- new_content_object["recommended_timestamp"] = time_now;
- new_content_object["recommended_count"] = 1;
- recommended_json[recommended_video_id] = std::move(new_content_object);
- saved_recommendation_count++;
- /* TODO: Save more than the first 3 video that hasn't been watched yet? */
- if(saved_recommendation_count == 3)
- break;
- }
- } else {
- fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", video_url.c_str());
- }
- }
-
- save_json_to_file_atomic(get_recommended_filepath(plugin_name), recommended_json);
- }
-
static const char *useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
static int accumulate_string_limit_head(char *data, int size, void *userdata) {
@@ -2145,7 +1924,6 @@ namespace QuickMedia {
void Program::video_content_page(Page *parent_page, VideoPage *video_page, std::string video_title, bool download_if_streaming_fails, BodyItems &next_play_items, int play_index, int *parent_body_page, const std::string &parent_page_search) {
sf::Clock time_watched_timer;
- bool added_recommendations = false;
bool video_loaded = false;
const bool is_youtube = strcmp(plugin_name, "youtube") == 0;
const bool is_matrix = strcmp(plugin_name, "matrix") == 0;
@@ -2224,11 +2002,10 @@ namespace QuickMedia {
std::function<void(const char*)> video_event_callback;
- auto load_video_error_check = [this, &related_videos, &channel_url, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, &added_recommendations, video_page, &video_event_callback, &on_window_create, &video_player_window, is_matrix](bool resume_video) mutable {
+ auto load_video_error_check = [this, &related_videos, &channel_url, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_matrix](bool resume_video) mutable {
time_watched_timer.restart();
video_loaded = false;
video_player_window = None;
- added_recommendations = false;
watched_videos.insert(video_url);
video_player = std::make_unique<VideoPlayer>(no_video, use_system_mpv_config, resume_video, is_matrix, video_event_callback, on_window_create, resources_root, get_largest_monitor_height(disp));
@@ -2394,7 +2171,7 @@ namespace QuickMedia {
video_player->quit_and_save_watch_later();
while(true) {
VideoPlayer::Error update_err = video_player->update();
- if(update_err != VideoPlayer::Error::OK)
+ if(update_err != VideoPlayer::Error::OK || !window.isOpen() || current_page == PageType::EXIT)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
@@ -2404,6 +2181,9 @@ namespace QuickMedia {
}
});
+ if(!window.isOpen() || current_page == PageType::EXIT)
+ return;
+
if(page_changed) {
current_page = PageType::VIDEO_CONTENT;
//video_player = std::make_unique<VideoPlayer>(no_video, use_system_mpv_config, true, video_event_callback, on_window_create, resources_root);
@@ -2500,12 +2280,6 @@ namespace QuickMedia {
continue;
}
- /* Only save recommendations for the video if we have been watching it for 15 seconds */
- if(is_youtube && video_loaded && !added_recommendations && time_watched_timer.getElapsedTime().asSeconds() >= 15) {
- added_recommendations = true;
- save_recommendations_from_related_videos(video_url, video_title, related_videos);
- }
-
if(video_player_window) {
if(!cursor_visible) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
index a6f3640..5f860d2 100644
--- a/src/VideoPlayer.cpp
+++ b/src/VideoPlayer.cpp
@@ -63,7 +63,20 @@ namespace QuickMedia {
XCloseDisplay(display);
}
- VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string&, const std::string&) {
+ static std::string escape_quotes(const std::string &str) {
+ std::string result;
+ for(char c : str) {
+ if(c == '"')
+ result += "\\\"";
+ else if(c == '\\')
+ result += "\\\\";
+ else
+ result += c;
+ }
+ return result;
+ }
+
+ VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &) {
parent_window = _parent_window;
if(!tmpnam(ipc_server_path)) {
@@ -71,6 +84,10 @@ namespace QuickMedia {
return Error::FAIL_TO_GENERATE_IPC_FILENAME;
}
+ Path cookies_filepath;
+ if(get_cookies_filepath(cookies_filepath, plugin_name) != 0)
+ fprintf(stderr, "Failed to create %s cookies filepath\n", plugin_name.c_str());
+
const std::string parent_window_str = std::to_string(parent_window);
std::vector<const char*> args;
@@ -94,6 +111,8 @@ namespace QuickMedia {
else
ytdl_format = "--ytdl-format=bestvideo[height<=?" + std::to_string(monitor_height) + "]+bestaudio/best";
+ std::string cookies_file_arg = "--cookies-file=" + cookies_filepath.data;
+
// TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched
args.insert(args.end(), {
"mpv",
@@ -105,11 +124,12 @@ namespace QuickMedia {
cache_dir.c_str(),
watch_later_dir.c_str(),
"--cache-on-disk=yes",
- "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub=",
ytdl_format.c_str(),
// TODO: Disable hr seek on low power devices?
"--hr-seek=yes",
"--gpu-context=x11egl",
+ "--cookies",
+ cookies_file_arg.c_str(),
input_conf.c_str(),
wid_arg.c_str()
});
@@ -129,18 +149,14 @@ namespace QuickMedia {
});
}
- /*
std::string ytdl_options_arg;
- if(!plugin_name.empty()) {
- Path cookies_filepath;
- if(get_cookies_filepath(cookies_filepath, plugin_name) != 0) {
- fprintf(stderr, "Warning: Failed to create %s cookies file\n", plugin_name.c_str());
- } else {
- ytdl_options_arg = "--ytdl-raw-options=cookies=" + cookies_filepath.data;
- args.push_back(ytdl_options_arg.c_str());
- }
+ if(plugin_name.empty()) {
+ ytdl_options_arg = "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub=";
+ args.push_back(ytdl_options_arg.c_str());
+ } else {
+ ytdl_options_arg = "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub=,mark-watched=,cookies=\"" + escape_quotes(cookies_filepath.data) + "\"";
+ args.push_back(ytdl_options_arg.c_str());
}
- */
if(no_video)
args.push_back("--no-video");
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index 09568f6..0f5e807 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -339,12 +339,6 @@ namespace QuickMedia {
return (c >= 8 && c <= 13) || c == ' ';
}
- static void remove_cookies_file_at_exit() {
- std::lock_guard<std::mutex> lock(cookies_mutex);
- if(!cookies_filepath.empty())
- remove(cookies_filepath.c_str());
- }
-
// TODO: Cache this and redownload it when a network request fails with this api key?
static std::string youtube_page_find_api_key() {
size_t api_key_index;
@@ -387,14 +381,13 @@ namespace QuickMedia {
static std::vector<CommandArg> get_cookies() {
std::lock_guard<std::mutex> lock(cookies_mutex);
if(cookies_filepath.empty()) {
- char filename[] = "/tmp/quickmedia.youtube.cookie.XXXXXX";
- int fd = mkstemp(filename);
- if(fd == -1)
- return {};
- close(fd);
+ Path cookies_filepath_p;
+ if(get_cookies_filepath(cookies_filepath_p, "youtube") != 0) {
+ show_notification("QuickMedia", "Failed to create youtube cookies file", Urgency::CRITICAL);
+ abort();
+ }
- cookies_filepath = filename;
- atexit(remove_cookies_file_at_exit);
+ cookies_filepath = cookies_filepath_p.data;
// TODO: Re-enable this if the api key ever changes in the future
#if 0
@@ -403,10 +396,14 @@ namespace QuickMedia {
api_key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
#endif
- // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments
- const char *args[] = { "curl", "-I", "-s", "-b", cookies_filepath.c_str(), "-c", cookies_filepath.c_str(), "https://www.youtube.com/subscription_manager?disable_polymer=1", nullptr };
- if(exec_program(args, nullptr, nullptr) != 0)
- fprintf(stderr, "Failed to fetch cookies to view youtube comments\n");
+ if(get_file_type(cookies_filepath_p) != FileType::REGULAR) {
+ // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments
+ const char *args[] = { "curl", "-I", "-s", "-b", cookies_filepath.c_str(), "-c", cookies_filepath.c_str(), "https://www.youtube.com/subscription_manager?disable_polymer=1", nullptr };
+ if(exec_program(args, nullptr, nullptr) != 0) {
+ show_notification("QuickMedia", "Failed to fetch cookies to view youtube comments", Urgency::CRITICAL);
+ abort();
+ }
+ }
}
return {
@@ -1451,6 +1448,191 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ PluginResult YoutubeRecommendedPage::get_page(const std::string&, int page, BodyItems &result_items) {
+ while(current_page < page) {
+ PluginResult plugin_result = search_get_continuation(continuation_token, result_items);
+ if(plugin_result != PluginResult::OK) return plugin_result;
+ ++current_page;
+ }
+ return PluginResult::OK;
+ }
+
+ PluginResult YoutubeRecommendedPage::search_get_continuation(const std::string &current_continuation_token, BodyItems &result_items) {
+ std::string next_url = "https://www.youtube.com/?pbj=1&ctoken=" + current_continuation_token;
+
+ std::vector<CommandArg> additional_args = {
+ { "-H", "x-spf-referer: https://www.youtube.com/" },
+ { "-H", "x-youtube-client-name: 1" },
+ { "-H", "x-spf-previous: https://www.youtube.com/" },
+ { "-H", "x-youtube-client-version: 2.20200626.03.00" },
+ { "-H", "referer: https://www.youtube.com/" }
+ };
+
+ std::vector<CommandArg> cookies = get_cookies();
+ additional_args.insert(additional_args.end(), cookies.begin(), cookies.end());
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, next_url, std::move(additional_args), true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+
+ if(!json_root.isArray())
+ return PluginResult::ERR;
+
+ std::string new_continuation_token;
+ for(const Json::Value &json_item : json_root) {
+ if(!json_item.isObject())
+ continue;
+
+ const Json::Value &response_json = json_item["response"];
+ if(!response_json.isObject())
+ continue;
+
+ const Json::Value &on_response_received_actions_json = response_json["onResponseReceivedActions"];
+ if(!on_response_received_actions_json.isArray())
+ continue;
+
+ for(const Json::Value &response_received_command : on_response_received_actions_json) {
+ if(!response_received_command.isObject())
+ continue;
+
+ const Json::Value &append_continuation_items_action_json = response_received_command["appendContinuationItemsAction"];
+ if(!append_continuation_items_action_json.isObject())
+ continue;
+
+ const Json::Value &continuation_items_json = append_continuation_items_action_json["continuationItems"];
+ if(!continuation_items_json.isArray())
+ continue;
+
+ for(const Json::Value &content_item_json : continuation_items_json) {
+ if(!content_item_json.isObject())
+ continue;
+
+ if(new_continuation_token.empty())
+ new_continuation_token = item_section_renderer_get_continuation_token(content_item_json);
+
+ const Json::Value &rich_item_renderer_json = content_item_json["richItemRenderer"];
+ if(!rich_item_renderer_json.isObject())
+ continue;
+
+ const Json::Value &item_content_json = rich_item_renderer_json["content"];
+ if(!item_content_json.isObject())
+ continue;
+
+ const Json::Value &video_renderer_json = item_content_json["videoRenderer"];
+ if(!video_renderer_json.isObject())
+ continue;
+
+ auto body_item = parse_common_video_item(video_renderer_json, added_videos);
+ if(body_item)
+ result_items.push_back(std::move(body_item));
+ }
+ }
+ }
+
+ if(!new_continuation_token.empty())
+ continuation_token = std::move(new_continuation_token);
+
+ return PluginResult::OK;
+ }
+
+ PluginResult YoutubeRecommendedPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr});
+ return PluginResult::OK;
+ }
+
+ PluginResult YoutubeRecommendedPage::lazy_fetch(BodyItems &result_items) {
+ current_page = 0;
+ continuation_token.clear();
+ added_videos.clear();
+
+ std::vector<CommandArg> additional_args = {
+ { "-H", "x-youtube-client-name: 1" },
+ { "-H", "x-youtube-client-version: 2.20200626.03.00" }
+ };
+
+ std::vector<CommandArg> cookies = get_cookies();
+ additional_args.insert(additional_args.end(), cookies.begin(), cookies.end());
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, "https://www.youtube.com/?pbj=1", std::move(additional_args), true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+
+ if(!json_root.isArray())
+ return PluginResult::ERR;
+
+ std::string new_continuation_token;
+ for(const Json::Value &json_item : json_root) {
+ if(!json_item.isObject())
+ continue;
+
+ const Json::Value &response_json = json_item["response"];
+ if(!response_json.isObject())
+ continue;
+
+ const Json::Value &contents_json = response_json["contents"];
+ if(!contents_json.isObject())
+ continue;
+
+ const Json::Value &tcbrr_json = contents_json["twoColumnBrowseResultsRenderer"];
+ if(!tcbrr_json.isObject())
+ continue;
+
+ const Json::Value &tabs_json = tcbrr_json["tabs"];
+ if(!tabs_json.isArray())
+ continue;
+
+ for(const Json::Value &tab_json : tabs_json) {
+ if(!tab_json.isObject())
+ continue;
+
+ const Json::Value &tab_renderer_json = tab_json["tabRenderer"];
+ if(!tab_renderer_json.isObject())
+ continue;
+
+ const Json::Value &content_json = tab_renderer_json["content"];
+ if(!content_json.isObject())
+ continue;
+
+ const Json::Value &rich_grid_renderer_json = content_json["richGridRenderer"];
+ if(!rich_grid_renderer_json.isObject())
+ continue;
+
+ const Json::Value &contents2_json = rich_grid_renderer_json["contents"];
+ if(!contents2_json.isArray())
+ continue;
+
+ for(const Json::Value &content_item_json : contents2_json) {
+ if(!content_item_json.isObject())
+ continue;
+
+ if(new_continuation_token.empty())
+ new_continuation_token = item_section_renderer_get_continuation_token(content_item_json);
+
+ const Json::Value &rich_item_renderer_json = content_item_json["richItemRenderer"];
+ if(!rich_item_renderer_json.isObject())
+ continue;
+
+ const Json::Value &item_content_json = rich_item_renderer_json["content"];
+ if(!item_content_json.isObject())
+ continue;
+
+ const Json::Value &video_renderer_json = item_content_json["videoRenderer"];
+ if(!video_renderer_json.isObject())
+ continue;
+
+ auto body_item = parse_common_video_item(video_renderer_json, added_videos);
+ if(body_item)
+ result_items.push_back(std::move(body_item));
+ }
+ }
+ }
+
+ if(!new_continuation_token.empty())
+ continuation_token = std::move(new_continuation_token);
+
+ return PluginResult::OK;
+ }
+
PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr});
return PluginResult::OK;
@@ -1512,8 +1694,8 @@ namespace QuickMedia {
if(!json_item.isObject())
continue;
+ const Json::Value &player_response_json = json_item["playerResponse"];
if(channel_url.empty()) {
- const Json::Value &player_response_json = json_item["playerResponse"];
if(player_response_json.isObject()) {
const Json::Value &video_details_json = player_response_json["videoDetails"];
if(video_details_json.isObject()) {
@@ -1530,6 +1712,38 @@ namespace QuickMedia {
xsrf_token = xsrf_token_json.asString();
}
+ if(player_response_json.isObject()) {
+ const Json::Value &playback_tracing_json = player_response_json["playbackTracking"];
+ if(playback_tracing_json.isObject()) {
+ if(playback_url.empty()) {
+ const Json::Value &video_stats_playback_url_json = playback_tracing_json["videostatsPlaybackUrl"];
+ if(video_stats_playback_url_json.isObject()) {
+ const Json::Value &base_url_json = video_stats_playback_url_json["baseUrl"];
+ if(base_url_json.isString())
+ playback_url = base_url_json.asString();
+ }
+ }
+
+ if(watchtime_url.empty()) {
+ const Json::Value &video_stats_watchtime_url_json = playback_tracing_json["videostatsWatchtimeUrl"];
+ if(video_stats_watchtime_url_json.isObject()) {
+ const Json::Value &base_url_json = video_stats_watchtime_url_json["baseUrl"];
+ if(base_url_json.isString())
+ watchtime_url = base_url_json.asString();
+ }
+ }
+
+ if(tracking_url.empty()) {
+ const Json::Value &p_tracking_url_json = playback_tracing_json["ptrackingUrl"];
+ if(p_tracking_url_json.isObject()) {
+ const Json::Value &base_url_json = p_tracking_url_json["baseUrl"];
+ if(base_url_json.isString())
+ tracking_url = base_url_json.asString();
+ }
+ }
+ }
+ }
+
const Json::Value &response_json = json_item["response"];
if(!response_json.isObject())
continue;