From e8cf95fd56bb6cc16f937d06c3554260fd789a92 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 3 Apr 2024 19:37:55 +0200 Subject: Youtube: fallback to yt-dlp, add support for sponsorblock (as chapters) --- src/plugins/Youtube.cpp | 145 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 38 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index fef5fce..482843e 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -2857,6 +2857,12 @@ namespace QuickMedia { } std::string YoutubeVideoPage::get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) { + if(use_youtube_dl_fallback) { + has_embedded_audio = youtube_dl_audio_fallback_url.empty(); + ext = ".webm"; // TODO: Guess for now + return youtube_dl_video_fallback_url; + } + if(!livestream_url.empty() && video_formats.empty() && audio_formats.empty()) { has_embedded_audio = true; return livestream_url; @@ -2892,6 +2898,11 @@ namespace QuickMedia { } std::string YoutubeVideoPage::get_audio_url(std::string &ext) { + if(use_youtube_dl_fallback) { + ext = ".opus"; // TODO: Guess for now + return youtube_dl_audio_fallback_url; + } + if(audio_formats.empty()) return ""; @@ -3054,6 +3065,56 @@ namespace QuickMedia { video_details.duration = 0.0; } + static void sponsorblock_add_chapters(Page *page, const std::string &url, int min_votes, std::vector &chapters) { + 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()); + return; + } + + const std::string sponsorblock_url = "https://sponsor.ajay.app/api/skipSegments?videoID=" + video_id; + Json::Value json_root; + if(page->download_json(json_root, sponsorblock_url, {}) != DownloadResult::OK) + return; + + if(!json_root.isArray()) + return; + + for(const Json::Value &item_json : json_root) { + if(!item_json.isObject()) + continue; + + const Json::Value &category_json = item_json["category"]; + const Json::Value &action_type_json = item_json["actionType"]; + const Json::Value &segment_json = item_json["segment"]; + const Json::Value &votes_json = item_json["votes"]; + if(!category_json.isString() || !action_type_json.isString() || !segment_json.isArray() || !votes_json.isInt()) + continue; + + if(strcmp(category_json.asCString(), "sponsor") != 0 || strcmp(action_type_json.asCString(), "skip") != 0) + continue; + + if(segment_json.size() != 2) + continue; + + if(!segment_json[0].isDouble() || !segment_json[1].isDouble()) + continue; + + 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)); + } + } + PluginResult YoutubeVideoPage::parse_video_response(const Json::Value &json_root, std::string &title, std::string &channel_url, std::vector &chapters, std::string &err_str) { livestream_url.clear(); video_formats.clear(); @@ -3062,45 +3123,13 @@ namespace QuickMedia { title.clear(); channel_url.clear(); chapters.clear(); + youtube_dl_video_fallback_url.clear(); + youtube_dl_audio_fallback_url.clear(); video_details_clear(video_details); if(!json_root.isObject()) return PluginResult::ERR; - const Json::Value &playability_status_json = json_root["playabilityStatus"]; - if(playability_status_json.isObject()) { - const Json::Value &status_json = playability_status_json["status"]; - if(status_json.isString() && (strcmp(status_json.asCString(), "UNPLAYABLE") == 0 || strcmp(status_json.asCString(), "LOGIN_REQUIRED") == 0)) { - const Json::Value &reason_json = playability_status_json["reason"]; - if(reason_json.isString()) - err_str = reason_json.asString(); - fprintf(stderr, "Unable to play video, status: %s, reason: %s\n", status_json.asCString(), reason_json.isString() ? reason_json.asCString() : "Unknown"); - return PluginResult::ERR; - } - } - - const Json::Value *streaming_data_json = &json_root["streamingData"]; - if(!streaming_data_json->isObject()) - return PluginResult::ERR; - - // TODO: Verify if this always works (what about copyrighted live streams?), also what about choosing video quality for live stream? Maybe use mpv --hls-bitrate option? - const Json::Value &hls_manifest_url_json = (*streaming_data_json)["hlsManifestUrl"]; - if(hls_manifest_url_json.isString()) - livestream_url = hls_manifest_url_json.asString(); - - /* - const Json::Value &dash_manifest_url_json = (*streaming_data_json)["dashManifestUrl"]; - if(livestream_url.empty() && dash_manifest_url_json.isString()) { - // TODO: mpv cant properly play dash videos. Video goes back and replays. - // So for now return here (get_video_info only hash dash stream and no hls stream) which will fallback to the player youtube endpoint which has hls stream. - return PluginResult::ERR; - } - */ - - parse_formats(*streaming_data_json); - if(video_formats.empty() && audio_formats.empty() && livestream_url.empty()) - return PluginResult::ERR; - const Json::Value &video_details_json = json_root["videoDetails"]; if(video_details_json.isObject()) { const Json::Value &channel_id_json = video_details_json["channelId"]; @@ -3166,6 +3195,48 @@ namespace QuickMedia { } } + const Json::Value &playability_status_json = json_root["playabilityStatus"]; + if(playability_status_json.isObject()) { + const Json::Value &status_json = playability_status_json["status"]; + if(status_json.isString() && (strcmp(status_json.asCString(), "UNPLAYABLE") == 0 || strcmp(status_json.asCString(), "LOGIN_REQUIRED") == 0)) { + 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); + use_youtube_dl_fallback = true; + return PluginResult::OK; + } else { + const Json::Value &reason_json = playability_status_json["reason"]; + if(reason_json.isString()) + err_str = reason_json.asString(); + fprintf(stderr, "Unable to play video, status: %s, reason: %s\n", status_json.asCString(), reason_json.isString() ? reason_json.asCString() : "Unknown"); + return PluginResult::ERR; + } + } + } + + const Json::Value *streaming_data_json = &json_root["streamingData"]; + if(!streaming_data_json->isObject()) + return PluginResult::ERR; + + // TODO: Verify if this always works (what about copyrighted live streams?), also what about choosing video quality for live stream? Maybe use mpv --hls-bitrate option? + const Json::Value &hls_manifest_url_json = (*streaming_data_json)["hlsManifestUrl"]; + if(hls_manifest_url_json.isString()) + livestream_url = hls_manifest_url_json.asString(); + + /* + const Json::Value &dash_manifest_url_json = (*streaming_data_json)["dashManifestUrl"]; + if(livestream_url.empty() && dash_manifest_url_json.isString()) { + // TODO: mpv cant properly play dash videos. Video goes back and replays. + // So for now return here (get_video_info only hash dash stream and no hls stream) which will fallback to the player youtube endpoint which has hls stream. + return PluginResult::ERR; + } + */ + + parse_formats(*streaming_data_json); + if(video_formats.empty() && audio_formats.empty() && livestream_url.empty()) + return PluginResult::ERR; + std::sort(video_formats.begin(), video_formats.end(), [](const YoutubeVideoFormat &format1, const YoutubeVideoFormat &format2) { return format1.base.bitrate > format2.base.bitrate; }); @@ -3174,6 +3245,8 @@ namespace QuickMedia { return format1.base.bitrate > format2.base.bitrate; }); + if(get_config().youtube.sponsorblock.enable) + sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, chapters); return PluginResult::OK; } @@ -3184,10 +3257,6 @@ namespace QuickMedia { return PluginResult::ERR; } - // The first one works for copyrighted videos and regular videos but only if they can be embedded. - // The second one works for age restricted videos and regular videos but only if they can be embedded. It doesn't work for copyrighted videos. - // The third one works for all non-copyrighted, non-age restricted videos, embeddable or not. - const int num_request_types = 1; std::string request_data[num_request_types] = { R"END( -- cgit v1.2.3