aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugins/Soundcloud.hpp20
-rw-r--r--src/plugins/Soundcloud.cpp182
2 files changed, 202 insertions, 0 deletions
diff --git a/plugins/Soundcloud.hpp b/plugins/Soundcloud.hpp
new file mode 100644
index 0000000..cf920be
--- /dev/null
+++ b/plugins/Soundcloud.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "Plugin.hpp"
+
+namespace QuickMedia {
+ class Soundcloud : public Plugin {
+ public:
+ Soundcloud() : Plugin("soundcloud") {}
+ SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override;
+ BodyItems get_related_media(const std::string &url) override;
+ bool search_suggestions_has_thumbnails() const override { return true; }
+ bool search_results_has_thumbnails() const override { return false; }
+ int get_search_delay() const override { return 350; }
+ bool search_suggestion_is_search() const override { return true; }
+ Page get_page_after_search() const override { return Page::VIDEO_CONTENT; }
+ private:
+ std::string last_related_media_playlist_id;
+ BodyItems last_playlist_data;
+ };
+} \ No newline at end of file
diff --git a/src/plugins/Soundcloud.cpp b/src/plugins/Soundcloud.cpp
new file mode 100644
index 0000000..a016f42
--- /dev/null
+++ b/src/plugins/Soundcloud.cpp
@@ -0,0 +1,182 @@
+#include "../../plugins/Soundcloud.hpp"
+#include <quickmedia/HtmlSearch.h>
+#include <json/reader.h>
+#include <string.h>
+
+namespace QuickMedia {
+ static bool begins_with(const char *str, const char *begin_with) {
+ return strncmp(str, begin_with, strlen(begin_with)) == 0;
+ }
+
+ static bool contains(const char *str, const char *substr) {
+ return strstr(str, substr);
+ }
+
+ 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) {
+ iterate_suggestion_result(child, result_items, iterate_count);
+ }
+ } else if(value.isString() && iterate_count > 2) {
+ std::string title = value.asString();
+ auto item = std::make_unique<BodyItem>(title);
+ result_items.push_back(std::move(item));
+ }
+ }
+
+ // TODO: Speed this up by using string.find instead of parsing html
+ SuggestionResult Soundcloud::update_search_suggestions(const std::string &text, BodyItems &result_items) {
+ std::string url = "https://soundcloud.com/search?q=";
+ url += url_param_encode(text);
+
+ std::string website_data;
+ if(download_to_string(url, website_data) != DownloadResult::OK)
+ return SuggestionResult::NET_ERR;
+
+ struct ItemData {
+ BodyItems *result_items;
+ size_t index;
+ };
+ ItemData item_data = { &result_items, 0 };
+
+ QuickMediaHtmlSearch html_search;
+ int result = quickmedia_html_search_init(&html_search, website_data.c_str());
+ if(result != 0)
+ goto cleanup;
+
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//h3[class=\"yt-lockup-title\"]/a",
+ [](QuickMediaHtmlNode *node, void *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
+ if(href && title && begins_with(href, "/watch?v=")) {
+ auto item = std::make_unique<BodyItem>(strip(title));
+ item->url = std::string("https://www.youtube.com") + href;
+ result_items->push_back(std::move(item));
+ }
+ }, &result_items);
+ if(result != 0)
+ goto cleanup;
+
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//span[class=\"yt-thumb-simple\"]//img",
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ ItemData *item_data = (ItemData*)userdata;
+ if(item_data->index >= item_data->result_items->size())
+ return;
+
+ const char *src = quickmedia_html_node_get_attribute_value(node, "src");
+ const char *data_thumb = quickmedia_html_node_get_attribute_value(node, "data-thumb");
+
+ if(src && contains(src, "i.ytimg.com/")) {
+ (*item_data->result_items)[item_data->index]->thumbnail_url = src;
+ ++item_data->index;
+ } else if(data_thumb && contains(data_thumb, "i.ytimg.com/")) {
+ (*item_data->result_items)[item_data->index]->thumbnail_url = data_thumb;
+ ++item_data->index;
+ }
+ }, &item_data);
+
+ cleanup:
+ quickmedia_html_search_deinit(&html_search);
+ return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR;
+ }
+
+ static std::string get_playlist_id_from_url(const std::string &url) {
+ std::string playlist_id = url;
+ size_t list_index = playlist_id.find("&list=");
+ if(list_index == std::string::npos)
+ return playlist_id;
+ return playlist_id.substr(list_index);
+ }
+
+ static std::string remove_index_from_playlist_url(const std::string &url) {
+ std::string result = url;
+ size_t index = result.rfind("&index=");
+ if(index == std::string::npos)
+ return result;
+ return result.substr(0, index);
+ }
+
+ // TODO: Make this faster by using string search instead of parsing html.
+ BodyItems Soundcloud::get_related_media(const std::string &url) {
+ BodyItems result_items;
+ struct ItemData {
+ BodyItems &result_items;
+ size_t index = 0;
+ };
+ ItemData item_data { result_items, 0 };
+
+ std::string modified_url = remove_index_from_playlist_url(url);
+ std::string playlist_id = get_playlist_id_from_url(modified_url);
+ if(playlist_id == last_related_media_playlist_id) {
+ result_items.reserve(last_playlist_data.size());
+ for(auto &data : last_playlist_data) {
+ result_items.push_back(std::make_unique<BodyItem>(*data));
+ }
+ return result_items;
+ }
+
+ std::string website_data;
+ if(download_to_string(modified_url, website_data) != DownloadResult::OK)
+ return result_items;
+
+ QuickMediaHtmlSearch html_search;
+ int result = quickmedia_html_search_init(&html_search, website_data.c_str());
+ if(result != 0)
+ goto cleanup;
+
+ if(!playlist_id.empty()) {
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//a",
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ auto *item_data = (ItemData*)userdata;
+ const char *node_class = quickmedia_html_node_get_attribute_value(node, "class");
+ const char *href = quickmedia_html_node_get_attribute_value(node, "href");
+ if(node_class && href && contains(node_class, "playlist-video")) {
+ auto item = std::make_unique<BodyItem>("");
+ item->url = std::string("https://www.youtube.com") + remove_index_from_playlist_url(href);
+ item_data->result_items.push_back(std::move(item));
+ }
+ }, &item_data);
+
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//li",
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ auto *item_data = (ItemData*)userdata;
+ if(item_data->index >= item_data->result_items.size())
+ return;
+
+ // TODO: Also add title for related media. This data is in @data-title
+ const char *data_thumbnail_url = quickmedia_html_node_get_attribute_value(node, "data-thumbnail-url");
+ if(data_thumbnail_url && contains(data_thumbnail_url, "ytimg.com")) {
+ item_data->result_items[item_data->index]->thumbnail_url = data_thumbnail_url;
+ ++item_data->index;
+ }
+ }, &item_data);
+ }
+
+ // We want non-playlist videos every when there is a playlist, since we want to play non-playlist videos after
+ // playing all playlist videos
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class=\"video-list\"]//div[class=\"content-wrapper\"]/a",
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ auto *item_data = (ItemData*)userdata;
+ const char *href = quickmedia_html_node_get_attribute_value(node, "href");
+ // TODO: Also add title for related media and thumbnail
+ if(href && begins_with(href, "/watch?v=")) {
+ auto item = std::make_unique<BodyItem>("");
+ item->url = std::string("https://www.youtube.com") + href;
+ item_data->result_items.push_back(std::move(item));
+ }
+ }, &item_data);
+
+ cleanup:
+ last_playlist_data.clear();
+ last_playlist_data.reserve(result_items.size());
+ for(auto &data : result_items) {
+ last_playlist_data.push_back(std::make_unique<BodyItem>(*data));
+ }
+ last_related_media_playlist_id = playlist_id;
+ quickmedia_html_search_deinit(&html_search);
+ return result_items;
+ }
+} \ No newline at end of file