From 4c37a5463c23fcdb165228aae096f2bb0a25b98b Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 3 Apr 2024 20:49:37 +0200 Subject: Autoskip youtube sponsors --- include/VideoPlayer.hpp | 5 +++-- plugins/Page.hpp | 6 ++++++ plugins/Youtube.hpp | 2 +- src/QuickMedia.cpp | 29 +++++++++++++++++++++++++++-- src/VideoPlayer.cpp | 20 +++++++++++++++++--- src/plugins/Youtube.cpp | 21 +++++++-------------- video_player/src/main.cpp | 25 ++++++++++++++++++++++++- 7 files changed, 85 insertions(+), 23 deletions(-) diff --git a/include/VideoPlayer.hpp b/include/VideoPlayer.hpp index 5dc491a..79de8cd 100644 --- a/include/VideoPlayer.hpp +++ b/include/VideoPlayer.hpp @@ -66,8 +66,9 @@ namespace QuickMedia { Error update(); // Returns time in seconds - Error get_time_in_file(double *result); - Error get_duration_in_file(double *result); + Error get_time_in_file(double *result_seconds); + Error set_time_in_file(double time_in_seconds); + Error get_duration(double *result); Error add_subtitle(const std::string &url, const std::string &title, const std::string &lang); Error cycle_fullscreen(); diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 97b0340..044759a 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -134,12 +134,18 @@ namespace QuickMedia { std::string title; }; + struct SponsorSegment { + double start_seconds; + double end_seconds; + }; + struct VideoInfo { std::string title; std::string channel_url; double duration = 0.0; std::vector chapters; std::string referer; + std::vector sponsor_segments; }; class VideoPage : public Page { diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index ac92526..d7008ae 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -187,7 +187,7 @@ namespace QuickMedia { void set_watch_progress(int64_t time_pos_sec, int64_t duration_sec) override; bool autoplay_next_item() override { return goto_next_item; } private: - PluginResult parse_video_response(const Json::Value &json_root, std::string &title, std::string &channel_url, std::vector &chapters, std::string &err_str); + PluginResult parse_video_response(const Json::Value &json_root, std::string &title, std::string &channel_url, std::vector &chapters, std::vector &sponsor_segments, std::string &err_str); void parse_format(const Json::Value &format_json, bool is_adaptive); void parse_formats(const Json::Value &streaming_data_json); private: diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c57540c..ac236b4 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -3252,6 +3252,17 @@ namespace QuickMedia { return url.find("yt_live_broadcast") != std::string::npos || url.find("manifest/") != std::string::npos; } + static bool find_sponsor_segment_end(const std::vector &sponsor_segments, double current_time_seconds, double &end_time_seconds) { + // TODO: Optimize + for(const SponsorSegment &sponsor_segments : sponsor_segments) { + if(current_time_seconds >= sponsor_segments.start_seconds && current_time_seconds <= sponsor_segments.end_seconds) { + end_time_seconds = sponsor_segments.end_seconds; + return true; + } + } + return false; + } + int Program::video_get_max_height() { if(video_max_height > 0) return video_max_height; @@ -3326,13 +3337,15 @@ namespace QuickMedia { update_duration = false; successfully_fetched_video_duration = false; double file_duration = 0.0; - video_player->get_duration_in_file(&file_duration); + video_player->get_duration(&file_duration); video_info.duration = std::max(video_info.duration, file_duration); if(video_info.duration > 0.001) successfully_fetched_video_duration = true; } }; + const bool use_sponsorblock = get_config().youtube.sponsorblock.enable; + mgl::Clock sponsorblock_update_clock; auto update_time_pos_handler = [&](bool force) { if(!video_player) return; @@ -3356,6 +3369,18 @@ namespace QuickMedia { video_page->set_watch_progress(video_time_pos, video_info.duration); } } + + if(use_sponsorblock && (force || sponsorblock_update_clock.get_elapsed_time_seconds() >= 1.0)) { + sponsorblock_update_clock.restart(); + double time_pos = 0.0; + if(video_player->get_time_in_file(&time_pos) == VideoPlayer::Error::OK) { + double sponsor_end = 0.0; + if(find_sponsor_segment_end(video_info.sponsor_segments, time_pos, sponsor_end)) { + fprintf(stderr, "Info: skipped sponsor segment\n"); + video_player->set_time_in_file(sponsor_end); + } + } + } update_video_duration_handler(); }; @@ -3492,7 +3517,7 @@ namespace QuickMedia { startup_args.use_youtube_dl = use_youtube_dl && !video_page->is_local(); startup_args.title = video_title; startup_args.start_time = start_time; - startup_args.chapters = std::move(video_info.chapters); + startup_args.chapters = video_info.chapters; startup_args.plugin_name = plugin_name; startup_args.cache_on_disk = !video_page->is_local(); startup_args.referer = video_info.referer; diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index 5369a07..1049bcf 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -48,6 +48,7 @@ namespace QuickMedia { static const double READ_TIMEOUT_SEC = 3.0; static std::string media_chapters_to_ffmetadata_chapters(std::vector chapters) { + // Required for mpv to display chapters correctly std::sort(chapters.begin(), chapters.end(), [](const MediaChapter &a, const MediaChapter &b) { return a.start_seconds < b.start_seconds; }); @@ -460,7 +461,7 @@ namespace QuickMedia { return Error::OK; } - VideoPlayer::Error VideoPlayer::get_time_in_file(double *result) { + VideoPlayer::Error VideoPlayer::get_time_in_file(double *result_seconds) { Json::Value json_root(Json::objectValue); json_root["command"] = "time-pos"; @@ -469,11 +470,24 @@ namespace QuickMedia { if(err != Error::OK) return err; - *result = time_pos_json.asDouble(); + *result_seconds = time_pos_json.asDouble(); return err; } - VideoPlayer::Error VideoPlayer::get_duration_in_file(double *result) { + VideoPlayer::Error VideoPlayer::set_time_in_file(double time_in_seconds) { + Json::Value json_root(Json::objectValue); + json_root["command"] = "set-time-pos"; + json_root["data"] = time_in_seconds; + + Json::Value time_pos_json; + Error err = send_command(json_root, &time_pos_json, Json::ValueType::realValue); + if(err != Error::OK) + return err; + + return err; + } + + VideoPlayer::Error VideoPlayer::get_duration(double *result) { Json::Value json_root(Json::objectValue); json_root["command"] = "duration"; diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 482843e..bcb6cf6 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -3065,7 +3065,7 @@ namespace QuickMedia { video_details.duration = 0.0; } - static void sponsorblock_add_chapters(Page *page, const std::string &url, int min_votes, std::vector &chapters) { + static void sponsorblock_add_chapters(Page *page, const std::string &url, int min_votes, std::vector &sponsor_segments) { std::string video_id; if(!youtube_url_extract_id(url, video_id)) { fprintf(stderr, "Failed to extract youtube id from %s\n", url.c_str()); @@ -3103,19 +3103,11 @@ namespace QuickMedia { if(votes_json.asInt() < min_votes) continue; - MediaChapter ad_start; - ad_start.start_seconds = segment_json[0].asDouble(); - ad_start.title = "Ad start"; - chapters.push_back(std::move(ad_start)); - - MediaChapter ad_end; - ad_end.start_seconds = segment_json[1].asDouble(); - ad_end.title = "Ad end"; - chapters.push_back(std::move(ad_end)); + sponsor_segments.push_back({segment_json[0].asDouble(), segment_json[1].asDouble()}); } } - PluginResult YoutubeVideoPage::parse_video_response(const Json::Value &json_root, std::string &title, std::string &channel_url, std::vector &chapters, std::string &err_str) { + PluginResult YoutubeVideoPage::parse_video_response(const Json::Value &json_root, std::string &title, std::string &channel_url, std::vector &chapters, std::vector &sponsor_segments, std::string &err_str) { livestream_url.clear(); video_formats.clear(); audio_formats.clear(); @@ -3123,6 +3115,7 @@ namespace QuickMedia { title.clear(); channel_url.clear(); chapters.clear(); + sponsor_segments.clear(); youtube_dl_video_fallback_url.clear(); youtube_dl_audio_fallback_url.clear(); video_details_clear(video_details); @@ -3202,7 +3195,7 @@ namespace QuickMedia { fprintf(stderr, "Failed to load youtube video, trying with yt-dlp instead\n"); if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) { if(get_config().youtube.sponsorblock.enable) - sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, chapters); + sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments); use_youtube_dl_fallback = true; return PluginResult::OK; } else { @@ -3246,7 +3239,7 @@ namespace QuickMedia { }); if(get_config().youtube.sponsorblock.enable) - sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, chapters); + sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments); return PluginResult::OK; } @@ -3290,7 +3283,7 @@ R"END( if(download_result != DownloadResult::OK) continue; - PluginResult result = parse_video_response(json_root, video_info.title, video_info.channel_url, video_info.chapters, err_str); + PluginResult result = parse_video_response(json_root, video_info.title, video_info.channel_url, video_info.chapters, video_info.sponsor_segments, err_str); if(result == PluginResult::OK) { err_str.clear(); video_info.duration = video_details.duration; diff --git a/video_player/src/main.cpp b/video_player/src/main.cpp index 0342b45..54b5bb4 100644 --- a/video_player/src/main.cpp +++ b/video_player/src/main.cpp @@ -51,6 +51,27 @@ static Json::Value handle_json_command_time_pos(mpv_handle *mpv_ctx) { return response_json; } +static Json::Value handle_json_command_set_time_pos(mpv_handle *mpv_ctx, const Json::Value &json_root) { + Json::Value response_json(Json::objectValue); + + const Json::Value &data_json = json_root["data"]; + if(!data_json.isDouble()) { + response_json["status"] = "error"; + response_json["message"] = "expected \"data\" to be a double"; + return response_json; + } + + double time_pos = data_json.asDouble(); + const int res = mpv_set_property_async(mpv_ctx, 0, "time-pos", MPV_FORMAT_DOUBLE, &time_pos); + if(res < 0) { + response_json["status"] = "error"; + response_json["message"] = std::string("set-time-pos: ") + mpv_error_string(res); + } else { + response_json["status"] = "success"; + } + return response_json; +} + static Json::Value handle_json_command_duration(mpv_handle *mpv_ctx) { double duration = 0.0; const int res = mpv_get_property(mpv_ctx, "duration", MPV_FORMAT_DOUBLE, &duration); @@ -183,6 +204,8 @@ static void handle_json_command(mpv_handle *mpv_ctx, const Json::Value &json_roo Json::Value response_json; if(strcmp(command_json.asCString(), "time-pos") == 0) { response_json = handle_json_command_time_pos(mpv_ctx); + } else if(strcmp(command_json.asCString(), "set-time-pos") == 0) { + response_json = handle_json_command_set_time_pos(mpv_ctx, json_root); } else if(strcmp(command_json.asCString(), "duration") == 0) { response_json = handle_json_command_duration(mpv_ctx); } else if(strcmp(command_json.asCString(), "sub-add") == 0) { @@ -192,7 +215,7 @@ static void handle_json_command(mpv_handle *mpv_ctx, const Json::Value &json_roo } else { response_json = Json::Value(Json::objectValue); response_json["status"] = "error"; - response_json["message"] = "invalid command " + command_json.asString() + ", expected time-pos, duration, sub-add or cycle-fullscreen"; + response_json["message"] = "invalid command " + command_json.asString() + ", expected time-pos, set-time-pos, duration, sub-add or cycle-fullscreen"; } if(request_id) -- cgit v1.2.3