aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-06-19 13:13:59 +0200
committerdec05eba <dec05eba@protonmail.com>2021-06-19 13:13:59 +0200
commit85c6c916541968f298badb391b718cdf6d81d332 (patch)
treebf69ea6c1d3a67ec7d41786b5cf883f93b3b87c6 /src
parent3f66a053942be531fe124cc16bb6b2e0eb94934e (diff)
Support youtube description chapters
Diffstat (limited to 'src')
-rw-r--r--src/QuickMedia.cpp22
-rw-r--r--src/VideoPlayer.cpp59
-rw-r--r--src/plugins/Youtube.cpp52
3 files changed, 120 insertions, 13 deletions
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index bf6cf7e..a4ea258 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -2412,7 +2412,6 @@ namespace QuickMedia {
if(create_directory_recursive(video_cache_dir) != 0) {
show_notification("QuickMedia", "Failed to create video cache directory", Urgency::CRITICAL);
current_page = previous_page;
- go_to_previous_page = true;
return false;
}
@@ -2435,18 +2434,15 @@ namespace QuickMedia {
case TaskResult::FALSE: {
show_notification("QuickMedia", "Failed to download " + video_url, Urgency::CRITICAL);
current_page = previous_page;
- go_to_previous_page = true;
return false;
}
case TaskResult::CANCEL: {
current_page = previous_page;
- go_to_previous_page = true;
return false;
}
}
} else if(video_is_not_streamble_result == TaskResult::CANCEL) {
current_page = previous_page;
- go_to_previous_page = true;
return false;
}
}
@@ -2496,8 +2492,9 @@ namespace QuickMedia {
const int num_load_tries_max = 3;
int load_try = 0;
std::string prev_start_time;
+ std::vector<MediaChapter> media_chapters;
- auto load_video_error_check = [this, &prev_start_time, &in_seeking, &video_url, &audio_url, &video_title, &video_tasks, &channel_url, previous_page, &go_to_previous_page, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_youtube, is_matrix, download_if_streaming_fails](std::string start_time = "", bool reuse_media_source = false) mutable {
+ auto load_video_error_check = [this, &prev_start_time, &media_chapters, &in_seeking, &video_url, &audio_url, &video_title, &video_tasks, &channel_url, previous_page, &go_to_previous_page, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_youtube, is_matrix, download_if_streaming_fails](std::string start_time = "", bool reuse_media_source = false) mutable {
video_player.reset();
channel_url.clear();
video_loaded = false;
@@ -2510,8 +2507,8 @@ namespace QuickMedia {
if(!reuse_media_source) {
std::string new_title;
- TaskResult load_result = run_task_with_loading_screen([video_page, &new_title, &channel_url]() {
- return video_page->load(new_title, channel_url) == PluginResult::OK;
+ TaskResult load_result = run_task_with_loading_screen([video_page, &new_title, &channel_url, &media_chapters]() {
+ return video_page->load(new_title, channel_url, media_chapters) == PluginResult::OK;
});
if(!new_title.empty())
@@ -2547,8 +2544,10 @@ namespace QuickMedia {
}
if(!is_youtube && download_if_streaming_fails) {
- if(!video_download_if_non_streamable(video_url, audio_url, is_audio_only, has_embedded_audio, previous_page))
+ if(!video_download_if_non_streamable(video_url, audio_url, is_audio_only, has_embedded_audio, previous_page)) {
+ go_to_previous_page = true;
return;
+ }
}
}
@@ -2560,7 +2559,7 @@ namespace QuickMedia {
watched_videos.insert(video_page->get_url());
video_player = std::make_unique<VideoPlayer>(is_audio_only, use_system_mpv_config, is_matrix && !is_youtube, video_event_callback, on_window_create, resources_root, largest_monitor_height);
- VideoPlayer::Error err = video_player->load_video(video_url.c_str(), audio_url.c_str(), window.getSystemHandle(), is_youtube, video_title, start_time);
+ VideoPlayer::Error err = video_player->load_video(video_url.c_str(), audio_url.c_str(), window.getSystemHandle(), is_youtube, video_title, start_time, media_chapters);
if(err != VideoPlayer::Error::OK) {
std::string err_msg = "Failed to play url: ";
err_msg += video_page->get_url();
@@ -3722,6 +3721,7 @@ namespace QuickMedia {
comment_page_scroll_stack.clear();
}
redraw = true;
+ idle_active_handler();
} else {
BodyItem *selected_item = thread_body->get_selected();
if(selected_item && !selected_item->url.empty()) {
@@ -6446,7 +6446,7 @@ namespace QuickMedia {
bool start() override {
remove(output_filepath.c_str());
- std::vector<const char*> args = { "youtube-dl", "--no-warnings", "--no-continue", "--output", output_filepath.c_str(), "--newline" };
+ std::vector<const char*> args = { "youtube-dl", "-f", "bestvideo+bestaudio/best", "--no-warnings", "--no-continue", "--output", output_filepath.c_str(), "--newline" };
if(no_video) args.push_back("-x");
args.insert(args.end(), { "--", url.c_str(), nullptr });
@@ -6573,7 +6573,7 @@ namespace QuickMedia {
if(download_use_youtube_dl) {
task_result = run_task_with_loading_screen([this, url, &filename]{
std::string json_str;
- std::vector<const char*> args = { "youtube-dl", "--skip-download", "--print-json", "--no-warnings" };
+ std::vector<const char*> args = { "youtube-dl", "-f", "bestvideo+bestaudio/best", "--skip-download", "--print-json", "--no-warnings" };
if(no_video) args.push_back("-x");
args.insert(args.end(), { "--", url, nullptr });
if(exec_program(args.data(), accumulate_string, &json_str) != 0)
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
index 9e17bda..1d854de 100644
--- a/src/VideoPlayer.cpp
+++ b/src/VideoPlayer.cpp
@@ -2,6 +2,7 @@
#include "../include/Storage.hpp"
#include "../include/Program.hpp"
#include "../include/Utils.hpp"
+#include "../include/StringUtils.hpp"
#include "../include/Notification.hpp"
#include <string>
#include <json/reader.h>
@@ -20,6 +21,49 @@ const int MAX_RETRIES_CONNECT = 1000;
const int READ_TIMEOUT_MS = 200;
namespace QuickMedia {
+ static std::string media_chapters_to_ffmetadata_chapters(const std::vector<MediaChapter> &chapters) {
+ std::string result = ";FFMETADATA1\n\n";
+ for(size_t i = 0; i < chapters.size(); ++i) {
+ const MediaChapter &chapter = chapters[i];
+ result += "[CHAPTER]\n";
+ result += "TIMEBASE=1/1\n";
+ result += "START=" + std::to_string(chapter.start_seconds) + "\n";
+ result += "END=" + std::to_string(i + 1 == chapters.size() ? chapter.start_seconds : chapters[i].start_seconds) + "\n";
+ std::string title = chapter.title;
+ string_replace_all(title, '\n', ' ');
+ result += "title=" + std::move(title) + "\n\n";
+ }
+ return result;
+ }
+
+ // If |chapters| is empty then |tmp_chapters_filepath| is removed, otherwise the file is overwritten
+ static bool create_tmp_file_with_chapters_data(char *tmp_chapters_filepath, const std::vector<MediaChapter> &chapters) {
+ if(chapters.empty()) {
+ if(tmp_chapters_filepath[0] != '\0') {
+ remove(tmp_chapters_filepath);
+ tmp_chapters_filepath[0] = '\0';
+ }
+ return true;
+ }
+
+ if(tmp_chapters_filepath[0] == '\0') {
+ strcpy(tmp_chapters_filepath, "/tmp/qm-mpv-chapter-XXXXXX");
+ mktemp(tmp_chapters_filepath);
+ if(tmp_chapters_filepath[0] == '\0') {
+ fprintf(stderr, "Failed to create temporay file\n");
+ return false;
+ }
+ }
+
+ if(file_overwrite(tmp_chapters_filepath, media_chapters_to_ffmetadata_chapters(chapters)) == 0) {
+ return true;
+ } else {
+ remove(tmp_chapters_filepath);
+ tmp_chapters_filepath[0] = '\0';
+ return false;
+ }
+ }
+
VideoPlayer::VideoPlayer(bool no_video, bool use_system_mpv_config, bool keep_open, EventCallbackFunc _event_callback, VideoPlayerWindowCreateCallback _window_create_callback, const std::string &resource_root, int monitor_height) :
exit_status(0),
no_video(no_video),
@@ -41,6 +85,7 @@ namespace QuickMedia {
response_data_status(ResponseDataStatus::NONE),
resource_root(resource_root)
{
+ tmp_chapters_filepath[0] = '\0';
display = XOpenDisplay(NULL);
if (!display) {
show_notification("QuickMedia", "Failed to open display to X11 server", Urgency::CRITICAL);
@@ -64,6 +109,9 @@ namespace QuickMedia {
if(display)
XCloseDisplay(display);
+
+ if(tmp_chapters_filepath[0] != '\0')
+ remove(tmp_chapters_filepath);
}
VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, const char *audio_path, sf::WindowHandle _parent_window, bool is_youtube, const std::string &title, const std::string &start_time) {
@@ -141,6 +189,12 @@ namespace QuickMedia {
if(no_video)
args.push_back("--no-video");
+ std::string chapters_file_arg;
+ if(tmp_chapters_filepath[0] != '\0') {
+ chapters_file_arg = std::string("--chapters-file=") + tmp_chapters_filepath;
+ args.push_back(chapters_file_arg.c_str());
+ }
+
std::string audio_file_arg;
if(audio_path && audio_path[0] != '\0') {
audio_file_arg = std::string("--audio-file=") + audio_path;
@@ -171,11 +225,14 @@ namespace QuickMedia {
return Error::OK;
}
- VideoPlayer::Error VideoPlayer::load_video(const char *path, const char *audio_path, sf::WindowHandle _parent_window, bool is_youtube, const std::string &title, const std::string &start_time) {
+ VideoPlayer::Error VideoPlayer::load_video(const char *path, const char *audio_path, sf::WindowHandle _parent_window, bool is_youtube, const std::string &title, const std::string &start_time, const std::vector<MediaChapter> &chapters) {
// This check is to make sure we dont change window that the video belongs to. This is not a usecase we will have so
// no need to support it for now at least.
assert(parent_window == 0 || parent_window == _parent_window);
assert(path);
+ if(!create_tmp_file_with_chapters_data(tmp_chapters_filepath, chapters))
+ fprintf(stderr, "Warning: failed to create chapters file. Chapters will not be displayed\n");
+
fprintf(stderr, "Playing video: %s, audio: %s\n", path ? path : "", audio_path ? audio_path : "");
if(video_process_id == -1)
return launch_video_process(path, audio_path, _parent_window, is_youtube, title, start_time);
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index a8c58cb..b45b5fe 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -5,6 +5,7 @@
#include "../../include/StringUtils.hpp"
#include "../../include/Scale.hpp"
#include "../../include/Notification.hpp"
+#include "../../include/VideoPlayer.hpp"
#include "../../include/Utils.hpp"
#include <json/reader.h>
extern "C" {
@@ -2019,7 +2020,49 @@ R"END(
return chosen_audio_format->base.url;
}
- PluginResult YoutubeVideoPage::load(std::string &title, std::string &channel_url) {
+ // Returns -1 if timestamp is in an invalid format
+ static int youtube_comment_timestamp_to_seconds(const char *str, size_t size) {
+ if(size > 30)
+ return -1;
+
+ char timestamp[32];
+ memcpy(timestamp, str, size);
+ timestamp[size] = '\0';
+
+ int hours = 0;
+ int minutes = 0;
+ int seconds = 0;
+ if(sscanf(timestamp, "%d:%d:%d", &hours, &minutes, &seconds) == 3)
+ return (hours * 60 * 60) + (minutes * 60) + seconds;
+ if(sscanf(timestamp, "%d:%d", &minutes, &seconds) == 2)
+ return (minutes * 60) + seconds;
+ if(sscanf(timestamp, "%d", &seconds) == 1)
+ return seconds;
+ return -1;
+ }
+
+ static std::vector<MediaChapter> youtube_description_extract_chapters(const std::string &description) {
+ std::vector<MediaChapter> result;
+ string_split(description, '\n', [&result](const char *str, size_t size) {
+ const void *first_space_p = memchr(str, ' ', size);
+ if(!first_space_p)
+ return true;
+
+ int timestamp_seconds = youtube_comment_timestamp_to_seconds(str, (const char*)first_space_p - str);
+ if(timestamp_seconds == -1)
+ return true;
+
+ MediaChapter chapter;
+ chapter.start_seconds = timestamp_seconds;
+ chapter.title.assign((const char*)first_space_p, (str + size) - (const char*)first_space_p);
+ chapter.title = strip(chapter.title);
+ result.push_back(std::move(chapter));
+ return true;
+ });
+ return result;
+ }
+
+ PluginResult YoutubeVideoPage::load(std::string &title, std::string &channel_url, std::vector<MediaChapter> &chapters) {
hls_manifest_url.clear();
video_formats.clear();
audio_formats.clear();
@@ -2084,6 +2127,13 @@ R"END(
const Json::Value &channel_id_json = video_details_json["channelId"];
if(channel_id_json.isString())
channel_url = "https://www.youtube.com/channel/" + channel_id_json.asString();
+
+ const Json::Value &description_json = video_details_json["shortDescription"];
+ if(description_json.isString()) {
+ std::string description = description_json.asString();
+ string_replace_all(description, '+', ' ');
+ chapters = youtube_description_extract_chapters(description);
+ }
}
const Json::Value &playback_tracing_json = json_root["playbackTracking"];