aboutsummaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/Youtube.cpp145
1 files changed, 107 insertions, 38 deletions
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<MediaChapter> &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<MediaChapter> &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(