aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-06-11 04:51:03 +0200
committerdec05eba <dec05eba@protonmail.com>2021-06-11 04:51:03 +0200
commit89383cff1ba5d8a928262fcb4c40382a981c78c8 (patch)
treeda8b6062cc6770fd37e7a6f7b8c09fb61f46f7b2
parent5b3becc79461d4ecf015e33515871cc09e26e04e (diff)
Remove dependency on youtube-dl for streaming youtube, resulting in faster video startup
-rw-r--r--README.md2
-rw-r--r--TODO10
-rw-r--r--include/AsyncImageLoader.hpp6
-rw-r--r--include/MessageQueue.hpp1
-rw-r--r--include/Program.hpp1
-rw-r--r--include/StringUtils.hpp1
-rw-r--r--include/VideoPlayer.hpp5
-rw-r--r--plugins/ImageBoard.hpp6
-rw-r--r--plugins/Matrix.hpp6
-rw-r--r--plugins/MediaGeneric.hpp6
-rw-r--r--plugins/Page.hpp18
-rw-r--r--plugins/Soundcloud.hpp7
-rw-r--r--plugins/Youtube.hpp39
-rw-r--r--plugins/youtube/Signature.hpp46
-rw-r--r--src/AsyncImageLoader.cpp17
-rw-r--r--src/DownloadUtils.cpp4
-rw-r--r--src/Program.cpp4
-rw-r--r--src/QuickMedia.cpp229
-rw-r--r--src/StringUtils.cpp16
-rw-r--r--src/VideoPlayer.cpp14
-rw-r--r--src/plugins/ImageBoard.cpp2
-rw-r--r--src/plugins/MediaGeneric.cpp2
-rw-r--r--src/plugins/Youtube.cpp269
-rw-r--r--src/plugins/youtube/Signature.cpp307
24 files changed, 860 insertions, 158 deletions
diff --git a/README.md b/README.md
index b64f14f..a8f68c6 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,7 @@ Note that at the moment, cached images will not be scaled with the dpi. Images d
### Optional
`noto-fonts-cjk` needs to be installed to view chinese, japanese and korean characters.\
`mpv` needs to be installed to play videos.\
-`youtube-dl` needs to be installed to play youtube music/videos.\
+`youtube-dl` needs to be installed to download youtube music/videos. (Note: `youtube-dl` is not required to watch (stream) youtube music/videos).\
`libnotify` which provides `notify-send` needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\
[automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking manga with `Ctrl + T`.\
`waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` or `--upscale-images-always` option.\
diff --git a/TODO b/TODO
index 4f0feee..c397294 100644
--- a/TODO
+++ b/TODO
@@ -124,7 +124,6 @@ Set curl download limits everywhere (when saving to file, downloading to json, e
In the downloader if we already have the url in thumbnail/video cache, then copy it to the destination instead of redownloading it. This would also fix downloading images when viewing a manga page.
Update timestamp of messages posted with matrix (and move them to the correct place in the timeline) when we receive the message from the server. This is needed when the localtime is messed up (for example when rebooting from windows into linux).
Remove dependency on imagemagick and create a function that forks the processes and creates a thumbnail from an input filepath and output filepath. That new process can use sf::Image to load and save the images.
-Youtube now requires signing in to view age restricted content. For such videos quickmedia should fallback to invidious.
Notification race condition when fetching the first notifications page and receiving a notification immediately after the first sync? we might end up with a duplicate notification.
Submit on notifications item in matrix should jump to the message in the room.
Notifications should load their replied-to-message.
@@ -157,4 +156,11 @@ Kill async image downloader thread when the item with the image is no longer vis
Embedding elements in rich text: first byte should be an invalid utf8 character and the next 3 bytes should be an index into a vector with the element data. This forms 1 utf32 character.
Do not set fps to monitor hz if no key is pressed and cursor is not moving (for example when the computer is sleeping). (Maybe just detect if any sfml event is received), but maybe that wouldn't work with accelerometer.
Ctrl+arrow key to move to previous/next video.
-Add keybinding to view file-manager images in fullscreen to preview them. Also add keybinding to create/delete directories. \ No newline at end of file
+Add keybinding to view file-manager images in fullscreen to preview them. Also add keybinding to create/delete directories.
+Completely remove youtube-dl dependency (or at least for downloading videos/music).
+Readd mark-watched.
+Add loading of english subtitles for youtube.
+Cancel search when new search happens.
+Set mpv title to the youtube video title (that we got from GET watch?v when fetching url).
+Update item height when it switches from not being merged with previous to being merged with previous. This happens when loading previous messages in matrix and the message is the top one.
+Reload youtube video url if the video is idle for too long. The video url is only valid for a specific amount of time (the valid duration is in the json). \ No newline at end of file
diff --git a/include/AsyncImageLoader.hpp b/include/AsyncImageLoader.hpp
index 7fe7c07..689ec5f 100644
--- a/include/AsyncImageLoader.hpp
+++ b/include/AsyncImageLoader.hpp
@@ -3,12 +3,12 @@
#include "../include/Storage.hpp"
#include "../include/MessageQueue.hpp"
#include "../include/FileAnalyzer.hpp"
+#include "../include/AsyncTask.hpp"
#include <SFML/System/Vector2.hpp>
#include <SFML/Graphics/Texture.hpp>
#include <SFML/System/Clock.hpp>
#include <string>
#include <memory>
-#include <thread>
#include <unordered_map>
namespace QuickMedia {
@@ -67,8 +67,8 @@ namespace QuickMedia {
private:
bool loading_image[NUM_IMAGE_LOAD_THREADS];
// TODO: Use curl single-threaded multi-download feature instead
- std::thread download_image_thread[NUM_IMAGE_LOAD_THREADS];
- std::thread load_image_thread;
+ AsyncTask<void> download_image_thread[NUM_IMAGE_LOAD_THREADS];
+ AsyncTask<void> load_image_thread;
MessageQueue<ThumbnailLoadData> image_load_queue;
std::unordered_map<std::string, std::shared_ptr<ThumbnailData>> thumbnails;
size_t counter = 0;
diff --git a/include/MessageQueue.hpp b/include/MessageQueue.hpp
index 3f38ca2..30826f4 100644
--- a/include/MessageQueue.hpp
+++ b/include/MessageQueue.hpp
@@ -44,6 +44,7 @@ namespace QuickMedia {
void close() {
std::unique_lock<std::mutex> lock(mutex);
running = false;
+ data_queue.clear();
cv.notify_one();
}
diff --git a/include/Program.hpp b/include/Program.hpp
index 82c912a..f164180 100644
--- a/include/Program.hpp
+++ b/include/Program.hpp
@@ -41,5 +41,6 @@ int exec_program_async(const char **args, pid_t *result_process_id);
void program_clear_current_thread();
void program_kill_in_thread(const std::thread::id &thread_id);
+bool program_is_dead_in_current_thread();
#endif /* QUICKMEDIA_PROGRAM_HPP */
diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp
index 8a94d2f..cd18366 100644
--- a/include/StringUtils.hpp
+++ b/include/StringUtils.hpp
@@ -7,6 +7,7 @@ namespace QuickMedia {
// Return false to stop iterating
using StringSplitCallback = std::function<bool(const char *str, size_t size)>;
+ void string_split(const std::string &str, const std::string &delimiter, StringSplitCallback callback_func);
void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func);
// Returns the number of replaced substrings
size_t string_replace_all(std::string &str, char old_char, char new_char);
diff --git a/include/VideoPlayer.hpp b/include/VideoPlayer.hpp
index 1d15aff..341c686 100644
--- a/include/VideoPlayer.hpp
+++ b/include/VideoPlayer.hpp
@@ -40,7 +40,8 @@ namespace QuickMedia {
VideoPlayer(const VideoPlayer&) = delete;
VideoPlayer& operator=(const VideoPlayer&) = delete;
- Error load_video(const char *path, sf::WindowHandle parent_window, const std::string &plugin_name, const std::string &title);
+ // |audio_path| is only set when video and audio are separate files/urls
+ Error load_video(const char *path, const char *audio_path, sf::WindowHandle parent_window, const std::string &plugin_name, const std::string &title);
// Should be called every update frame
Error update();
@@ -55,7 +56,7 @@ namespace QuickMedia {
int exit_status;
private:
Error send_command(const char *cmd, size_t size);
- Error launch_video_process(const char *path, sf::WindowHandle parent_window, const std::string &plugin_name, const std::string &title);
+ Error launch_video_process(const char *path, const char *audio_path, sf::WindowHandle parent_window, const std::string &plugin_name, const std::string &title);
VideoPlayer::Error read_ipc_func();
private:
bool no_video;
diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp
index 6b287b6..651eabf 100644
--- a/plugins/ImageBoard.hpp
+++ b/plugins/ImageBoard.hpp
@@ -16,16 +16,15 @@ namespace QuickMedia {
class ImageBoardThreadPage : public VideoPage {
public:
- ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id) : VideoPage(program), board_id(std::move(board_id)), thread_id(std::move(thread_id)) {}
+ ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id) : VideoPage(program, ""), board_id(std::move(board_id)), thread_id(std::move(thread_id)) {}
const char* get_title() const override { return ""; }
PageTypez get_type() const override { return PageTypez::IMAGE_BOARD_THREAD; }
bool autoplay_next_item() override { return true; }
- std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override;
+ std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) override;
std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override {
return nullptr;
}
- std::string get_url() override { return video_url; }
virtual PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg);
// If |filepath| is empty then no file is uploaded
virtual PostResult post_comment(const std::string &captcha_id, const std::string &comment, const std::string &filepath = "") = 0;
@@ -33,6 +32,5 @@ namespace QuickMedia {
const std::string board_id;
const std::string thread_id;
- std::string video_url;
};
} \ No newline at end of file
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index ebaa166..568357a 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -410,16 +410,14 @@ namespace QuickMedia {
// Dummy, only play one video. TODO: Play all videos in room, as related videos?
class MatrixVideoPage : public VideoPage {
public:
- MatrixVideoPage(Program *program) : VideoPage(program) {}
+ MatrixVideoPage(Program *program) : VideoPage(program, "") {}
const char* get_title() const override { return ""; }
- std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program*, const std::string&, const std::string&) override {
+ std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program*) override {
return nullptr;
}
std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override {
return nullptr;
}
- std::string get_url() override { return url; }
- std::string url;
};
class MatrixChatPage : public Page {
diff --git a/plugins/MediaGeneric.hpp b/plugins/MediaGeneric.hpp
index c7ce00b..102e315 100644
--- a/plugins/MediaGeneric.hpp
+++ b/plugins/MediaGeneric.hpp
@@ -80,17 +80,15 @@ namespace QuickMedia {
class MediaGenericVideoPage : public VideoPage {
public:
- MediaGenericVideoPage(Program *program, MediaGenericSearchPage *search_page, const std::string &url) : VideoPage(program), search_page(search_page), url(url) {}
+ MediaGenericVideoPage(Program *program, MediaGenericSearchPage *search_page, const std::string &url) : VideoPage(program, url), search_page(search_page) {}
const char* get_title() const override { return ""; }
BodyItems get_related_media(const std::string &url, std::string &channel_url) override;
std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) override;
- std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override;
+ std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) override;
std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override {
return nullptr;
}
- std::string get_url() override { return url; }
private:
MediaGenericSearchPage *search_page;
- std::string url;
};
} \ No newline at end of file
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index 54d7d88..0c5b093 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -102,18 +102,30 @@ namespace QuickMedia {
class VideoPage : public Page {
public:
- VideoPage(Program *program) : Page(program) {}
+ VideoPage(Program *program, std::string url) : Page(program), url(std::move(url)) {}
virtual PageTypez get_type() const override { return PageTypez::VIDEO; }
virtual bool autoplay_next_item() { return false; }
virtual BodyItems get_related_media(const std::string &url, std::string &channel_url) { (void)url; (void)channel_url; return {}; }
virtual std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) { (void)program; (void)search_delay; return nullptr; }
virtual std::unique_ptr<Page> create_comments_page(Program *program) { (void)program; return nullptr; }
// Return nullptr if the service doesn't support related videos page
- virtual std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) = 0;
+ virtual std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) = 0;
// Return nullptr if the service doesn't support channels page
virtual std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) = 0;
- virtual std::string get_url() = 0;
+ void set_url(std::string new_url) { url = std::move(new_url); }
+ std::string get_url() { return url; }
+ // Falls back to |get_url| if this and |get_audio_url| returns empty strings
+ virtual std::string get_video_url(int max_height, bool &has_embedded_audio) {
+ (void)max_height;
+ has_embedded_audio = true;
+ return "";
+ }
+ // Only used if |get_video_url| sets |has_embedded_audio| to false
+ virtual std::string get_audio_url() { return ""; }
virtual std::string url_get_playable_url(const std::string &url) { return url; }
virtual bool video_should_be_skipped(const std::string &url) { (void)url; return false; }
+ virtual PluginResult load() { return PluginResult::OK; }
+ protected:
+ std::string url;
};
} \ No newline at end of file
diff --git a/plugins/Soundcloud.hpp b/plugins/Soundcloud.hpp
index 873de5a..c7883ba 100644
--- a/plugins/Soundcloud.hpp
+++ b/plugins/Soundcloud.hpp
@@ -57,15 +57,12 @@ namespace QuickMedia {
class SoundcloudAudioPage : public VideoPage {
public:
- SoundcloudAudioPage(Program *program, const std::string &url) : VideoPage(program), url(url) {}
+ SoundcloudAudioPage(Program *program, const std::string &url) : VideoPage(program, url) {}
const char* get_title() const override { return ""; }
bool autoplay_next_item() override { return true; }
- std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *, const std::string &, const std::string &) override { return nullptr; }
+ std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *) override { return nullptr; }
std::unique_ptr<Page> create_channels_page(Program *, const std::string &) override { return nullptr; }
- std::string get_url() override { return url; }
std::string url_get_playable_url(const std::string &url) override;
bool video_should_be_skipped(const std::string &url) override { return url == "track" || url.find("/stream/users/") != std::string::npos; }
- private:
- std::string url;
};
} \ No newline at end of file
diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp
index 746619c..dea6cc2 100644
--- a/plugins/Youtube.hpp
+++ b/plugins/Youtube.hpp
@@ -5,14 +5,35 @@
#include <unordered_set>
namespace QuickMedia {
- class YoutubeSearchPage : public Page {
+ struct YoutubeFormat {
+ std::string url;
+ int bitrate = 0;
+ };
+
+ struct YoutubeVideoFormat {
+ YoutubeFormat base;
+ int width = 0;
+ int height = 0;
+ int fps = 0;
+ bool has_embedded_audio = false;
+ };
+
+ struct YoutubeAudioFormat {
+ YoutubeFormat base;
+ };
+
+ bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id);
+
+ class YoutubeSearchPage : public LazyFetchPage {
public:
- YoutubeSearchPage(Program *program) : Page(program) {}
+ YoutubeSearchPage(Program *program) : LazyFetchPage(program) {}
const char* get_title() const override { return "Search"; }
bool search_is_filter() override { return false; }
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ bool lazy_fetch_is_loader() override { return true; }
private:
PluginResult search_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items);
private:
@@ -110,18 +131,24 @@ namespace QuickMedia {
class YoutubeVideoPage : public VideoPage {
public:
- YoutubeVideoPage(Program *program, const std::string &url) : VideoPage(program), url(url) {}
+ YoutubeVideoPage(Program *program, std::string url) : VideoPage(program, std::move(url)) {}
const char* get_title() const override { return ""; }
BodyItems get_related_media(const std::string &url, std::string &channel_url) override;
std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) override;
std::unique_ptr<Page> create_comments_page(Program *program) override;
- std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override;
+ std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) override;
std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) override;
- std::string get_url() override { return url; }
+ std::string get_video_url(int max_height, bool &has_embedded_audio) override;
+ std::string get_audio_url() override;
+ PluginResult load() override;
+ private:
+ void parse_format(const Json::Value &format_json, bool is_adaptive);
+ void parse_formats(const Json::Value &streaming_data_json);
private:
std::string xsrf_token;
std::string comments_continuation_token;
- std::string url;
+ std::vector<YoutubeVideoFormat> video_formats;
+ std::vector<YoutubeAudioFormat> audio_formats;
std::string playback_url;
std::string watchtime_url;
diff --git a/plugins/youtube/Signature.hpp b/plugins/youtube/Signature.hpp
new file mode 100644
index 0000000..7456615
--- /dev/null
+++ b/plugins/youtube/Signature.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "../../include/AsyncTask.hpp"
+#include <string>
+#include <vector>
+#include <map>
+#include <mutex>
+#include <time.h>
+
+namespace QuickMedia {
+ // Thread safe
+ class YoutubeSignatureDecryptor {
+ public:
+ static YoutubeSignatureDecryptor& get_instance();
+ bool decrypt(const std::string &s, const std::string &sp, std::string &sig_key, std::string &sig_value);
+ private:
+ YoutubeSignatureDecryptor();
+ ~YoutubeSignatureDecryptor();
+ YoutubeSignatureDecryptor(const YoutubeSignatureDecryptor&) = delete;
+ YoutubeSignatureDecryptor& operator=(const YoutubeSignatureDecryptor&) = delete;
+
+ struct DecryptFuncCall {
+ std::string func_name;
+ long arg;
+ };
+
+ enum class DecryptFunction {
+ REVERSE,
+ SPLICE,
+ SWAP
+ };
+
+ bool js_code_to_operations(const std::string &function_body_str, const std::string &var_body_str, std::vector<DecryptFuncCall> &new_func_calls, std::map<std::string, DecryptFunction> &new_func_decls);
+ int update_decrypt_function();
+ private:
+ AsyncTask<void> poll_task;
+ std::mutex update_signature_mutex;
+ std::string decryption_function;
+ time_t decrypt_function_last_updated = 0;
+ bool running = false;
+ bool up_to_date = false;
+
+ std::vector<DecryptFuncCall> func_calls;
+ std::map<std::string, DecryptFunction> func_decls;
+ };
+} \ No newline at end of file
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index 1a1e120..9e8d090 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -87,7 +87,7 @@ namespace QuickMedia {
loading_image[i] = false;
}
- load_image_thread = std::thread([this]() mutable {
+ load_image_thread = AsyncTask<void>([this]() mutable {
std::optional<ThumbnailLoadData> thumbnail_load_data_opt;
while(true) {
thumbnail_load_data_opt = image_load_queue.pop_wait();
@@ -126,17 +126,6 @@ namespace QuickMedia {
AsyncImageLoader::~AsyncImageLoader() {
image_load_queue.close();
- if(load_image_thread.joinable()) {
- program_kill_in_thread(load_image_thread.get_id());
- load_image_thread.join();
- }
-
- for(size_t i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) {
- if(download_image_thread[i].joinable()) {
- program_kill_in_thread(download_image_thread[i].get_id());
- download_image_thread[i].join();
- }
- }
}
void AsyncImageLoader::load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, std::shared_ptr<ThumbnailData> thumbnail_data) {
@@ -178,11 +167,9 @@ namespace QuickMedia {
loading_image[free_index] = true;
thumbnail_data->loading_state = LoadingState::LOADING;
- if(download_image_thread[free_index].joinable())
- download_image_thread[free_index].join();
// TODO: Keep the thread running and use conditional variable instead to sleep until a new image should be loaded. Same in ImageViewer.
- download_image_thread[free_index] = std::thread([this, free_index, thumbnail_path, url, resize_target_size, thumbnail_data]() mutable {
+ download_image_thread[free_index] = AsyncTask<void>([this, free_index, thumbnail_path, url, resize_target_size, thumbnail_data]() mutable {
thumbnail_data->image = std::make_unique<sf::Image>();
Path thumbnail_path_resized = thumbnail_path;
diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp
index 774fba9..0977b78 100644
--- a/src/DownloadUtils.cpp
+++ b/src/DownloadUtils.cpp
@@ -24,6 +24,7 @@ static const char *useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64)
namespace QuickMedia {
DownloadResult download_head_to_string(const std::string &url, std::string &result, bool use_browser_useragent, bool fail_on_error) {
+ result.clear();
sf::Clock timer;
std::vector<const char*> args;
args.insert(args.end(), { "curl", "-I", "-g", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-s" });
@@ -50,6 +51,7 @@ namespace QuickMedia {
}
DownloadResult url_get_remote_name(const std::string &url, std::string &result, bool use_browser_useragent) {
+ result.clear();
sf::Clock timer;
std::vector<const char*> args;
args.insert(args.end(), { "curl", "-I", "-g", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-s" });
@@ -131,6 +133,7 @@ namespace QuickMedia {
// TODO: Add timeout
DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector<CommandArg> &additional_args, bool use_browser_useragent, bool fail_on_error) {
+ result.clear();
sf::Clock timer;
std::vector<const char*> args;
args.insert(args.end(), { "curl", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-g", "-s", "-L" });
@@ -162,6 +165,7 @@ namespace QuickMedia {
}
DownloadResult download_to_string_cache(const std::string &url, std::string &result, const std::vector<CommandArg> &additional_args, bool use_browser_useragent, DownloadErrorHandler error_handler, Path cache_path) {
+ result.clear();
Path media_file_path;
if(cache_path.data.empty()) {
SHA256 sha256;
diff --git a/src/Program.cpp b/src/Program.cpp
index a8189ae..5220a4c 100644
--- a/src/Program.cpp
+++ b/src/Program.cpp
@@ -280,3 +280,7 @@ void program_clear_current_thread() {
void program_kill_in_thread(const std::thread::id &thread_id) {
current_thread_program.kill_in_thread(thread_id);
}
+
+bool program_is_dead_in_current_thread() {
+ return current_thread_program.is_killed();
+}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index fa0f0d8..7479299 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -2145,30 +2145,6 @@ namespace QuickMedia {
return false;
}
- static bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) {
- size_t index = youtube_url.find("youtube.com/watch?v=");
- if(index != std::string::npos) {
- index += 20;
- size_t end_index = youtube_url.find("&", index);
- if(end_index == std::string::npos)
- end_index = youtube_url.size();
- youtube_video_id = youtube_url.substr(index, end_index - index);
- return true;
- }
-
- index = youtube_url.find("youtu.be/");
- if(index != std::string::npos) {
- index += 9;
- size_t end_index = youtube_url.find("?", index);
- if(end_index == std::string::npos)
- end_index = youtube_url.size();
- youtube_video_id = youtube_url.substr(index, end_index - index);
- return true;
- }
-
- return false;
- }
-
static int watch_history_get_item_by_id(const Json::Value &video_history_json, const char *id) {
assert(video_history_json.isArray());
@@ -2372,68 +2348,14 @@ 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) {
PageType previous_page = pop_page_stack();
- std::string video_url = video_page->get_url();
- std::string original_video_url = video_url;
sf::Clock time_watched_timer;
bool video_loaded = false;
- std::string youtube_video_id;
- const bool is_youtube = youtube_url_extract_id(video_url, youtube_video_id);
+ std::string youtube_video_id_dummy;
+ const bool is_youtube = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy);
const bool is_matrix = strcmp(plugin_name, "matrix") == 0;
- bool video_url_is_local = false;
- if(!is_youtube && download_if_streaming_fails) {
- Path video_cache_dir = get_cache_dir().join("media");
- Path video_path = video_cache_dir;
- SHA256 sha256;
- sha256.add(video_url.data(), video_url.size());
- video_path.join(sha256.getHash());
- if(get_file_type(video_path) == FileType::REGULAR) {
- fprintf(stderr, "%s is found in cache. Playing from cache...\n", video_url.c_str());
- video_url = std::move(video_path.data);
- video_url_is_local = true;
- } else {
- TaskResult video_is_not_streamble_result = run_task_with_loading_screen([video_url]() {
- return video_url_is_non_streamable_mp4(video_url.c_str());
- });
- if(video_is_not_streamble_result == TaskResult::TRUE) {
- fprintf(stderr, "%s is detected to be a non-streamable mp4 file, downloading it before playing it...\n", video_url.c_str());
- if(create_directory_recursive(video_cache_dir) != 0) {
- show_notification("QuickMedia", "Failed to create video cache directory", Urgency::CRITICAL);
- current_page = previous_page;
- return;
- }
-
- TaskResult download_file_result = run_task_with_loading_screen([&video_path, video_url]() {
- return download_to_file(video_url, video_path.data, {}, true) == DownloadResult::OK;
- });
- switch(download_file_result) {
- case TaskResult::TRUE: {
- video_url = std::move(video_path.data);
- video_url_is_local = true;
- break;
- }
- case TaskResult::FALSE: {
- show_notification("QuickMedia", "Failed to download " + video_url, Urgency::CRITICAL);
- current_page = previous_page;
- return;
- }
- case TaskResult::CANCEL: {
- current_page = previous_page;
- return;
- }
- }
- } else if(video_is_not_streamble_result == TaskResult::CANCEL) {
- current_page = previous_page;
- return;
- }
- }
- }
-
- window.setFramerateLimit(FPS_IDLE);
- idle = true;
-
- time_watched_timer.restart();
+ idle_active_handler();
std::unique_ptr<VideoPlayer> video_player;
BodyItems related_videos;
@@ -2451,14 +2373,110 @@ 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, video_page, &video_event_callback, &on_window_create, &video_player_window, is_youtube, is_matrix](bool resume_video) mutable {
+ auto load_video_error_check = [this, &related_videos, &channel_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_youtube, is_matrix, download_if_streaming_fails](bool resume_video) mutable {
+ TaskResult load_result = run_task_with_loading_screen([video_page]() {
+ return video_page->load() == PluginResult::OK;
+ });
+
+ if(load_result == TaskResult::CANCEL) {
+ current_page = previous_page;
+ return;
+ } else if(load_result == TaskResult::FALSE) {
+ show_notification("QuickMedia", "Failed to load media", Urgency::CRITICAL);
+ current_page = previous_page;
+ return;
+ }
+
+ bool is_audio_only = no_video;
+ bool has_embedded_audio = true;
+ const int largest_monitor_height = get_largest_monitor_height(disp);
+ std::string video_url = video_page->get_video_url(largest_monitor_height, has_embedded_audio);
+ std::string audio_url;
+ if(video_url.empty() || no_video) {
+ video_url = video_page->get_audio_url();
+ if(video_url.empty()) {
+ video_url = video_page->get_url();
+ has_embedded_audio = true;
+ } else {
+ is_audio_only = true;
+ has_embedded_audio = false;
+ }
+ } else if(!has_embedded_audio) {
+ audio_url = video_page->get_audio_url();
+ }
+
+ bool video_url_is_local = false;
+ if(!is_youtube && download_if_streaming_fails) {
+ Path video_cache_dir = get_cache_dir().join("media");
+ Path video_path = video_cache_dir;
+ SHA256 sha256;
+ sha256.add(video_url.data(), video_url.size());
+ video_path.join(sha256.getHash());
+ if(get_file_type(video_path) == FileType::REGULAR) {
+ fprintf(stderr, "%s is found in cache. Playing from cache...\n", video_url.c_str());
+ video_url = std::move(video_path.data);
+ video_url_is_local = true;
+ audio_url.clear();
+ if(no_video) {
+ is_audio_only = true;
+ has_embedded_audio = false;
+ } else {
+ is_audio_only = false;
+ has_embedded_audio = true;
+ }
+ } else {
+ TaskResult video_is_not_streamble_result = run_task_with_loading_screen([video_url]() {
+ return video_url_is_non_streamable_mp4(video_url.c_str());
+ });
+ if(video_is_not_streamble_result == TaskResult::TRUE) {
+ fprintf(stderr, "%s is detected to be a non-streamable mp4 file, downloading it before playing it...\n", video_url.c_str());
+ if(create_directory_recursive(video_cache_dir) != 0) {
+ show_notification("QuickMedia", "Failed to create video cache directory", Urgency::CRITICAL);
+ current_page = previous_page;
+ return;
+ }
+
+ TaskResult download_file_result = run_task_with_loading_screen([&video_path, video_url]() {
+ return download_to_file(video_url, video_path.data, {}, true) == DownloadResult::OK;
+ });
+ switch(download_file_result) {
+ case TaskResult::TRUE: {
+ video_url = std::move(video_path.data);
+ video_url_is_local = true;
+ audio_url.clear();
+ if(no_video) {
+ is_audio_only = true;
+ has_embedded_audio = false;
+ } else {
+ is_audio_only = false;
+ has_embedded_audio = true;
+ }
+ break;
+ }
+ case TaskResult::FALSE: {
+ show_notification("QuickMedia", "Failed to download " + video_url, Urgency::CRITICAL);
+ current_page = previous_page;
+ return;
+ }
+ case TaskResult::CANCEL: {
+ current_page = previous_page;
+ return;
+ }
+ }
+ } else if(video_is_not_streamble_result == TaskResult::CANCEL) {
+ current_page = previous_page;
+ return;
+ }
+ }
+ }
+
time_watched_timer.restart();
video_loaded = false;
video_player_window = None;
watched_videos.insert(video_url);
- video_player = std::make_unique<VideoPlayer>(no_video, use_system_mpv_config, resume_video, is_matrix && !is_youtube, video_event_callback, on_window_create, resources_root, get_largest_monitor_height(disp));
- VideoPlayer::Error err = video_player->load_video(video_url.c_str(), window.getSystemHandle(), plugin_name, video_title);
+ video_player = std::make_unique<VideoPlayer>(is_audio_only, use_system_mpv_config, resume_video, 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(), plugin_name, video_title);
if(err != VideoPlayer::Error::OK) {
std::string err_msg = "Failed to play url: ";
err_msg += video_url;
@@ -2469,8 +2487,9 @@ namespace QuickMedia {
return;
channel_url.clear();
- TaskResult load_related_media_result = run_task_with_loading_screen([video_page, &related_videos, &video_url, &channel_url]{
- related_videos = video_page->get_related_media(video_url, channel_url);
+ TaskResult load_related_media_result = run_task_with_loading_screen([video_page, &related_videos, &channel_url]{
+ // TODO: Do async
+ related_videos = video_page->get_related_media(video_page->get_url(), channel_url);
return true;
});
@@ -2484,9 +2503,9 @@ namespace QuickMedia {
return;
std::string video_id;
- if(!youtube_url_extract_id(video_url, video_id)) {
+ if(!youtube_url_extract_id(video_page->get_url(), video_id)) {
std::string err_msg = "Failed to extract id of youtube url ";
- err_msg += video_url;
+ err_msg += video_page->get_url();
err_msg + ", video wont be saved in history";
show_notification("QuickMedia", err_msg.c_str(), Urgency::LOW);
return;
@@ -2545,20 +2564,21 @@ namespace QuickMedia {
bool cursor_visible = true;
sf::Clock cursor_hide_timer;
- auto save_video_url_to_clipboard = [&original_video_url, &video_player]() {
- if(video_url_supports_timestamp(original_video_url)) {
+ auto save_video_url_to_clipboard = [video_page, &video_player]() {
+ std::string url = video_page->get_url();
+ if(video_url_supports_timestamp(url)) {
// TODO: Remove timestamp (&t= or ?t=) from video_url
double time_in_file = 0.0;
if(video_player && (video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK))
time_in_file = 0.0;
- std::string clipboard = original_video_url;
+ std::string clipboard = std::move(url);
if((int)time_in_file > 0)
clipboard += "&t=" + std::to_string((int)time_in_file);
sf::Clipboard::setString(sf::String::fromUtf8(clipboard.begin(), clipboard.end()));
} else {
- sf::Clipboard::setString(sf::String::fromUtf8(original_video_url.begin(), original_video_url.end()));
+ sf::Clipboard::setString(sf::String::fromUtf8(url.begin(), url.end()));
}
};
@@ -2600,7 +2620,7 @@ namespace QuickMedia {
} else if(pressed_keysym == XK_f && pressing_ctrl) {
window_set_fullscreen(disp, window.getSystemHandle(), WindowFullscreenState::TOGGLE);
} else if(pressed_keysym == XK_s && pressing_ctrl) {
- video_page_download_video(original_video_url, !is_matrix || is_youtube, video_player_window);
+ video_page_download_video(video_page->get_url(), !is_matrix || is_youtube, video_player_window);
} else if(pressed_keysym == XK_r && pressing_ctrl) {
if(!cursor_visible)
window.setMouseCursorVisible(true);
@@ -2609,7 +2629,7 @@ namespace QuickMedia {
int search_delay = 0;
auto search_page = video_page->create_search_page(this, search_delay);
auto comments_page = video_page->create_comments_page(this);
- auto related_videos_page = video_page->create_related_videos_page(this, video_url, video_title);
+ auto related_videos_page = video_page->create_related_videos_page(this);
auto channels_page = video_page->create_channels_page(this, channel_url);
if(search_page || related_videos_page || channels_page) {
XUnmapWindow(disp, video_player_window);
@@ -2654,7 +2674,6 @@ namespace QuickMedia {
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);
load_video_error_check(true);
} else {
XMapWindow(disp, video_player_window);
@@ -2729,8 +2748,20 @@ namespace QuickMedia {
break;
}
- video_url = video_page->url_get_playable_url(new_video_url);
- original_video_url = video_url;
+ TaskResult get_playable_url_result = run_task_with_loading_screen([video_page, &new_video_url]() {
+ video_page->set_url(video_page->url_get_playable_url(new_video_url));
+ return true;
+ });
+
+ if(get_playable_url_result == TaskResult::CANCEL) {
+ current_page = previous_page;
+ return;
+ } else if(get_playable_url_result == TaskResult::FALSE) {
+ show_notification("QuickMedia", "Failed to get playable url", Urgency::CRITICAL);
+ current_page = previous_page;
+ return;
+ }
+
video_title = std::move(new_video_title);
load_video_error_check(false);
} else if(update_err != VideoPlayer::Error::OK) {
@@ -3516,7 +3547,7 @@ namespace QuickMedia {
page_stack.push(PageType::IMAGE_BOARD_THREAD);
current_page = PageType::VIDEO_CONTENT;
watched_videos.clear();
- thread_page->video_url = selected_item->url;
+ thread_page->set_url(selected_item->url);
BodyItems next_items;
// TODO: Use real title
video_content_page(thread_page, thread_page, selected_item->get_title(), true, thread_body->items, thread_body->get_selected_item());
@@ -5054,7 +5085,7 @@ namespace QuickMedia {
bool is_audio = (message_type == MessageType::AUDIO);
bool prev_no_video = no_video;
no_video = is_audio;
- video_page->url = selected->url;
+ video_page->set_url(selected->url);
BodyItems next_items;
// TODO: Add title
video_content_page(matrix_chat_page, video_page.get(), "", message_type == MessageType::VIDEO || message_type == MessageType::AUDIO, next_items, 0);
diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp
index 0345558..8ed142f 100644
--- a/src/StringUtils.cpp
+++ b/src/StringUtils.cpp
@@ -2,7 +2,8 @@
#include <string.h>
namespace QuickMedia {
- void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func) {
+ template <typename T>
+ static void string_split_t(const std::string &str, const T &delimiter, StringSplitCallback callback_func) {
size_t index = 0;
while(index < str.size()) {
size_t new_index = str.find(delimiter, index);
@@ -12,10 +13,21 @@ namespace QuickMedia {
if(!callback_func(str.data() + index, new_index - index))
break;
- index = new_index + 1;
+ if constexpr(std::is_same<char, T>::value)
+ index = new_index + 1;
+ else
+ index = new_index + delimiter.size();
}
}
+ void string_split(const std::string &str, const std::string &delimiter, StringSplitCallback callback_func) {
+ string_split_t(str, delimiter, callback_func);
+ }
+
+ void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func) {
+ string_split_t(str, delimiter, callback_func);
+ }
+
size_t string_replace_all(std::string &str, char old_char, char new_char) {
size_t num_replaced_substrings = 0;
for(char &c : str) {
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
index adebddc..2b0653c 100644
--- a/src/VideoPlayer.cpp
+++ b/src/VideoPlayer.cpp
@@ -80,7 +80,7 @@ namespace QuickMedia {
return result;
}
- VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &) {
+ VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, const char *audio_path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &) {
parent_window = _parent_window;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == -1) {
@@ -172,6 +172,12 @@ namespace QuickMedia {
if(no_video)
args.push_back("--no-video");
+ std::string audio_file_arg = "--audio-file=";
+ if(audio_path && audio_path[0] != '\0') {
+ audio_file_arg += audio_path;
+ args.push_back(audio_file_arg.c_str());
+ }
+
args.insert(args.end(), { "--", path, nullptr });
if(exec_program_async(args.data(), &video_process_id) != 0) {
@@ -190,12 +196,14 @@ namespace QuickMedia {
return Error::OK;
}
- VideoPlayer::Error VideoPlayer::load_video(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &title) {
+ VideoPlayer::Error VideoPlayer::load_video(const char *path, const char *audio_path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &title) {
// 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);
+ fprintf(stderr, "Playing video: %s, audio: %s\n", path ? path : "", audio_path ? audio_path : "");
if(video_process_id == -1)
- return launch_video_process(path, _parent_window, plugin_name, title);
+ return launch_video_process(path, audio_path, _parent_window, plugin_name, title);
Json::Value command_data(Json::arrayValue);
command_data.append("loadfile");
diff --git a/src/plugins/ImageBoard.cpp b/src/plugins/ImageBoard.cpp
index 881d8ff..a2ffca7 100644
--- a/src/plugins/ImageBoard.cpp
+++ b/src/plugins/ImageBoard.cpp
@@ -1,7 +1,7 @@
#include "../../plugins/ImageBoard.hpp"
namespace QuickMedia {
- std::unique_ptr<RelatedVideosPage> ImageBoardThreadPage::create_related_videos_page(Program*, const std::string&, const std::string&) {
+ std::unique_ptr<RelatedVideosPage> ImageBoardThreadPage::create_related_videos_page(Program*) {
return nullptr;
}
diff --git a/src/plugins/MediaGeneric.cpp b/src/plugins/MediaGeneric.cpp
index 19ad87c..c3a8d8e 100644
--- a/src/plugins/MediaGeneric.cpp
+++ b/src/plugins/MediaGeneric.cpp
@@ -191,7 +191,7 @@ namespace QuickMedia {
return std::make_unique<MediaGenericSearchPage>(*search_page);
}
- std::unique_ptr<RelatedVideosPage> MediaGenericVideoPage::create_related_videos_page(Program *program, const std::string&, const std::string&) {
+ std::unique_ptr<RelatedVideosPage> MediaGenericVideoPage::create_related_videos_page(Program *program) {
return std::make_unique<MediaGenericRelatedPage>(program, search_page);
}
} \ No newline at end of file
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index d9df409..9cec69c 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -1,10 +1,12 @@
#include "../../plugins/Youtube.hpp"
+#include "../../plugins/youtube/Signature.hpp"
#include "../../include/Storage.hpp"
#include "../../include/NetUtils.hpp"
#include "../../include/StringUtils.hpp"
#include "../../include/Scale.hpp"
#include "../../include/Notification.hpp"
#include "../../include/Utils.hpp"
+#include <json/reader.h>
extern "C" {
#include <HtmlParser.h>
}
@@ -13,6 +15,30 @@ extern "C" {
#include <unistd.h>
namespace QuickMedia {
+ bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) {
+ size_t index = youtube_url.find("youtube.com/watch?v=");
+ if(index != std::string::npos) {
+ index += 20;
+ size_t end_index = youtube_url.find("&", index);
+ if(end_index == std::string::npos)
+ end_index = youtube_url.size();
+ youtube_video_id = youtube_url.substr(index, end_index - index);
+ return true;
+ }
+
+ index = youtube_url.find("youtu.be/");
+ if(index != std::string::npos) {
+ index += 9;
+ size_t end_index = youtube_url.find("?", index);
+ if(end_index == std::string::npos)
+ end_index = youtube_url.size();
+ youtube_video_id = youtube_url.substr(index, end_index - index);
+ return true;
+ }
+
+ return false;
+ }
+
// This is a common setup of text in the youtube json
static std::optional<std::string> yt_json_get_text(const Json::Value &json, const char *root_name) {
if(!json.isObject())
@@ -257,7 +283,10 @@ namespace QuickMedia {
body_item->set_description_color(sf::Color(179, 179, 179));
body_item->url = "https://www.youtube.com/channel/" + channel_id_json.asString();
if(thumbnail) {
- body_item->thumbnail_url = std::string("https:") + thumbnail->url;
+ if(string_starts_with(thumbnail->url, "https:"))
+ body_item->thumbnail_url = thumbnail->url;
+ else
+ body_item->thumbnail_url = std::string("https:") + thumbnail->url;
body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
body_item->thumbnail_size.x = thumbnail->width;
body_item->thumbnail_size.y = thumbnail->height;
@@ -386,13 +415,16 @@ namespace QuickMedia {
static std::vector<CommandArg> get_cookies() {
std::lock_guard<std::mutex> lock(cookies_mutex);
if(cookies_filepath.empty()) {
+ YoutubeSignatureDecryptor::get_instance();
+
Path cookies_filepath_p;
if(get_cookies_filepath(cookies_filepath_p, "youtube") != 0) {
show_notification("QuickMedia", "Failed to create youtube cookies file", Urgency::CRITICAL);
return {};
}
- // TODO: Re-enable this if the api key ever changes in the future
+ // TODO: Re-enable this if the api key ever changes in the future.
+ // Maybe also put signature decryption in the same request? since it requests the same page.
#if 0
//api_key = youtube_page_find_api_key();
#else
@@ -655,6 +687,11 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ PluginResult YoutubeSearchPage::lazy_fetch(BodyItems&) {
+ get_cookies();
+ return PluginResult::OK;
+ }
+
PluginResult YoutubeSearchPage::search_get_continuation(const std::string &url, const std::string &current_continuation_token, BodyItems &result_items) {
std::string next_url = url + "&pbj=1&ctoken=" + current_continuation_token;
@@ -1800,11 +1837,237 @@ namespace QuickMedia {
return std::make_unique<YoutubeCommentsPage>(program, xsrf_token, comments_continuation_token);
}
- std::unique_ptr<RelatedVideosPage> YoutubeVideoPage::create_related_videos_page(Program *program, const std::string&, const std::string&) {
+ std::unique_ptr<RelatedVideosPage> YoutubeVideoPage::create_related_videos_page(Program *program) {
return std::make_unique<YoutubeRelatedVideosPage>(program);
}
std::unique_ptr<Page> YoutubeVideoPage::create_channels_page(Program *program, const std::string &channel_url) {
return std::make_unique<YoutubeChannelPage>(program, channel_url, "", "Channel videos");
}
+
+ static std::map<std::string, std::string> http_params_parse(const std::string &http_params) {
+ std::map<std::string, std::string> result;
+ string_split(http_params, '&', [&result](const char *str, size_t size) {
+ const void *split_p = memchr(str, '=', size);
+ if(split_p == nullptr)
+ return true;
+
+ std::string key(str, (const char*)split_p - str);
+ std::string value((const char*)split_p + 1, (str + size) - ((const char*)split_p + 1));
+ key = url_param_decode(key);
+ value = url_param_decode(value);
+ result[std::move(key)] = std::move(value);
+ return true;
+ });
+ return result;
+ }
+
+ static std::string url_extract_param(const std::string &url, const std::string &param) {
+ std::string param_s = param + "=";
+ size_t index = url.find(param_s);
+ if(index == std::string::npos)
+ return "";
+
+ index += param_s.size();
+ size_t end = url.find('&', index);
+ if(end == std::string::npos)
+ end = url.size();
+
+ return url.substr(index, end - index);
+ }
+
+ std::string YoutubeVideoPage::get_video_url(int max_height, bool &has_embedded_audio) {
+ if(video_formats.empty()) {
+ has_embedded_audio = true;
+ return "";
+ }
+
+ for(const auto &video_format : video_formats) {
+ if(video_format.height <= max_height) {
+ has_embedded_audio = video_format.has_embedded_audio;
+ return video_format.base.url;
+ }
+ }
+
+ has_embedded_audio = video_formats.back().has_embedded_audio;
+ return video_formats.back().base.url;
+ }
+
+ std::string YoutubeVideoPage::get_audio_url() {
+ if(audio_formats.empty())
+ return "";
+
+ return audio_formats.front().base.url;
+ }
+
+ PluginResult YoutubeVideoPage::load() {
+ video_formats.clear();
+ audio_formats.clear();
+
+ 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 PluginResult::ERR;
+ }
+
+ 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());
+
+ std::string response;
+ DownloadResult download_result = download_to_string("https://www.youtube.com/get_video_info?html5=1&video_id=" + video_id + "&eurl=https://www.youtube.googleapis.com/v/" + video_id, response, std::move(additional_args), true); // TODO: true?
+ if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result);
+
+ std::string player_response_param = url_extract_param(response, "player_response");
+ player_response_param = url_param_decode(player_response_param);
+
+ Json::Value json_root;
+ Json::CharReaderBuilder json_builder;
+ std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
+ std::string json_errors;
+ if(!json_reader->parse(player_response_param.data(), player_response_param.data() + player_response_param.size(), &json_root, &json_errors)) {
+ fprintf(stderr, "Failed to read param as json, error: %s\n", json_errors.c_str());
+ return PluginResult::ERR;
+ }
+
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &streaming_data_json = json_root["streamingData"];
+ if(!streaming_data_json.isObject())
+ return PluginResult::ERR;
+
+ parse_formats(streaming_data_json);
+
+ if(video_formats.empty() && audio_formats.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;
+ });
+
+ std::sort(audio_formats.begin(), audio_formats.end(), [](const YoutubeAudioFormat &format1, const YoutubeAudioFormat &format2) {
+ return format1.base.bitrate > format2.base.bitrate;
+ });
+
+ return PluginResult::OK;
+ }
+
+ static bool parse_cipher_format(const Json::Value &format, YoutubeFormat &youtube_format) {
+ std::map<std::string, std::string> cipher_params;
+ const Json::Value &cipher_json = format["cipher"];
+ if(cipher_json.isString()) {
+ cipher_params = http_params_parse(cipher_json.asString());
+ } else {
+ const Json::Value &signature_cipher_json = format["signatureCipher"];
+ if(signature_cipher_json.isString())
+ cipher_params = http_params_parse(signature_cipher_json.asString());
+ }
+
+ const std::string &url = cipher_params["url"];
+ if(cipher_params.empty() || url.empty())
+ return false;
+
+ std::string url_decoded = url_param_decode(url);
+
+ const std::string &s = cipher_params["s"];
+ const std::string &sp = cipher_params["sp"];
+ std::string sig_key;
+ std::string sig_value;
+ if(YoutubeSignatureDecryptor::get_instance().decrypt(s, sp, sig_key, sig_value))
+ url_decoded += "&" + std::move(sig_key) + "=" + std::move(sig_value);
+
+ youtube_format.url = std::move(url_decoded);
+ return true;
+ }
+
+ void YoutubeVideoPage::parse_format(const Json::Value &format_json, bool is_adaptive) {
+ if(!format_json.isArray())
+ return;
+
+ for(const Json::Value &format : format_json) {
+ if(!format.isObject())
+ continue;
+
+ if(is_adaptive) {
+ // TODO: Fix. Some streams use sq/ instead of index
+ const Json::Value &index_range_json = format["indexRange"];
+ if(index_range_json.isNull()) {
+ fprintf(stderr, "Ignoring adaptive stream without indexRange\n");
+ continue;
+ }
+ }
+
+ YoutubeFormat youtube_format_base;
+
+ const Json::Value &mime_type_json = format["mimeType"];
+ if(!mime_type_json.isString()) continue;
+
+ const Json::Value &bitrate_json = format["bitrate"];
+ if(!bitrate_json.isInt()) continue;
+ youtube_format_base.bitrate = bitrate_json.asInt();
+
+ if(strncmp(mime_type_json.asCString(), "video/", 6) == 0) {
+ bool has_embedded_audio = false;
+ const char *codecs_p = strstr(mime_type_json.asCString(), "codecs=\"");
+ if(codecs_p) {
+ codecs_p += 8;
+ const char *codecs_sep_p = strchr(codecs_p, ',');
+ const char *codecs_end_p = strchr(codecs_p, '"');
+ has_embedded_audio = (codecs_sep_p != nullptr && (!codecs_end_p || codecs_sep_p < codecs_end_p));
+ }
+
+ YoutubeVideoFormat video_format;
+ video_format.base = std::move(youtube_format_base);
+ video_format.has_embedded_audio = has_embedded_audio;
+
+ const Json::Value &width_json = format["width"];
+ if(!width_json.isInt()) continue;
+ video_format.width = width_json.asInt();
+
+ const Json::Value &height_json = format["height"];
+ if(!height_json.isInt()) continue;
+ video_format.height = height_json.asInt();
+
+ const Json::Value &fps_json = format["fps"];
+ if(!fps_json.isInt()) continue;
+ video_format.fps = fps_json.asInt();
+
+ const Json::Value &url_json = format["url"];
+ if(url_json.isString()) {
+ video_format.base.url = url_json.asString();
+ } else {
+ if(!parse_cipher_format(format, video_format.base))
+ continue;
+ }
+
+ video_formats.push_back(std::move(video_format));
+ } else if(strncmp(mime_type_json.asCString(), "audio/", 6) == 0) {
+ YoutubeAudioFormat audio_format;
+ audio_format.base = std::move(youtube_format_base);
+
+ const Json::Value &url_json = format["url"];
+ if(url_json.isString()) {
+ audio_format.base.url = url_json.asString();
+ } else {
+ if(!parse_cipher_format(format, audio_format.base))
+ continue;
+ }
+
+ audio_formats.push_back(std::move(audio_format));
+ }
+ }
+ }
+
+ void YoutubeVideoPage::parse_formats(const Json::Value &streaming_data_json) {
+ const Json::Value &formats_json = streaming_data_json["formats"];
+ parse_format(formats_json, false);
+
+ const Json::Value &adaptive_formats_json = streaming_data_json["adaptiveFormats"];
+ parse_format(adaptive_formats_json, false);
+ }
} \ No newline at end of file
diff --git a/src/plugins/youtube/Signature.cpp b/src/plugins/youtube/Signature.cpp
new file mode 100644
index 0000000..30093d5
--- /dev/null
+++ b/src/plugins/youtube/Signature.cpp
@@ -0,0 +1,307 @@
+#include "../../../plugins/youtube/Signature.hpp"
+#include "../../../include/Storage.hpp"
+#include "../../../include/Notification.hpp"
+#include "../../../include/DownloadUtils.hpp"
+#include "../../../include/StringUtils.hpp"
+#include "../../../include/Program.hpp"
+#include <regex>
+#include <unistd.h>
+
+namespace QuickMedia {
+ enum UpdateDecryptFunctionError {
+ U_DEC_FUN_NET_ERR = 1,
+ U_DEC_FUN_FAILED_MATCH_ERR = 2
+ };
+
+ static YoutubeSignatureDecryptor *instance = nullptr;
+ static const int timeout_default_sec = 60;
+
+ bool YoutubeSignatureDecryptor::js_code_to_operations(const std::string &function_body_str, const std::string &var_body_str, std::vector<DecryptFuncCall> &new_func_calls, std::map<std::string, DecryptFunction> &new_func_decls) {
+ std::vector<std::string> function_body;
+ string_split(function_body_str, ';', [&function_body](const char *str, size_t size) {
+ function_body.push_back(std::string(str, size));
+ return true;
+ });
+
+ if(function_body.empty())
+ return false;
+
+ std::vector<std::string> var_body;
+ string_split(var_body_str, "},", [&var_body](const char *str, size_t size) {
+ var_body.push_back(std::string(str, size));
+ return true;
+ });
+
+ if(var_body.empty())
+ return false;
+
+ //fprintf(stderr, "function body: %s\n", function_body_str.c_str());
+ for(const std::string &func_call_str : function_body) {
+ const size_t var_name_split_index = func_call_str.find('.');
+ if(var_name_split_index == std::string::npos) return false;
+
+ const size_t func_name_end_index = func_call_str.find('(', var_name_split_index + 1);
+ if(func_name_end_index == std::string::npos) return false;
+
+ const size_t values_index = func_call_str.find(',', func_name_end_index + 1);
+ if(values_index == std::string::npos) return false;
+
+ const size_t values_end_index = func_call_str.find(')', values_index + 1);
+ if(values_end_index == std::string::npos) return false;
+
+ std::string func_name = func_call_str.substr(var_name_split_index + 1, func_name_end_index - (var_name_split_index + 1));
+ func_name = strip(func_name);
+ std::string value_args = func_call_str.substr(values_index + 1, values_end_index - (values_index + 1));
+
+ errno = 0;
+ char *endptr;
+ const long value_int = strtol(value_args.c_str(), &endptr, 10);
+ if(endptr != value_args.c_str() && errno == 0)
+ new_func_calls.push_back({ std::move(func_name), value_int });
+ else
+ return false;
+
+ //fprintf(stderr, "func_call: %s, value: %ld\n", new_func_calls.back().func_name.c_str(), value_int);
+ }
+
+ //fprintf(stderr, "declaration body: %s\n", var_body_str.c_str());
+ for(const std::string &func_decl_str : var_body) {
+ const size_t func_name_split_index = func_decl_str.find(':');
+ if(func_name_split_index == std::string::npos) return false;
+
+ const size_t func_start_index = func_decl_str.find('{', func_name_split_index + 1);
+ if(func_start_index == std::string::npos) return false;
+
+ std::string func_name = func_decl_str.substr(0, func_name_split_index);
+ func_name = strip(func_name);
+ std::string function_body = func_decl_str.substr(func_start_index + 1);
+ function_body = strip(function_body);
+ if(!function_body.empty() && function_body.back() == '}')
+ function_body.pop_back();
+
+ DecryptFunction decrypt_function;
+ if(function_body == "a.reverse()")
+ decrypt_function = DecryptFunction::REVERSE;
+ else if(function_body == "a.splice(0,b)")
+ decrypt_function = DecryptFunction::SPLICE;
+ else
+ decrypt_function = DecryptFunction::SWAP;
+ //fprintf(stderr, "declared function: %s, body: |%s|\n", func_name.c_str(), function_body.c_str());
+ new_func_decls[std::move(func_name)] = decrypt_function;
+ }
+
+ for(const auto &func_call : new_func_calls) {
+ if(new_func_decls.find(func_call.func_name) == new_func_decls.end()) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: declaration for decryption function %s not found\n", func_call.func_name.c_str());
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 10 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ YoutubeSignatureDecryptor::YoutubeSignatureDecryptor() {
+ {
+ Path youtube_cache_dir = get_cache_dir().join("youtube");
+ if(create_directory_recursive(youtube_cache_dir) != 0) {
+ show_notification("QuickMedia", "Failed to create youtube cache directory", Urgency::CRITICAL);
+ return;
+ }
+
+ youtube_cache_dir.join("decryption_function");
+ if(file_get_content(youtube_cache_dir, decryption_function) == 0) {
+ file_get_last_modified_time_seconds(youtube_cache_dir.data.c_str(), &decrypt_function_last_updated);
+
+ size_t newline_index = decryption_function.find('\n');
+ if(newline_index != std::string::npos) {
+ std::string function_body_str = decryption_function.substr(0, newline_index);
+ std::string var_body_str = decryption_function.substr(newline_index + 1);
+
+ std::vector<DecryptFuncCall> new_func_calls;
+ std::map<std::string, DecryptFunction> new_func_decls;
+ if(js_code_to_operations(function_body_str, var_body_str, new_func_calls, new_func_decls)) {
+ func_calls = std::move(new_func_calls);
+ func_decls = std::move(new_func_decls);
+
+ time_t time_now = time(nullptr);
+ if(time_now - decrypt_function_last_updated <= timeout_default_sec)
+ up_to_date = true;
+ }
+ }
+ }
+ }
+
+ running = true;
+ poll_task = AsyncTask<void>([this]() mutable {
+ bool has_notified_error = false;
+ int decrypt_function_update_timeout_seconds = timeout_default_sec;
+ while(running) {
+ time_t time_now = time(nullptr);
+ if(time_now - decrypt_function_last_updated > decrypt_function_update_timeout_seconds) {
+ int update_res = update_decrypt_function();
+ if(update_res != 0) {
+ if(!has_notified_error) {
+ if(program_is_dead_in_current_thread()) {
+ running = false;
+ return;
+ } else if(update_res == U_DEC_FUN_NET_ERR) {
+ show_notification("QuickMedia", "Failed to decrypt youtube signature. Is your internet down?", Urgency::CRITICAL);
+ decrypt_function_update_timeout_seconds = 10;
+ } else if(update_res == U_DEC_FUN_FAILED_MATCH_ERR) {
+ show_notification("QuickMedia", "Failed to decrypt youtube signature. Make sure you are running the latest version of QuickMedia", Urgency::CRITICAL);
+ running = false;
+ return;
+ }
+ }
+ has_notified_error = true;
+ } else {
+ decrypt_function_update_timeout_seconds = timeout_default_sec;
+ }
+ }
+ usleep(1 * 1000 * 1000); // 1 second
+ }
+ });
+ }
+
+ YoutubeSignatureDecryptor::~YoutubeSignatureDecryptor() {
+ running = false;
+ }
+
+ // static
+ YoutubeSignatureDecryptor& YoutubeSignatureDecryptor::get_instance() {
+ if(!instance)
+ instance = new YoutubeSignatureDecryptor();
+ return *instance;
+ }
+
+ bool YoutubeSignatureDecryptor::decrypt(const std::string &s, const std::string &sp, std::string &sig_key, std::string &sig_value) {
+ if(s.empty() || sp.empty())
+ return false;
+
+ if(!up_to_date) {
+ int num_tries = 0;
+ const int max_tries = 30;
+ while(running && num_tries < max_tries && !program_is_dead_in_current_thread()) { // 6 seconds in total
+ std::lock_guard<std::mutex> lock(update_signature_mutex);
+ if(up_to_date)
+ break;
+ ++num_tries;
+ usleep(200 * 1000); // 200 milliseconds
+ }
+
+ if(num_tries == max_tries) {
+ show_notification("QuickMedia", "Failed to get decryption function for youtube. Make sure your internet is up and that you are running the latest version of QuickMedia", Urgency::CRITICAL);
+ return false;
+ }
+ }
+
+ std::lock_guard<std::mutex> lock(update_signature_mutex);
+ std::string sig = s;
+ for(const auto &func_call : func_calls) {
+ auto func_decl_it = func_decls.find(func_call.func_name);
+ assert(func_decl_it != func_decls.end());
+
+ switch(func_decl_it->second) {
+ case DecryptFunction::REVERSE: {
+ std::reverse(sig.begin(), sig.end());
+ break;
+ }
+ case DecryptFunction::SPLICE: {
+ long int erase_index = func_call.arg;
+ if(erase_index > 0 && (size_t)erase_index < sig.size())
+ sig.erase(0, erase_index);
+ break;
+ }
+ case DecryptFunction::SWAP: {
+ if(sig.empty()) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: sig unexpectedly empty in swap\n");
+ } else {
+ char c = sig[0];
+ sig[0] = sig[func_call.arg % sig.size()];
+ sig[func_call.arg % sig.size()] = c;
+ }
+ break;
+ }
+ }
+ }
+
+ sig_key = sp;
+ sig_value = sig;
+ return true;
+ }
+
+ int YoutubeSignatureDecryptor::update_decrypt_function() {
+ std::string response;
+ DownloadResult download_result = download_to_string("https://www.youtube.com/watch?v=CvFH_6DNRCY&gl=US&hl=en", response, {}, true);
+ if(download_result != DownloadResult::OK) {
+ fprintf(stderr, "YoutubeSignatureDecryptor::update_decrypt_function failed. Failed to get youtube page\n");
+ return U_DEC_FUN_NET_ERR;
+ }
+
+ std::smatch base_js_match;
+ if(!std::regex_search(response, base_js_match, std::regex(R"END((\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base\.js))END", std::regex::ECMAScript)) || base_js_match.size() != 2) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 1 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return U_DEC_FUN_FAILED_MATCH_ERR;
+ }
+
+ const std::string &url = base_js_match[1].str();
+ download_result = download_to_string("https://www.youtube.com" + url, response, {}, true);
+ if(download_result != DownloadResult::OK) {
+ fprintf(stderr, "YoutubeSignatureDecryptor::update_decrypt_function failed. Failed to get https://www.youtube.com%s\n", url.c_str());
+ return U_DEC_FUN_NET_ERR;
+ }
+
+ std::smatch function_match;
+ if(!std::regex_search(response, function_match, std::regex(R"END((^|\n)\w+=function\(\w\)\{\w=\w\.split\(""\);([^\}]*)\})END", std::regex::ECMAScript)) || function_match.size() != 3) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 2 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return U_DEC_FUN_FAILED_MATCH_ERR;
+ }
+
+ std::string function_body_str = function_match[2].str();
+ size_t last_semicolon_index = function_body_str.rfind(';');
+ if(last_semicolon_index == std::string::npos) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 3 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return U_DEC_FUN_FAILED_MATCH_ERR;
+ }
+
+ function_body_str.erase(last_semicolon_index, function_body_str.size());
+ string_replace_all(function_body_str, '\n', ' ');
+
+ size_t var_dot_index = function_body_str.find('.');
+ if(var_dot_index == std::string::npos) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 5 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return U_DEC_FUN_FAILED_MATCH_ERR;
+ }
+
+ std::string var_name = function_body_str.substr(0, var_dot_index);
+ string_replace_all(response, '\n', ' ');
+ std::smatch var_body_match;
+ if(!std::regex_search(response, var_body_match, std::regex("var " + var_name + "=\\{(.*?)\\};", std::regex::ECMAScript)) || var_body_match.size() != 2) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 6 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return U_DEC_FUN_FAILED_MATCH_ERR;
+ }
+
+ std::string var_body_str = var_body_match[1].str();
+ string_replace_all(var_body_str, '\n', ' ');
+
+ std::vector<DecryptFuncCall> new_func_calls;
+ std::map<std::string, DecryptFunction> new_func_decls;
+ if(!js_code_to_operations(function_body_str, var_body_str, new_func_calls, new_func_decls)) {
+ fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 7 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n");
+ return U_DEC_FUN_FAILED_MATCH_ERR;
+ }
+
+ {
+ std::lock_guard<std::mutex> lock(update_signature_mutex);
+ decryption_function = function_body_str + "\n" + var_body_str;
+ decrypt_function_last_updated = time(nullptr);
+ up_to_date = true;
+ func_calls = std::move(new_func_calls);
+ func_decls = std::move(new_func_decls);
+ }
+
+ file_overwrite_atomic(get_cache_dir().join("youtube").join("decryption_function"), decryption_function);
+ return 0;
+ }
+} \ No newline at end of file