diff options
Diffstat (limited to 'src/odysee.js')
-rw-r--r-- | src/odysee.js | 425 |
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. `"'). */ +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(); +} |