From 73b077835dc6085c2642451fa7dbde629b9eadfc Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 9 Aug 2019 03:35:56 +0200 Subject: Readd video seek, make search asynchronous --- src/QuickMedia.cpp | 158 ++++++++++++++++++++++++-------------- src/SearchBar.cpp | 1 + src/VideoPlayer.cpp | 188 ++++++++++++++++++++++++++++++++++++---------- src/plugins/Manganelo.cpp | 6 +- src/plugins/Plugin.cpp | 6 +- src/plugins/Youtube.cpp | 14 ++-- 6 files changed, 261 insertions(+), 112 deletions(-) (limited to 'src') diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index cb58a1d..32e0b2d 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include const sf::Color front_color(43, 45, 47); @@ -70,15 +69,6 @@ namespace QuickMedia { return search_result; } - static void update_search_suggestions(const sf::String &text, Body *body, Plugin *plugin) { - body->clear_items(); - if(text.isEmpty()) - return; - - SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items); - body->clamp_selection(); - } - static void usage() { fprintf(stderr, "usage: QuickMedia \n"); fprintf(stderr, "OPTIONS:\n"); @@ -216,8 +206,10 @@ namespace QuickMedia { } void Program::search_suggestion_page() { - search_bar->onTextUpdateCallback = [this](const std::string &text) { - update_search_suggestions(text, body, current_plugin); + std::string update_search_text; + bool search_running = false; + search_bar->onTextUpdateCallback = [&update_search_text](const std::string &text) { + update_search_text = text; }; search_bar->onTextSubmitCallback = [this](const std::string &text) { @@ -273,6 +265,23 @@ namespace QuickMedia { search_bar->update(); + if(!update_search_text.empty() && !search_running) { + search_suggestion_future = std::async(std::launch::async, [this, update_search_text]() { + BodyItems result; + SuggestionResult suggestion_result = current_plugin->update_search_suggestions(update_search_text, result); + return result; + }); + update_search_text.clear(); + search_running = true; + } + + if(search_running && search_suggestion_future.valid() && search_suggestion_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + body->clear_items(); + body->items = search_suggestion_future.get(); + body->clamp_selection(); + search_running = false; + } + window.clear(back_color); body->draw(window, body_pos, body_size); search_bar->draw(window); @@ -353,27 +362,39 @@ namespace QuickMedia { throw std::runtime_error("Failed to open display to X11 server"); XDisplayScope display_scope(disp); - #if 0 - sf::RenderWindow video_window(sf::VideoMode(300, 300), "QuickMedia Video Player"); - video_window.setVerticalSyncEnabled(true); - XReparentWindow(disp, video_window.getSystemHandle(), window.getSystemHandle(), 0, 0); - XMapWindow(disp, video_window.getSystemHandle()); - XSync(disp, False); - video_window.setSize(sf::Vector2u(400, 300)); - #endif + std::unique_ptr video_player_ui_window; + auto on_window_create = [disp, &video_player_ui_window](sf::WindowHandle video_player_window) { + int screen = DefaultScreen(disp); + Window ui_window = XCreateWindow(disp, RootWindow(disp, screen), + 0, 0, 1, 1, 0, + DefaultDepth(disp, screen), + InputOutput, + DefaultVisual(disp, screen), + 0, NULL); + + XReparentWindow(disp, ui_window, video_player_window, 0, 0); + XMapWindow(disp, ui_window); + XFlush(disp); + + video_player_ui_window = std::make_unique(ui_window); + }; // This variable is needed because calling play_video is not possible in onPlaybackEndedCallback bool play_next_video = false; + bool ui_resize = true; std::unique_ptr video_player; - auto play_video = [this, &video_player, &play_next_video]() { + auto play_video = [this, &video_player, &play_next_video, &on_window_create, &video_player_ui_window, &ui_resize]() { printf("Playing video: %s\n", content_url.c_str()); watched_videos.insert(content_url); - video_player = std::make_unique([this, &play_next_video](const char *event_name) { + video_player = std::make_unique([this, &play_next_video, &video_player_ui_window, &ui_resize](const char *event_name) { if(strcmp(event_name, "end-file") == 0) { + video_player_ui_window = nullptr; + ui_resize = true; + std::string new_video_url; - std::vector> related_media = current_plugin->get_related_media(content_url); + BodyItems related_media = current_plugin->get_related_media(content_url); // Find video that hasn't been played before in this video session for(auto it = related_media.begin(), end = related_media.end(); it != end; ++it) { if(watched_videos.find((*it)->url) == watched_videos.end()) { @@ -394,7 +415,7 @@ namespace QuickMedia { // TODO: This doesn't seem to work correctly right now, it causes video to become black when changing video (context reset bug). //video_player->load_file(video_url); } - }); + }, on_window_create); VideoPlayer::Error err = video_player->load_video(content_url.c_str(), window.getSystemHandle()); if(err != VideoPlayer::Error::OK) { @@ -406,11 +427,18 @@ namespace QuickMedia { }; play_video(); + auto on_doubleclick = []() { + // TODO: Toggle fullscreen of video here + }; + sf::Clock time_since_last_left_click; int left_click_counter; sf::Event event; - sf::RectangleShape rect(sf::Vector2f(500, 500)); + sf::RectangleShape rect; + rect.setFillColor(sf::Color::Red); + sf::Clock get_progress_timer; + double progress = 0.0; while (current_page == Page::VIDEO_CONTENT) { if(play_next_video) { @@ -421,38 +449,16 @@ namespace QuickMedia { while (window.pollEvent(event)) { base_event_handler(event, Page::SEARCH_SUGGESTION); if(event.type == sf::Event::Resized) { - //video_window.setSize(sf::Vector2u(event.size.width, event.size.height)); - } else if(event.key.code == sf::Keyboard::Space) { + if(video_player_ui_window) + ui_resize = true; + } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Space) { if(video_player->toggle_pause() != VideoPlayer::Error::OK) { fprintf(stderr, "Failed to toggle pause!\n"); } - } - } - - #if 0 - while(video_window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; - } else if(event.type == sf::Event::Resized) { - sf::FloatRect visible_area(0, 0, event.size.width, event.size.height); - video_window.setView(sf::View(visible_area)); - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - return; - } - - if(event.key.code == sf::Keyboard::Space) { - if(video_player.toggle_pause() != VideoPlayer::Error::OK) { - fprintf(stderr, "Failed to toggle pause!\n"); - } - } - } - - if(event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left) { + } else if(event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left) { if(time_since_last_left_click.restart().asMilliseconds() <= DOUBLE_CLICK_TIME) { if(++left_click_counter == 2) { - //on_doubleclick(); + on_doubleclick(); left_click_counter = 0; } } else { @@ -460,21 +466,57 @@ namespace QuickMedia { } } } - #endif + + if(video_player_ui_window) { + while(video_player_ui_window->pollEvent(event)) { + if(event.type == sf::Event::Resized) { + sf::FloatRect visible_area(0, 0, event.size.width, event.size.height); + video_player_ui_window->setView(sf::View(visible_area)); + } + } + } VideoPlayer::Error update_err = video_player->update(); if(update_err == VideoPlayer::Error::FAIL_TO_CONNECT_TIMEOUT) { show_notification("Video player", "Failed to connect to mpv ipc after 5 seconds", Urgency::CRITICAL); current_page = Page::SEARCH_SUGGESTION; return; + } else if(update_err != VideoPlayer::Error::OK) { + show_notification("Video player", "Unexpected error while updating", Urgency::CRITICAL); + current_page = Page::SEARCH_SUGGESTION; + return; } - window.clear(); - window.display(); - // TODO: Show loading video animation - //video_window.clear(sf::Color::Red); - //video_window.display(); + //window.clear(); + //window.display(); + + if(get_progress_timer.getElapsedTime().asMilliseconds() >= 500) { + get_progress_timer.restart(); + video_player->get_progress(&progress); + } + + if(video_player_ui_window) { + const float ui_height = window_size.y * 0.01f; + if(ui_resize) { + ui_resize = false; + video_player_ui_window->setSize(sf::Vector2u(window_size.x, ui_height)); + video_player_ui_window->setPosition(sf::Vector2i(0, window_size.y - ui_height)); + } + + // TODO: Make window transparent, so the ui overlay for the video has transparency + video_player_ui_window->clear(sf::Color(33, 33, 33)); + rect.setSize(sf::Vector2f(window_size.x * progress, window_size.y * 0.01)); + video_player_ui_window->draw(rect); + video_player_ui_window->display(); + + if(sf::Mouse::isButtonPressed(sf::Mouse::Left)) { + auto mouse_pos = sf::Mouse::getPosition(window); + if(mouse_pos.y >= window_size.y - ui_height) { + video_player->set_progress((double)mouse_pos.x / (double)window_size.x); + } + } + } } } diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index 1094883..b967224 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -31,6 +31,7 @@ namespace QuickMedia { void SearchBar::update() { if(updated_search && time_since_search_update.getElapsedTime().asMilliseconds() >= text_autosearch_delay) { + time_since_search_update.restart(); updated_search = false; sf::String str = text.getString(); if(show_placeholder) diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index e1c8f73..0e6735a 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -2,6 +2,7 @@ #include "../include/Program.h" #include #include +#include #include #include @@ -11,18 +12,29 @@ #include const int RETRY_TIME_MS = 1000; -const int MAX_RETRIES = 5; +const int MAX_RETRIES_CONNECT = 5; +const int MAX_RETRIES_FIND_WINDOW = 10; +const int READ_TIMEOUT_MS = 200; namespace QuickMedia { - VideoPlayer::VideoPlayer(EventCallbackFunc _event_callback) : + VideoPlayer::VideoPlayer(EventCallbackFunc _event_callback, VideoPlayerWindowCreateCallback _window_create_callback) : video_process_id(-1), ipc_socket(-1), connected_to_ipc(false), connect_tries(0), + find_window_tries(0), event_callback(_event_callback), - alive(true) + window_create_callback(_window_create_callback), + window_handle(0), + parent_window(0), + display(nullptr), + request_id(1), + expected_request_id(0), + request_response_data(Json::nullValue) { - + display = XOpenDisplay(NULL); + if (!display) + throw std::runtime_error("Failed to open display to X11 server"); } VideoPlayer::~VideoPlayer() { @@ -35,12 +47,13 @@ namespace QuickMedia { if(video_process_id != -1) remove(ipc_server_path); - alive = false; - if(event_read_thread.joinable()) - event_read_thread.join(); + if(display) + XCloseDisplay(display); } - VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle parent_window) { + VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window) { + parent_window = _parent_window; + if(!tmpnam(ipc_server_path)) { perror("Failed to generate ipc file name"); return Error::FAIL_TO_GENERATE_IPC_FILENAME; @@ -49,7 +62,7 @@ namespace QuickMedia { const std::string parent_window_str = std::to_string(parent_window); const char *args[] = { "mpv", /*"--keep-open=yes", "--keep-open-pause=no",*/ "--input-ipc-server", ipc_server_path, "--no-config", "--no-input-default-bindings", "--input-vo-keyboard=no", "--no-input-cursor", - "--cache-secs=120", "--demuxer-max-bytes=20M", "--demuxer-max-back-bytes=10M", + "--cache-secs=120", "--demuxer-max-bytes=40M", "--demuxer-max-back-bytes=20M", /*"--vo=gpu", "--hwdec=auto",*/ "--wid", parent_window_str.c_str(), "--", path, nullptr }; if(exec_program_async(args, &video_process_id) != 0) @@ -71,40 +84,84 @@ namespace QuickMedia { return Error::OK; } - VideoPlayer::Error VideoPlayer::load_video(const char *path, sf::WindowHandle parent_window) { + VideoPlayer::Error VideoPlayer::load_video(const char *path, sf::WindowHandle _parent_window) { + // 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 not at least. + assert(parent_window == 0 || parent_window == _parent_window); if(video_process_id == -1) - return launch_video_process(path, parent_window); + return launch_video_process(path, _parent_window); std::string cmd = "loadfile "; cmd += path; return send_command(cmd.c_str(), cmd.size()); } + static std::vector get_child_window(Display *display, Window window) { + std::vector result; + Window root_window; + Window parent_window; + Window *child_window; + unsigned int num_children; + if(XQueryTree(display, window, &root_window, &parent_window, &child_window, &num_children) != 0) { + for(unsigned int i = 0; i < num_children; i++) + result.push_back(child_window[i]); + } + return result; + } + VideoPlayer::Error VideoPlayer::update() { if(ipc_socket == -1) return Error::INIT_FAILED; - if(connect_tries == MAX_RETRIES) + if(connect_tries == MAX_RETRIES_CONNECT) return Error::FAIL_TO_CONNECT_TIMEOUT; - if(!connected_to_ipc && ipc_connect_retry_timer.getElapsedTime().asMilliseconds() >= RETRY_TIME_MS) { + if(find_window_tries == MAX_RETRIES_FIND_WINDOW) + return Error::FAIL_TO_FIND_WINDOW; + + if(!connected_to_ipc && retry_timer.getElapsedTime().asMilliseconds() >= RETRY_TIME_MS) { + retry_timer.restart(); if(connect(ipc_socket, (struct sockaddr*)&ipc_addr, sizeof(ipc_addr)) == -1) { ++connect_tries; - if(connect_tries == MAX_RETRIES) { - fprintf(stderr, "Failed to connect to mpv ipc after 5 seconds, last error: %s\n", strerror(errno)); + if(connect_tries == MAX_RETRIES_CONNECT) { + fprintf(stderr, "Failed to connect to mpv ipc after %d seconds, last error: %s\n", RETRY_TIME_MS * MAX_RETRIES_CONNECT, strerror(errno)); return Error::FAIL_TO_CONNECT_TIMEOUT; } } else { connected_to_ipc = true; - if(event_callback) - event_read_thread = std::thread(&VideoPlayer::read_ipc_func, this); } } + if(connected_to_ipc && window_handle == 0 && retry_timer.getElapsedTime().asMilliseconds() >= RETRY_TIME_MS) { + retry_timer.restart(); + std::vector child_windows = get_child_window(display, parent_window); + size_t num_children = child_windows.size(); + if(num_children == 0) { + ++find_window_tries; + if(find_window_tries == MAX_RETRIES_FIND_WINDOW) { + fprintf(stderr, "Failed to find mpv window after %d seconds\n", RETRY_TIME_MS * MAX_RETRIES_FIND_WINDOW); + return Error::FAIL_TO_FIND_WINDOW_TIMEOUT; + } + } else if(num_children == 1) { + window_handle = child_windows[0]; + if(window_create_callback) + window_create_callback(window_handle); + } else { + fprintf(stderr, "Expected window to have one child (the video player) but it has %zu\n", num_children); + return Error::UNEXPECTED_WINDOW_ERROR; + } + } + + if(connected_to_ipc && event_callback) { + Error err = read_ipc_func(); + if(err != Error::OK) + return err; + } + return Error::OK; } - void VideoPlayer::read_ipc_func() { + VideoPlayer::Error VideoPlayer::read_ipc_func() { assert(connected_to_ipc); assert(event_callback); @@ -114,35 +171,35 @@ namespace QuickMedia { std::string json_errors; char buffer[2048]; - while(alive) { - ssize_t bytes_read = read(ipc_socket, buffer, sizeof(buffer)); - if(bytes_read == -1) { - int err = errno; - if(err == EAGAIN) { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + ssize_t bytes_read = read(ipc_socket, buffer, sizeof(buffer)); + if(bytes_read == -1) { + int err = errno; + if(err != EAGAIN) { + fprintf(stderr, "Failed to read from ipc socket, error: %s\n", strerror(err)); + return Error::FAIL_TO_READ; + } + } else if(bytes_read > 0) { + int start = 0; + for(int i = 0; i < bytes_read; ++i) { + if(buffer[i] != '\n') continue; - } - fprintf(stderr, "Failed to read from ipc socket, error: %s\n", strerror(err)); - break; - } else if(bytes_read > 0) { - int start = 0; - for(int i = 0; i < bytes_read; ++i) { - if(buffer[i] != '\n') - continue; - - if(json_reader->parse(buffer + start, buffer + i, &json_root, &json_errors)) { - const Json::Value &event = json_root["event"]; - if(event.isString()) - event_callback(event.asCString()); - } else { - fprintf(stderr, "Failed to parse json for ipc: |%.*s|, reason: %s\n", (int)bytes_read, buffer, json_errors.c_str()); + if(json_reader->parse(buffer + start, buffer + i, &json_root, &json_errors)) { + const Json::Value &event = json_root["event"]; + const Json::Value &request_id_json = json_root["request_id"]; + if(event.isString()) + event_callback(event.asCString()); + else if(expected_request_id != 0 && request_id_json.isNumeric() && request_id_json.asUInt() == expected_request_id) { + request_response_data = json_root["data"]; } - - start = i + 1; + } else { + fprintf(stderr, "Failed to parse json for ipc: |%.*s|, reason: %s\n", (int)bytes_read, buffer, json_errors.c_str()); } + + start = i + 1; } } + return Error::OK; } VideoPlayer::Error VideoPlayer::toggle_pause() { @@ -150,6 +207,55 @@ namespace QuickMedia { return send_command(cmd, sizeof(cmd) - 1); } + VideoPlayer::Error VideoPlayer::get_progress(double *result) { + unsigned int cmd_request_id = request_id; + ++request_id; + // Overflow check. 0 is defined as no request, 1 is the first valid one + if(request_id == 0) + request_id = 1; + + std::string cmd = "{ \"command\": [\"get_property\", \"percent-pos\"], \"request_id\": "; + cmd += std::to_string(cmd_request_id) + " }\n"; + Error err = send_command(cmd.c_str(), cmd.size()); + if(err != Error::OK) + return err; + + sf::Clock read_timer; + expected_request_id = cmd_request_id; + do { + err = read_ipc_func(); + if(err != Error::OK) + goto cleanup; + + if(!request_response_data.isNull()) + break; + } while(read_timer.getElapsedTime().asMilliseconds() < READ_TIMEOUT_MS); + + if(request_response_data.isNull()) { + err = Error::READ_TIMEOUT; + goto cleanup; + } + + if(!request_response_data.isDouble()) { + fprintf(stderr, "Read: expected to receive data of type double for request id %u, was type %s\n", cmd_request_id, "TODO: Fill type here"); + err = Error::READ_INCORRECT_TYPE; + goto cleanup; + } + + *result = request_response_data.asDouble() * 0.01; + + cleanup: + expected_request_id = 0; + request_response_data = Json::Value(Json::nullValue); + return err; + } + + VideoPlayer::Error VideoPlayer::set_progress(double progress) { + std::string cmd = "{ \"command\": [\"set_property\", \"percent-pos\", "; + cmd += std::to_string(progress * 100.0) + "] }"; + return send_command(cmd.c_str(), cmd.size()); + } + VideoPlayer::Error VideoPlayer::send_command(const char *cmd, size_t size) { if(!connected_to_ipc) return Error::FAIL_NOT_CONNECTED; diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index e91baf0..ec522d2 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -3,7 +3,7 @@ #include namespace QuickMedia { - SearchResult Manganelo::search(const std::string &url, std::vector> &result_items) { + SearchResult Manganelo::search(const std::string &url, BodyItems &result_items) { std::string website_data; if(download_to_string(url, website_data) != DownloadResult::OK) return SearchResult::NET_ERR; @@ -15,7 +15,7 @@ namespace QuickMedia { result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='chapter-list']/div[class='row']//a", [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (std::vector>*)userdata; + auto *item_data = (BodyItems*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); if(href && text) { @@ -50,7 +50,7 @@ namespace QuickMedia { return true; } - SuggestionResult Manganelo::update_search_suggestions(const std::string &text, std::vector> &result_items) { + SuggestionResult Manganelo::update_search_suggestions(const std::string &text, BodyItems &result_items) { std::string url = "https://manganelo.com/home_json_search"; std::string search_term = "searchword="; search_term += url_param_encode(text); diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index 5d81aad..79c6403 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -10,19 +10,19 @@ static int accumulate_string(char *data, int size, void *userdata) { } namespace QuickMedia { - SearchResult Plugin::search(const std::string &text, std::vector> &result_items) { + SearchResult Plugin::search(const std::string &text, BodyItems &result_items) { (void)text; (void)result_items; return SearchResult::OK; } - SuggestionResult Plugin::update_search_suggestions(const std::string &text, std::vector> &result_items) { + SuggestionResult Plugin::update_search_suggestions(const std::string &text, BodyItems &result_items) { (void)text; (void)result_items; return SuggestionResult::OK; } - std::vector> Plugin::get_related_media(const std::string &url) { + BodyItems Plugin::get_related_media(const std::string &url) { (void)url; return {}; } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 2a3f011..56e8574 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -12,7 +12,7 @@ namespace QuickMedia { return strstr(str, substr); } - static void iterate_suggestion_result(const Json::Value &value, std::vector> &result_items, int &iterate_count) { + static void iterate_suggestion_result(const Json::Value &value, BodyItems &result_items, int &iterate_count) { ++iterate_count; if(value.isArray()) { for(const Json::Value &child : value) { @@ -26,7 +26,7 @@ namespace QuickMedia { } // TODO: Speed this up by using string.find instead of parsing html - SuggestionResult Youtube::update_search_suggestions(const std::string &text, std::vector> &result_items) { + SuggestionResult Youtube::update_search_suggestions(const std::string &text, BodyItems &result_items) { // Keep this for backup. This is using search suggestion the same way youtube does it, but the results // are not as good as doing an actual search. #if 0 @@ -79,7 +79,7 @@ namespace QuickMedia { return SuggestionResult::NET_ERR; struct ItemData { - std::vector> *result_items; + BodyItems *result_items; size_t index; }; ItemData item_data = { &result_items, 0 }; @@ -91,7 +91,7 @@ namespace QuickMedia { result = quickmedia_html_find_nodes_xpath(&html_search, "//h3[class=\"yt-lockup-title\"]/a", [](QuickMediaHtmlNode *node, void *userdata) { - auto *result_items = (std::vector>*)userdata; + auto *result_items = (BodyItems*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *title = quickmedia_html_node_get_attribute_value(node, "title"); // Checking for watch?v helps skipping ads @@ -127,8 +127,8 @@ namespace QuickMedia { return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR; } - std::vector> Youtube::get_related_media(const std::string &url) { - std::vector> result_items; + BodyItems Youtube::get_related_media(const std::string &url) { + BodyItems result_items; std::string website_data; if(download_to_string(url, website_data) != DownloadResult::OK) @@ -141,7 +141,7 @@ namespace QuickMedia { result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class=\"video-list\"]//div[class=\"content-wrapper\"]/a", [](QuickMediaHtmlNode *node, void *userdata) { - auto *result_items = (std::vector>*)userdata; + auto *result_items = (BodyItems*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); // TODO: Also add title for related media if(href && begins_with(href, "/watch?v=")) { -- cgit v1.2.3