aboutsummaryrefslogtreecommitdiff
path: root/src/odysee.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/odysee.js')
-rw-r--r--src/odysee.js425
1 files changed, 425 insertions, 0 deletions
diff --git a/src/odysee.js b/src/odysee.js
new file mode 100644
index 0000000..cd1c49c
--- /dev/null
+++ b/src/odysee.js
@@ -0,0 +1,425 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Make video playback and search on odysee.com functional without nonfree js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute forms of that code without the copy of the GNU
+ * GPL normally required by section 4, provided you include this
+ * license notice and, in case of non-source distribution, a URL
+ * through which recipients can access the Corresponding Source.
+ * If you modify file(s) with this exception, you may extend this
+ * exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this
+ * exception statement from your version.
+ *
+ * As a special exception to the GPL, any HTML file which merely
+ * makes function calls to this code, and for that purpose
+ * includes it by reference shall be deemed a separate work for
+ * copyright law purposes. If you modify this code, you may extend
+ * this exception to your version of the code, but you are not
+ * obligated to do so. If you do not wish to do so, delete this
+ * exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+/* use with https://odysee.com/*** */
+
+/* Helper functions for ajax. */
+
+function ajax_callback(xhttp, cb)
+{
+ cb(xhttp.response);
+}
+
+function perform_ajax(method, url, callback, err_callback, data)
+{
+ const xhttp = new XMLHttpRequest();
+ xhttp.onload = () => ajax_callback(xhttp, callback);
+ xhttp.onerror = err_callback;
+ xhttp.onabort = err_callback;
+ xhttp.open(method, url, true);
+ try {
+ xhttp.send(data);
+ } catch(e) {
+ err_callback();
+ }
+}
+
+/* Helper functions for strings with HTML entities (e.g. `&quot;'). */
+function HTML_decode(text)
+{
+ const tmp_span = document.createElement("span");
+ tmp_span.innerHTML = text;
+ return tmp_span.textContent;
+}
+
+/* Odysee API servers. */
+
+const odysee_resolve_url = "https://api.na-backend.odysee.com/api/v1/proxy?m=resolve";
+const lighthouse_search_url = "https://lighthouse.odysee.com/search";
+
+/*
+ * If we're on a video page, show the video. Use JSON data embedded in <head>
+ * if possible. If not - fetch video data using Odysee API.
+ */
+
+let data = null;
+
+function process_json_script(json_script)
+{
+ try {
+ data = JSON.parse(json_script.textContent);
+ } catch (e) {
+ console.log("Error parsing content data", e);
+ }
+}
+
+for (const json_script of document.querySelectorAll("head script")) {
+ if (["blocked-type", "type"].map(a => json_script.getAttribute(a))
+ .includes("application/ld+json"))
+ process_json_script(json_script);
+}
+
+const body = document.createElement("body");
+const video_container = document.createElement("div");
+
+body.appendChild(video_container);
+
+function show_video(content_url, title, upload_date, description)
+{
+ if (content_url) {
+ const video = document.createElement("video");
+ const source = document.createElement("source");
+
+ source.src = content_url;
+
+ video.setAttribute("width", "100%");
+ video.setAttribute("height", "auto");
+ video.setAttribute("controls", "");
+
+ video.appendChild(source);
+
+ video_container.appendChild(video);
+ }
+
+ if (title) {
+ const h1 = document.createElement("h1");
+
+ h1.textContent = HTML_decode(title);
+ h1.setAttribute("style", "color: #555;");
+
+ video_container.appendChild(h1);
+ }
+
+ if (upload_date) {
+ try {
+ const date = new Date(upload_date).toString();
+ const date_div = document.createElement("div");
+
+ date_div.textContent = `Uploaded: ${date}`;
+ date_div.setAttribute("style", "font-size: 14px; font-weight: bold; margin-bottom: 5px;");
+
+ video_container.appendChild(date_div);
+ } catch(e) {
+ console.log("Error parsing content upload date", e);
+ }
+ }
+
+ if (description) {
+ const description_div = document.createElement("div");
+
+ description_div.textContent = HTML_decode(description);
+ description_div.setAttribute("style", "white-space: pre;");
+
+ video_container.appendChild(description_div);
+ }
+}
+
+function show_video_from_query(response)
+{
+ try {
+ var result = Object.values(JSON.parse(response).result)[0];
+
+ if (result.value_type !== "stream")
+ return;
+
+ var date = result.timestamp * 1000;
+ var description = result.value.description;
+ var title = result.value.title;
+ const name = encodeURIComponent(result.name);
+ var url = `https://odysee.com/$/stream/${name}/${result.claim_id}`;
+ } catch (e) {
+ return;
+ }
+
+ show_video(url, title, date, description);
+}
+
+function fetch_show_video(name, claim_id)
+{
+ const payload = {
+ jsonrpc: "2.0",
+ method: "resolve",
+ params: {
+ urls: [`lbry://${decodeURIComponent(name)}#${claim_id}`],
+ include_purchase_receipt: true
+ },
+ id: Math.round(Math.random() * 10**14)
+ };
+
+ perform_ajax("POST", odysee_resolve_url, show_video_from_query,
+ () => null, JSON.stringify(payload));
+}
+
+if (data && typeof data === "object" && data["@type"] === "VideoObject") {
+ show_video(data.contentUrl, data.name, data.uploadDate, data.description);
+} else {
+ const match = /\/([^/]+):([0-9a-f]+)$/.exec(document.URL);
+ if (match)
+ fetch_show_video(match[1], match[2]);
+}
+
+/* Show search. */
+
+const search_input = document.createElement("input");
+const search_submit = document.createElement("button");
+const search_form = document.createElement("form");
+const error_div = document.createElement("div");
+
+search_submit.textContent = "Search Odysee";
+
+search_form.setAttribute("style", "margin: 15px 0 0 0;");
+
+search_form.appendChild(search_input);
+search_form.appendChild(search_submit);
+
+error_div.textContent = "Failed to perform search :c";
+error_div.setAttribute("style", "display: none;");
+
+body.appendChild(search_form);
+body.appendChild(error_div);
+
+/* Replace the UI. */
+
+document.documentElement.replaceChild(body, document.body);
+
+/* Add the logic of performing search and showing results. */
+
+function show_error()
+{
+ error_div.setAttribute("style", "color: #b44;");
+}
+
+function clear_error()
+{
+ error_div.setAttribute("style", "display: none;");
+}
+
+let results_div = null;
+const load_more_but = document.createElement("button");
+
+load_more_but.textContent = "Load more";
+
+function show_search_entries(new_results_div, response)
+{
+ try {
+ var results = Object.values(JSON.parse(response).result);
+ } catch (e) {
+ console.log("Failed to parse search response :c",
+ "Bad response format from api.na-backend.odysee.com.");
+ show_error();
+ return;
+ }
+
+ for (const result of results) {
+ try {
+ if (result.value_type !== "stream")
+ continue;
+
+ let channel_specifier = "";
+ let channel_name = null;
+ try {
+ channel_name = result.signing_channel.name;
+ const channel_name_enc = encodeURIComponent(channel_name);
+ const channel_digit = result.signing_channel.claim_id[0];
+ channel_specifier = `${channel_name_enc}:${channel_digit}`;
+ } catch (e) {
+ }
+ const video_name = encodeURIComponent(result.name);
+ const video_id = result.claim_id[0];
+
+ const result_a = document.createElement("a");
+ const thumbnail = document.createElement("img");
+ const title_span = document.createElement("span");
+ const uploader_div = document.createElement("div");
+ const description_div = document.createElement("div");
+
+ thumbnail.setAttribute("style", "width: 100px; height: auto;");
+ thumbnail.setAttribute("alt", result.value.thumbnail.url);
+ thumbnail.src = result.value.thumbnail.url;
+
+ title_span.setAttribute("style", "font-weight: bold;");
+ title_span.textContent = result.value.title;
+
+ uploader_div.setAttribute("style", "margin-left: 5px; font-size: 21px; color: #555;");
+ uploader_div.textContent = channel_name;
+
+ description_div.setAttribute("style", "white-space: pre;");
+ description_div.textContent = result.value.description;
+
+ result_a.setAttribute("style", "display: block; width: 100%; text-decoration: none; color: #333; margin: 8px; border-style: solid; border-width: 3px 0 0 0; border-color: #7aa;");
+ result_a.href = `https://odysee.com${channel_specifier}/${video_name}:${video_id}`;
+
+ if (result.value.thumbnail.url)
+ result_a.appendChild(thumbnail);
+ result_a.appendChild(title_span);
+ if (channel_name)
+ result_a.appendChild(uploader_div);
+ result_a.appendChild(description_div);
+
+ new_results_div.appendChild(result_a);
+ }
+ catch(e) {
+ console.log(e);
+ }
+ }
+
+ clear_error();
+
+ if (results_div)
+ results_div.remove();
+
+ results_div = new_results_div;
+
+ body.appendChild(results_div);
+ body.appendChild(load_more_but);
+
+ enable_search_form();
+}
+
+function search_ajax_error(url)
+{
+ console.log(`Failed to query ${url} :c`);
+ show_error();
+ enable_search_form();
+}
+
+function get_detailed_search_entries(new_results_div, response)
+{
+ /* TODO: Simplify JSON handling using sanitize_JSON.js from Hachette. */
+ try {
+ var response_data = JSON.parse(response);
+ if (!Array.isArray(response_data))
+ throw "Bad response format from lighthouse.odysee.com.";
+ } catch (e) {
+ show_error();
+ console.log("Failed to parse search response :c", e);
+ enable_search_form();
+ return;
+ }
+
+ const callback = r => show_search_entries(new_results_div, r);
+ const lbry_urls = [];
+
+ for (const search_result of response_data) {
+ if (!search_result.claimId || !search_result.name)
+ continue;
+ lbry_urls.push(`lbry://${search_result.name}#${search_result.claimId}`);
+ }
+
+ const payload = {
+ jsonrpc: "2.0",
+ method: "resolve",
+ params: {
+ urls: lbry_urls,
+ include_purchase_receipt: true
+ },
+ id: Math.round(Math.random() * 10**14)
+ };
+
+ const url = odysee_resolve_url;
+
+ perform_ajax("POST", url, callback, () => search_ajax_error(url),
+ JSON.stringify(payload));
+}
+
+function get_search_entries(new_results_div, query, from)
+{
+ const callback = r => get_detailed_search_entries(new_results_div, r);
+ const url = `${lighthouse_search_url}?s=${encodeURIComponent(query)}&size=20&from=${from}&claimType=file,channel&nsfw=false&free_only=true`;
+
+ new_results_div.setAttribute("data-fetched", parseInt(from) + 20);
+
+ perform_ajax("GET", url, callback, () => search_ajax_error(url));
+}
+
+function search(event)
+{
+ if (event)
+ event.preventDefault();
+
+ if (!/[^\s]/.test(search_input.value))
+ return;
+
+ disable_search_form();
+
+ const new_results_div = document.createElement("div");
+
+ new_results_div.setAttribute("data-query", search_input.value);
+
+ get_search_entries(new_results_div, search_input.value, 0);
+}
+
+function search_more()
+{
+ disable_search_form();
+
+ get_search_entries(results_div, results_div.getAttribute("data-query"),
+ results_div.getAttribute("data-fetched"));
+}
+
+load_more_but.addEventListener("click", search_more);
+
+function enable_search_form()
+{
+ search_form.addEventListener("submit", search);
+ search_submit.removeAttribute("disabled");
+ if (results_div)
+ load_more_but.removeAttribute("disabled");
+}
+
+function disable_search_form()
+{
+ search_form.removeEventListener("submit", search);
+ search_submit.setAttribute("disabled", "");
+ load_more_but.setAttribute("disabled", "");
+}
+
+
+enable_search_form();
+
+
+const match = /^[^?]*search\?q=([^&]+)/.exec(document.URL)
+if (match) {
+ search_input.value = decodeURIComponent(match[1]);
+ search();
+}