aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/AsyncImageLoader.hpp33
-rw-r--r--include/MessageQueue.hpp4
-rw-r--r--src/AsyncImageLoader.cpp169
-rw-r--r--src/QuickMedia.cpp1
-rw-r--r--src/plugins/Youtube.cpp32
5 files changed, 160 insertions, 79 deletions
diff --git a/include/AsyncImageLoader.hpp b/include/AsyncImageLoader.hpp
index 6258b02..77fd5b4 100644
--- a/include/AsyncImageLoader.hpp
+++ b/include/AsyncImageLoader.hpp
@@ -26,11 +26,19 @@ namespace QuickMedia {
size_t counter = 0;
};
+ struct ThumbnailLoadData {
+ Path path;
+ Path thumbnail_path;
+ bool local;
+ std::shared_ptr<ThumbnailData> thumbnail_data;
+ sf::Vector2i resize_target_size;
+ };
+
// If |symlink_if_no_resize| is false then a copy is made from |thumbnail_path| to |thumbnail_path_resized| instead of a symlink if |thumbnail_path| is not larger than |resize_target_size|.
// One example of why you might not want a symlink is if |thumbnail_path| is a temporary file.
bool create_thumbnail(const Path &thumbnail_path, const Path &thumbnail_path_resized, sf::Vector2i resize_target_size, ContentType content_type, bool symlink_if_no_resize);
- constexpr int NUM_IMAGE_LOAD_THREADS = 4;
+ constexpr int NUM_IMAGE_LOAD_PARALLEL = 4;
class AsyncImageLoader {
public:
@@ -51,14 +59,6 @@ namespace QuickMedia {
AsyncImageLoader(AsyncImageLoader &other) = delete;
AsyncImageLoader& operator=(AsyncImageLoader &other) = delete;
- struct ThumbnailLoadData {
- Path path;
- Path thumbnail_path;
- bool local;
- std::shared_ptr<ThumbnailData> thumbnail_data;
- sf::Vector2i resize_target_size;
- };
-
// set |resize_target_size| to {0, 0} to disable resizing.
// Note: this method is not thread-safe
void load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, std::shared_ptr<ThumbnailData> thumbnail_data);
@@ -66,10 +66,19 @@ namespace QuickMedia {
// Returns -1 if all threads are busy
int get_free_load_index() const;
private:
- bool loading_image[NUM_IMAGE_LOAD_THREADS];
+ struct Download {
+ ReadProgram read_program;
+ int64_t download_start = 0;
+ Path thumbnail_path;
+ std::shared_ptr<ThumbnailData> thumbnail_data;
+ sf::Vector2i resize_target_size;
+ std::string url;
+ };
+
+ std::mutex download_mutex;
// TODO: Use curl single-threaded multi-download feature instead
- AsyncTask<void> download_image_thread[NUM_IMAGE_LOAD_THREADS];
- AsyncTask<void> load_image_thread;
+ Download downloads[NUM_IMAGE_LOAD_PARALLEL];
+ AsyncTask<void> load_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 286739d..ba28431 100644
--- a/include/MessageQueue.hpp
+++ b/include/MessageQueue.hpp
@@ -68,6 +68,10 @@ namespace QuickMedia {
++it;
}
}
+
+ bool is_running() const {
+ return running;
+ }
private:
std::deque<T> data_queue;
std::mutex mutex;
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index a871078..fce745a 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -11,6 +11,7 @@
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/sendfile.h>
+#include <sys/time.h>
#include <fcntl.h>
#include <signal.h>
#include <malloc.h>
@@ -128,6 +129,7 @@ namespace QuickMedia {
// parent
+ // TODO: Do not wait here and instead check if finished in |load_thread|
int status = 0;
if(waitpid(pid, &status, 0) == -1) {
perror("waitpid failed");
@@ -176,44 +178,82 @@ namespace QuickMedia {
return *instance;
}
+ static void process_thumbnail(ThumbnailLoadData &thumbnail_load_data) {
+ thumbnail_load_data.thumbnail_data->image = std::make_unique<sf::Image>();
+
+ Path thumbnail_path_resized = thumbnail_load_data.thumbnail_path;
+ if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0)
+ thumbnail_path_resized.append("_" + std::to_string(thumbnail_load_data.resize_target_size.x) + "x" + std::to_string(thumbnail_load_data.resize_target_size.y));
+
+ if(get_file_type(thumbnail_path_resized) == FileType::REGULAR) {
+ load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_path_resized.data);
+ thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
+ fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_path_resized.data.c_str());
+ return;
+ }
+
+ Path thumbnail_original_path;
+ if(thumbnail_load_data.local)
+ thumbnail_original_path = thumbnail_load_data.path;
+ else
+ thumbnail_original_path = thumbnail_load_data.thumbnail_path;
+
+ if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0)
+ create_thumbnail(thumbnail_original_path, thumbnail_path_resized, thumbnail_load_data.thumbnail_data.get(), thumbnail_load_data.resize_target_size);
+ else
+ load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_original_path.data);
+
+ thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
+ }
+
+ static int64_t timeval_to_milliseconds(struct timeval &time) {
+ return (int64_t)time.tv_sec * 1000 + (int64_t)time.tv_usec / 1000;
+ }
+
AsyncImageLoader::AsyncImageLoader() {
- for(int i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) {
- loading_image[i] = false;
+ for(int i = 0; i < NUM_IMAGE_LOAD_PARALLEL; ++i) {
+ downloads[i].read_program.pid = -1;
+ downloads[i].read_program.read_fd = -1;
}
- load_image_thread = AsyncTask<void>([this]() mutable {
+ load_thread = AsyncTask<void>([this]() mutable {
std::optional<ThumbnailLoadData> thumbnail_load_data_opt;
- while(true) {
- thumbnail_load_data_opt = image_load_queue.pop_wait();
- if(!thumbnail_load_data_opt)
- break;
-
- ThumbnailLoadData &thumbnail_load_data = thumbnail_load_data_opt.value();
- thumbnail_load_data.thumbnail_data->image = std::make_unique<sf::Image>();
-
- Path thumbnail_path_resized = thumbnail_load_data.thumbnail_path;
- if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0)
- thumbnail_path_resized.append("_" + std::to_string(thumbnail_load_data.resize_target_size.x) + "x" + std::to_string(thumbnail_load_data.resize_target_size.y));
-
- if(get_file_type(thumbnail_path_resized) == FileType::REGULAR) {
- load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_path_resized.data);
- thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
- fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_path_resized.data.c_str());
- continue;
+ while(image_load_queue.is_running()) {
+ thumbnail_load_data_opt = image_load_queue.pop_if_available();
+ if(thumbnail_load_data_opt)
+ process_thumbnail(thumbnail_load_data_opt.value());
+
+ for(int i = 0; i < NUM_IMAGE_LOAD_PARALLEL; ++i) {
+ Download &download = downloads[i];
+ if(download.read_program.pid == -1)
+ continue;
+
+ int status = 0;
+ if(wait_program_non_blocking(download.read_program.pid, &status)) {
+ Path tmp_thumbnail_path = download.thumbnail_path;
+ tmp_thumbnail_path.append(".tmp");
+ if(status == 0 && rename_atomic(tmp_thumbnail_path.data.c_str(), download.thumbnail_path.data.c_str()) == 0) {
+ struct timeval time;
+ gettimeofday(&time, nullptr);
+ fprintf(stderr, "Download duration for %s: %ld ms\n", download.url.c_str(), timeval_to_milliseconds(time) - download.download_start);
+
+ ThumbnailLoadData load_data = { std::move(download.url), std::move(download.thumbnail_path), false, download.thumbnail_data, download.resize_target_size };
+ process_thumbnail(load_data);
+ } else {
+ fprintf(stderr, "Thumbnail download failed for %s\n", download.url.c_str());
+ }
+
+ std::lock_guard<std::mutex> lock(download_mutex);
+ close(download.read_program.read_fd);
+ download.read_program.pid = -1;
+ download.read_program.read_fd = -1;
+ download.thumbnail_path.data.clear();
+ download.url.c_str();
+ download.thumbnail_data = nullptr;
+ }
}
- Path thumbnail_original_path;
- if(thumbnail_load_data.local)
- thumbnail_original_path = thumbnail_load_data.path;
- else
- thumbnail_original_path = thumbnail_load_data.thumbnail_path;
-
- if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0)
- create_thumbnail(thumbnail_original_path, thumbnail_path_resized, thumbnail_load_data.thumbnail_data.get(), thumbnail_load_data.resize_target_size);
- else
- load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_original_path.data);
-
- thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
+ std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
});
}
@@ -222,6 +262,17 @@ namespace QuickMedia {
image_load_queue.close();
}
+ static bool download_file_async(const char *url, const char *save_filepath, ReadProgram *read_program) {
+ const char *args[] = {
+ "curl", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-g", "-s", "-L", "-f",
+ "-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
+ "-o", save_filepath,
+ "--", url,
+ nullptr
+ };
+ return exec_program_pipe(args, read_program) == 0;
+ }
+
void AsyncImageLoader::load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, std::shared_ptr<ThumbnailData> thumbnail_data) {
if(thumbnail_data->loading_state != LoadingState::NOT_LOADED)
return;
@@ -239,6 +290,7 @@ namespace QuickMedia {
struct stat file_stat;
memset(&file_stat, 0, sizeof(file_stat));
if(stat(url.c_str(), &file_stat) != 0 || !S_ISREG(file_stat.st_mode)) {
+ fprintf(stderr, "Failed to load thumbnail %s: no such file\n", url.c_str());
thumbnail_data->image = std::make_unique<sf::Image>();
thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
return;
@@ -246,13 +298,13 @@ namespace QuickMedia {
thumbnail_path.append("_" + std::to_string(file_stat.st_mtim.tv_sec));
thumbnail_data->loading_state = LoadingState::LOADING;
- image_load_queue.push({ url, thumbnail_path, true, thumbnail_data, resize_target_size });
+ image_load_queue.push({ url, std::move(thumbnail_path), true, thumbnail_data, resize_target_size });
return;
}
if(get_file_type(thumbnail_path) == FileType::REGULAR) {
thumbnail_data->loading_state = LoadingState::LOADING;
- image_load_queue.push({ url, thumbnail_path, false, thumbnail_data, resize_target_size });
+ image_load_queue.push({ url, std::move(thumbnail_path), false, thumbnail_data, resize_target_size });
return;
}
@@ -260,39 +312,22 @@ namespace QuickMedia {
if(free_index == -1)
return;
- loading_image[free_index] = true;
+ std::lock_guard<std::mutex> lock(download_mutex);
thumbnail_data->loading_state = LoadingState::LOADING;
-
- // 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] = 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;
- if(resize_target_size.x != 0 && resize_target_size.y != 0)
- thumbnail_path_resized.append("_" + std::to_string(resize_target_size.x) + "x" + std::to_string(resize_target_size.y));
-
- if(get_file_type(thumbnail_path_resized) == FileType::REGULAR) {
- load_image_from_file(*thumbnail_data->image, thumbnail_path_resized.data);
- thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
- fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_path_resized.data.c_str());
- return;
- }
-
- if(get_file_type(thumbnail_path.data) == FileType::FILE_NOT_FOUND && download_to_file(url, thumbnail_path.data, {}, true) != DownloadResult::OK) {
- thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
- loading_image[free_index] = false;
- return;
- }
-
- if(resize_target_size.x != 0 && resize_target_size.y != 0)
- create_thumbnail(thumbnail_path, thumbnail_path_resized, thumbnail_data.get(), resize_target_size);
- else
- load_image_from_file(*thumbnail_data->image, thumbnail_path.data);
-
- thumbnail_data->loading_state = LoadingState::FINISHED_LOADING;
- loading_image[free_index] = false;
+ Path tmp_thumbnail_path = thumbnail_path;
+ tmp_thumbnail_path.append(".tmp");
+ if(!download_file_async(url.c_str(), tmp_thumbnail_path.data.c_str(), &downloads[free_index].read_program)) {
+ fprintf(stderr, "Failed to start download of %s\n", url.c_str());
return;
- });
+ }
+
+ struct timeval time;
+ gettimeofday(&time, nullptr);
+ downloads[free_index].download_start = timeval_to_milliseconds(time);
+ downloads[free_index].thumbnail_path = std::move(thumbnail_path);
+ downloads[free_index].thumbnail_data = thumbnail_data;
+ downloads[free_index].resize_target_size = resize_target_size;
+ downloads[free_index].url = url;
}
std::shared_ptr<ThumbnailData> AsyncImageLoader::get_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size) {
@@ -325,8 +360,8 @@ namespace QuickMedia {
}
int AsyncImageLoader::get_free_load_index() const {
- for(int i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) {
- if(!loading_image[i])
+ for(int i = 0; i < NUM_IMAGE_LOAD_PARALLEL; ++i) {
+ if(downloads[i].read_program.pid == -1)
return i;
}
return -1;
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 709bac5..03e67be 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -2870,6 +2870,7 @@ namespace QuickMedia {
youtube_downloader_task = AsyncTask<void>([&youtube_video_media_proxy, &youtube_audio_media_proxy]() {
sf::Clock timer;
const double sleep_time_millisec = 1;
+ // TODO: Poll instead of sleep
while(!program_is_dead_in_current_thread()) {
if(youtube_video_media_proxy)
youtube_video_media_proxy->update();
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index 012a20b..40b6970 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -425,6 +425,33 @@ namespace QuickMedia {
return std::nullopt;
}
+ static bool video_is_live(const Json::Value &video_item_json) {
+ if(!video_item_json.isObject())
+ return false;
+
+ const Json::Value &badges_json = video_item_json["badges"];
+ if(!badges_json.isArray())
+ return false;
+
+ for(const Json::Value &badge_json : badges_json) {
+ if(!badge_json.isObject())
+ continue;
+
+ const Json::Value &metadata_badge_renderer_json = badge_json["metadataBadgeRenderer"];
+ if(!metadata_badge_renderer_json.isObject())
+ continue;
+
+ const Json::Value &style_json = metadata_badge_renderer_json["style"];
+ if(!style_json.isString())
+ continue;
+
+ if(strcmp(style_json.asCString(), "BADGE_STYLE_TYPE_LIVE_NOW") == 0)
+ return true;
+ }
+
+ return false;
+ }
+
static std::shared_ptr<BodyItem> parse_common_video_item(const Json::Value &video_item_json, std::unordered_set<std::string> &added_videos) {
const Json::Value &video_id_json = video_item_json["videoId"];
if(!video_id_json.isString())
@@ -491,6 +518,11 @@ namespace QuickMedia {
desc += '\n';
desc += length.value();
}
+ if(video_is_live(video_item_json)) {
+ if(!desc.empty())
+ desc += '\n';
+ desc += "Live now";
+ }
if(owner_text) {
if(!desc.empty())
desc += '\n';