From e24c60dd6acbb8db5912a7715c302374d7eb18b8 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 18 Feb 2022 18:51:22 +0100 Subject: translate all site fixes to the new Hydrilla format Fixes in new format are yet to be tested. Things may break. Alternative site interfaces were removed. This repository is meant exclusively for holding fixes for js-encumbered websites. Jahoti's SParse code shall be put in a separate repository. --- src/accuweather.js | 19 ++ src/bandcamp.js | 31 +++ src/box.js | 300 ++++++++++++++++++++++++++ src/fedoraaccounts.js | 37 ++++ src/google_drive_files.js | 168 +++++++++++++++ src/google_drive_folders.js | 384 +++++++++++++++++++++++++++++++++ src/google_forms.js | 66 ++++++ src/google_sheets_download.js | 210 ++++++++++++++++++ src/internet_archive_video.js | 91 ++++++++ src/odysee.js | 425 +++++++++++++++++++++++++++++++++++++ src/opencores.js | 46 ++++ src/pcspecialist_cookie_notice.js | 21 ++ src/pcspecialist_display_prices.js | 57 +++++ src/phoronix_benchmarks.js | 38 ++++ src/royal_geographical_society.js | 20 ++ src/santander_centrum24.js | 25 +++ src/stack_exchange_cookienotice.js | 20 ++ src/sumofus.js | 56 +++++ src/vaticannews_videos.js | 24 +++ src/worldcat.js | 67 ++++++ src/yewtube_urls.js | 58 +++++ src/youtube_yewtube_redirection.js | 21 ++ 22 files changed, 2184 insertions(+) create mode 100644 src/accuweather.js create mode 100644 src/bandcamp.js create mode 100644 src/box.js create mode 100644 src/fedoraaccounts.js create mode 100644 src/google_drive_files.js create mode 100644 src/google_drive_folders.js create mode 100644 src/google_forms.js create mode 100644 src/google_sheets_download.js create mode 100644 src/internet_archive_video.js create mode 100644 src/odysee.js create mode 100644 src/opencores.js create mode 100644 src/pcspecialist_cookie_notice.js create mode 100644 src/pcspecialist_display_prices.js create mode 100644 src/phoronix_benchmarks.js create mode 100644 src/royal_geographical_society.js create mode 100644 src/santander_centrum24.js create mode 100644 src/stack_exchange_cookienotice.js create mode 100644 src/sumofus.js create mode 100644 src/vaticannews_videos.js create mode 100644 src/worldcat.js create mode 100644 src/yewtube_urls.js create mode 100644 src/youtube_yewtube_redirection.js (limited to 'src') diff --git a/src/accuweather.js b/src/accuweather.js new file mode 100644 index 0000000..d178451 --- /dev/null +++ b/src/accuweather.js @@ -0,0 +1,19 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +for (var nonAd of document.querySelectorAll('.ads-not-loaded .non-ad')) nonAd.style.visibility = 'visible'; diff --git a/src/bandcamp.js b/src/bandcamp.js new file mode 100644 index 0000000..eafcc1e --- /dev/null +++ b/src/bandcamp.js @@ -0,0 +1,31 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var div, player, playerBox = document.querySelector('.inline_player'); +playerBox.innerHTML = ''; + +for (var track of JSON.parse(document.querySelector('[data-tralbum]').dataset.tralbum).trackinfo) { + div = document.createElement('div'); + player = document.createElement('audio'); + player.controls = 'controls'; + + div.innerText = track.title + ': '; + player.src = track.file['mp3-128']; // Is this always available? + div.append(player); + playerBox.append(div); +} diff --git a/src/box.js b/src/box.js new file mode 100644 index 0000000..f02bf3a --- /dev/null +++ b/src/box.js @@ -0,0 +1,300 @@ +/** + * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions + * + * Copyright 2022 Jacob K + * Copyright 2022 Wojtek Kosior + * + * 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 . + * + * 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. + */ + +// meta: match should be https://***.app.box.com/s/* (*** instead of * for the first section because otherwise plain app.box.com URLs won't work) +// meta: some test cases (mostly found at https://old.reddit.com/search?q="box.com"&include_over_18=on&sort=new) + // https://uwmadison.app.box.com/s/ydht2incbdmw1lhpjg5t40adguc0fm14 + // umadison's enrollment report + // pdf + // https://app.box.com/s/gc4ygloi4qtimeh98dq9mmydyuydawcn + // password-protected 7z file (nsfw) + // https://app.box.com/shared/static/su6xx6zx50cd68zdtbm3wfxhh9kwke8x.zip + // a soundtrack in a zip file + // This is a static download, so it works without this script. + // https://app.box.com/s/vysdh2u78yih3c8leetgq82il954a3g3 + // some gambling ad + // pptx + // https://app.box.com/s/nnlplkmjhimau404qohh9my10pwmo8es + // a list of books(?) + // txt + // https://ucla.app.box.com/s/mv32q624ojihohzh8d0mhhj0b3xluzbz + // "COVID-19 Pivot Plan Decision Matrix" + // If you load the proprietary scripts on this page, you'll see that there is no download button + // TODO: find a public folder link (the private links I have seem to work) + // TODO: find a (preferably public) link with a folder inside a folder, as these may need to be handled differently + +/* Extract data from a script that sets multiple variables. */ // from here: https://api-demo.hachette-hydrilla.org/content/sgoogle_sheets_download/google_sheets_download.js + +let prefetchedData = null; // This variable isn't actually used. +for (const script of document.scripts) { + const match = /Box.prefetchedData = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.prefetchedData = " in the script files and then grabs the json text after that. + if (!match) + continue; + prefetchedData = JSON.parse(match[1]); +} + +let config = null; +for (const script of document.scripts) { + const match = /Box.config = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.config = " in the script files and then grabs the json text after that. + if (!match) + continue; + config = JSON.parse(match[1]); +} + +let postStreamData = null; +for (const script of document.scripts) { + const match = /Box.postStreamData = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.postStreamData = " in the script files and then grabs the json text after that. + if (!match) + continue; + postStreamData = JSON.parse(match[1]); +} + +// get domain from URL +const domain = document.location.href.split("/")[2]; + +/* Replace current page contents. */ +const replacement_html = `\ + + + + + + +

loading...

+

error occured :(

+

+ + + +`; + +/* + * We could instead use document.write(), but browser's debugging tools would + * not see the new page contents. + */ +const parser = new DOMParser(); +const alt_doc = parser.parseFromString(replacement_html, "text/html"); +document.documentElement.replaceWith(alt_doc.documentElement); + +const nodes = {}; +document.querySelectorAll('[id]').forEach(node => nodes[node.id] = node); + +function show_error() { + nodes.loading.classList.add("hide"); + nodes.error.classList.remove("hide"); +} + +function show_title(text) { + nodes.title.innerText = text; + nodes.loading.classList.add("hide"); + nodes.title.classList.remove("hide"); +} + +async function hack_file() { + nodes.loading.classList.remove("hide"); + + const tokens_url = "/app-api/enduserapp/elements/tokens"; + const file_nr = postStreamData["/app-api/enduserapp/shared-item"].itemID; + const file_id = `file_${file_nr}`; + const shared_name = postStreamData["/app-api/enduserapp/shared-item"].sharedName; + + /* + * We need to perform a POST to obtain a token that will be used later to + * authenticate against Box's API endpoint. + */ + const tokens_response = await fetch(tokens_url, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "Request-Token": config.requestToken, + "X-Box-Client-Name": "enduserapp", + "X-Box-Client-Version": "20.712.2", + "X-Box-EndUser-API": `sharedName=${shared_name}`, + "X-Request-Token": config.requestToken + }, + body: JSON.stringify({"fileIDs": [file_id]}) + }); + console.log("tokens_response", tokens_response); + + const access_token = (await tokens_response.json())[file_id].read; + console.log("access_token", access_token); + + const fields = [ + "permissions", "shared_link", "sha1", "file_version", "name", "size", + "extension", "representations", "watermark_info", + "authenticated_download_url", "is_download_available", + "content_created_at", "content_modified_at", "created_at", "created_by", + "modified_at", "modified_by", "owned_by", "description", + "metadata.global.boxSkillsCards", "expires_at", "version_limit", + "version_number", "is_externally_owned", "restored_from", + "uploader_display_name" + ]; + + const file_info_url = + `https://api.box.com/2.0/files/${file_nr}?fields=${fields.join()}`; + + /* + * We need to perform a GET to obtain file metadata. The fields we curently + * make use of are "authenticated_download_url" and "file_version", but in + * the request we also include names of other fields that the original Box + * client would include. The metadata is then dumped as JSON on the page, so + * the user, if curious, can look at it. + */ + const file_info_response = await fetch(file_info_url, { + headers: { + "Accept": "application/json", + "Authorization": `Bearer ${access_token}`, + "BoxApi": `shared_link=${document.URL}`, + "X-Box-Client-Name": "ContentPreview", + "X-Rep-Hints": "[3d][pdf][text][mp3][json][jpg?dimensions=1024x1024&paged=false][jpg?dimensions=2048x2048,png?dimensions=2048x2048][dash,mp4][filmstrip]" + }, + }); + console.log("file_info_response", file_info_response); + + const file_info = await file_info_response.json(); + console.log("file_info", file_info); + + const params = new URLSearchParams(); + params.set("preview", true); + params.set("version", file_info.file_version.id); + params.set("access_token", access_token); + params.set("shared_link", document.URL); + params.set("box_client_name", "box-content-preview"); + params.set("box_client_version", "2.82.0"); + params.set("encoding", "gzip"); + + /* We use file metadata from earlier requests to construct the link. */ + const download_url = + `${file_info.authenticated_download_url}?${params.toString()}`; + console.log("download_url", download_url); + + show_title(file_info.name); + + nodes.download_button.href = download_url; + if (!file_info.permissions.can_download) + nodes.download_button.classList.add("unofficial"); + nodes.file_info.innerText = JSON.stringify(file_info); + nodes.single_file_section.classList.remove("hide"); +} + +if (postStreamData["/app-api/enduserapp/shared-item"].itemType == "file") { + /* + * We call hack_file and in case it asynchronously throws an exception, we + * make an error message appear. + */ + hack_file().then(() => {}, show_error); +} else if (postStreamData["/app-api/enduserapp/shared-item"].itemType == "folder") { + show_title(postStreamData["/app-api/enduserapp/shared-folder"].currentFolderName); + + // TODO: implement a download folder button (included in proprietary app) + /* + The original download folder button sends a GET request that gets 2 URLs + in the response. 1 of those URLs downloads the file, and a POST request + is sent after (or maybe while in some cases?) a file is downloaded, to + let the server know how much is downloaded. + */ + // for each item in the folder, show a button with a link to download it + postStreamData["/app-api/enduserapp/shared-folder"].items.forEach(function(item) { + console.log("item", item); + + const file_direct_url = "https://"+domain+"/index.php?rm=box_download_shared_file&shared_name="+postStreamData["/app-api/enduserapp/shared-item"].sharedName+"&file_id="+item.typedID; + + const folderButton = nodes.download_button.cloneNode(false); + folderButton.removeAttribute("id"); + + if (item.type == "file") { + folderButton.href = file_direct_url; + folderButton.innerText = item.name; // show the name of the file + } else if (item.type == "folder") { + folderButton.innerText = "[folders inside folders not yet supported]"; + } else { + folderButton.innerText = "[this item type is not supported]"; + } + + document.body.appendChild(folderButton); + }); +} else { + console.log('expected "folder" or "file" as the item type (postStreamData["/app-api/enduserapp/shared-item"].itemType) but got ' + postStreamData["/app-api/enduserapp/shared-item"].itemType + ' instead; this item type is not implemented'); + show_error(); +} diff --git a/src/fedoraaccounts.js b/src/fedoraaccounts.js new file mode 100644 index 0000000..27e76d9 --- /dev/null +++ b/src/fedoraaccounts.js @@ -0,0 +1,37 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * Fix registration of a Fedora account + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + */ + +/* Haketilo fix to use with https://accounts.fedoraproject.org */ + +var by_id = id => document.getElementById(id); + +var login_register_tabs = ['login', 'register'].map(by_id); +var login_register_buttons = login_register_tabs.map(e => by_id(`${e.id}-tab`)) + +function switch_tab(i) +{ + login_register_buttons[i].classList.add('active'); + login_register_buttons[1 - i].classList.remove('active'); + + login_register_tabs[i].classList.add('show', 'active'); + login_register_tabs[1 - i].classList.remove('show', 'active'); +} + +for (const i of [0, 1]) { + login_register_buttons[i].addEventListener('click', () => switch_tab(i)); + login_register_buttons[i].href = '#' +} diff --git a/src/google_drive_files.js b/src/google_drive_files.js new file mode 100644 index 0000000..f93194b --- /dev/null +++ b/src/google_drive_files.js @@ -0,0 +1,168 @@ +/** + * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions + * + * Make files on drive.google.com downloadable without nonfree js + * + * Copyright (C) 2021 Wojtek Kosior + * + * 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 . + * + * 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://drive.google.com/file/d/** */ + +const og = {}; + +for (const node of document.head.childNodes) { + if (node.tagName === "STYLE") { + document.head.removeChild(node); + continue; + } + + if (node.tagName !== "META") + continue; + + const match = /^og:(.+)/.exec(node.getAttribute("property")); + if (!match) + continue; + + og[match[1]] = node.getAttribute("content"); +} + +const match = new RegExp("/file/d/([^/]+)").exec(document.URL); +const file_id = match && match[1]; + +/* Extract file's mime type from script. */ + +let mime_type; + +for (const script of document.scripts) { + const match = /itemJson\s*:\s*(\[.*\])\s*}\s*;$/.exec(script.textContent); + if (!match) + continue; + + let data; + try { + data = JSON.parse(match[1]); + } catch(e) { + console.log(e); + continue; + } + + if (/^[^\/]+\/[^\/]+$/.test(data[11])) + mime_type = data[11]; +} + +/* If file is folder, redirect to its page. */ + +const redirect = file_id && + /^application\/vnd.google-apps.(folder|shortcut)$/.test(mime_type); +if (redirect) + window.location.href = `https://drive.google.com/drive/folders/${file_id}`; + +const download_link = + file_id && `https://drive.google.com/uc?export=download&id=${file_id}`; + +const body = document.createElement("body"); +const name_div = document.createElement("div"); +const download_div = document.createElement("div"); +const download_button = document.createElement("a"); +const type_div = document.createElement("div"); +const type_span = document.createElement("span"); +const show_image_div = document.createElement("div"); +const show_image_button = document.createElement("button"); +const image = document.createElement("img"); + + +let button_text = "download"; + +if (!og.title) + button_text += " file"; +else + name_div.textContent = og.title; + +name_div.setAttribute("style", "font-weight: bold; display:inline-block;"); + + +if (download_link) + download_button.setAttribute("href", download_link); +else + button_text += " (unavailable)"; + +download_button.textContent = button_text; +download_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: black; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888;"); + + +download_div.appendChild(download_button); +download_div.setAttribute("style", "padding: 10px; display: inline-block; margin: 0 0 10px;"); + + +type_span.textContent = "type: "; +type_span.setAttribute("style", "font-weight: bold; display:inline-block; white-space: pre;"); + +type_div.setAttribute("style", "margin: 0 0 10px;"); +type_div.appendChild(type_span); +if (mime_type || og.type) + type_div.append(mime_type || og.type); + + +function show_image() +{ + const image = document.createElement("img"); + image.setAttribute("src", og.image); + image.setAttribute("width", og["image:width"]); + image.setAttribute("height", og["image:height"]); + image.setAttribute("style", "border: 1px solid #eee;"); + + document.body.replaceChild(image, show_image_div); +} + +show_image_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: black; background-color: lightgreen; box-shadow: -4px 8px 8px #888; font-family: inherit; font-size: inherit; border: none;"); +show_image_button.textContent = "show image"; +show_image_button.addEventListener("click", show_image); +show_image_div.appendChild(show_image_button); + + +body.setAttribute("style", "margin: 20px;"); +if (og.title) + body.appendChild(name_div); +body.appendChild(download_div); +if (og.type) + body.appendChild(type_div); +if (og.image && og["image:width"] && og["image:height"]) + body.appendChild(show_image_div); + +if (!redirect) + document.documentElement.replaceChild(body, document.body); diff --git a/src/google_drive_folders.js b/src/google_drive_folders.js new file mode 100644 index 0000000..7d37032 --- /dev/null +++ b/src/google_drive_folders.js @@ -0,0 +1,384 @@ +/** + * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions + * + * Make folders on drive.google.com browsable without nonfree js + * + * Copyright (C) 2021 Wojtek Kosior + * + * 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 . + * + * 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://drive.google.com/drive/folders/*** */ + +/* Define how to handle various mime types used by Google. */ + +const known_mimes = { + "application/vnd.google-apps.folder": { + links: id => ({ + view: `https://drive.google.com/drive/folders/${id}` + }), + type: "folder", + is_folder: true + }, + "application/vnd.google-apps.shortcut": { + links: id => ({ + view: `https://drive.google.com/drive/folders/${id}` + }), + type: "shortcut", + is_folder: true + }, + "application/vnd.google-apps.document": { + links: id => ({ + view: `https://docs.google.com/document/d/${id}`, + download: `https://docs.google.com/document/d/${id}/export?format=odt`, + }), + type: "Google text document", + new_mime: "application/vnd.oasis.opendocument.text" + }, + "application/vnd.google-apps.spreadsheet": { + links: id => ({ + view: `https://docs.google.com/spreadsheets/d/${id}`, + download: `https://docs.google.com/spreadsheets/d/${id}/export?format=ods` + }), + type: "Google spreadsheet", + new_mime: "application/vnd.oasis.opendocument.spreadsheet" + }, + "application/vnd.google-apps.presentation": { + links: id => ({ + view: `https://docs.google.com/presentation/d/${id}`, + download: `https://docs.google.com/presentation/d/${id}/export/pptx` + }), + type: "Google presentation", + new_mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation" + }, + "application/vnd.google-apps.drawing": { + links: id => ({ + view: `https://docs.google.com/drawings/d/${id}`, + download: `https://docs.google.com/drawings/d/${id}/export/jpeg` + }), + type: "Google drawing", + new_mime: "image/jpeg" + }, + "application/vnd.google-apps.script": { + links: id => ({ + download: `https://script.google.com/feeds/download/export?format=json&id=${id}` + }), + type: "Google script", + new_mime: "application/vnd.google-apps.script+json" + }, + "application/vnd.google-apps.jam": { + links: id => ({ + download: `https://jamboard.google.com/export?id=${id}` + }), + type: "Google jam", + new_mime: "application/pdf" + } +}; + +/* + * Human-friendly names defined here will be displayed to the user instead of + * raw mime types. Please add more here. + */ +let mime_display_overrides = { + "image/jpeg": "JPEG image", + "application/octet-stream": "binary data", + "application/pdf": "PDF document", + "application/rar": "RAR archive", + "application/zip": "ZIP archive" +}; + +let default_link_producer = id => ({ + view: `https://drive.google.com/file/d/${id}`, + download: `https://drive.google.com/uc?export=download&id=${id}` +}); + +for (const [mime, display_name] of Object.entries(mime_display_overrides)) { + known_mimes[mime] = { + links: default_link_producer, + type: display_name, + new_mime: mime + } +} + +delete mime_display_overrides; + +function get_mime_info(mime) { + return known_mimes[mime] || { + links: default_link_producer, + type: mime || "", + new_mime: mime || "application/octet-stream" + } +} + +/* Prepare folder contents data as well as data regarding the folder itself. */ + +const content = new Map(); + +function add_content_item(item) +{ + const old_item = content.get(item.id) || {}; + Object.assign(old_item, item); + content.set(item.id, old_item); +} + +const this_folder = {}; + +function replace_string_escape(match, group) +{ + return String.fromCharCode(`0x${group}`); +} + +function try_parse(data, replace_x_escapes) +{ + if (!data) + return null; + + if (replace_x_escapes) + data = data.replaceAll(/\\x([0-9a-f]{2})/g, replace_string_escape); + + try { + return JSON.parse(data); + } catch (e) { + console.log(e); + } + + return null; +} + +function process_file_data(file_data, callback) +{ + if (!Array.isArray(file_data) || !typeof file_data[0] === "string") { + console.log("cannot process the following file data object:", + file_data); + return; + } + + const result = {id: file_data[0], folders: []}; + + if (Array.isArray(file_data[1])) { + for (const item of file_data[1]) { + if (typeof item === "string" && item !== this_folder.id) + result.folders.push(item); + } + } + if (typeof file_data[2] === "string") + result.filename = file_data[2]; + if (typeof file_data[3] === "string" && file_data[3].search("/") >= 0) + result.mime = file_data[3]; + if (typeof file_data[9] === "number") + result.date1 = new Date(file_data[9]); + if (typeof file_data[10] === "number") + result.date2 = new Date(file_data[10]); + + callback(result); +} + +/* + * By searching for scripts with calls to AF_initDataCallback we get about 7 + * matches. All arguments to this function seem to be arrays parseable as JSON, + * but their contents are very different and only two of those ~7 arrays + * actually hold some data useful to us. Here we try to filter out the other + * cases and then extract the useful data. + */ +function process_af_init_data(data) +{ + if (!Array.isArray(data) || !/^driveweb/.test(data[0]) || + !Array.isArray(data[1])) + return; + + /* First useful "kind" of object we can encounter is this folder's data. */ + if (typeof data[1][0] === "string") { + process_file_data(data[1], item => Object.assign(this_folder, item)); + return; + } + + /* + * Second "kind" of object holds data about all items in the folder. + * Folders and Files are stored in separate lists (doesn't matter to us, + * since we distinguish them based on mime type anyway). + */ + for (const data_sub of data[1]) { + if (!Array.isArray(data_sub) || !/^driveweb/.test(data_sub[0]) || + !Array.isArray(data_sub[1])) + continue; + for (const item of data_sub[1]) + process_file_data(item, add_content_item); + } +} + +/* + * Folder items data actually exists in both of the 2 kinds of scripts we search + * for. In case of both of the regexes below we call `process_file_data' in the + * end. As a result we process the same file multiple time as it appears in 2 + * scripts. This is, however, a good thing, because it makes a change less + * likely to break our fix. + */ + +const ivd_data_regex = /\s*window\s*\['_DRIVE_ivd'\]\s*=\s*'(\\x5b[^']+)'(.*)$/; +const af_init_data_regex = /AF_initDataCallback\s*\(.+data\s*:\s*(\[.*\])[^\]]+$/; + +for (const script of document.scripts) { + const ivd_data_match = ivd_data_regex.exec(script.textContent); + if (ivd_data_match) { + const ivd_data = try_parse(ivd_data_match[1], true); + if (ivd_data && Array.isArray(ivd_data) && Array.isArray(ivd_data[0])) { + for (const item of ivd_data[0]) + process_file_data(item, add_content_item); + } + } + + const af_init_data_match = af_init_data_regex.exec(script.textContent); + if (af_init_data_match) { + const af_init_data = try_parse(af_init_data_match[1], false); + if (af_init_data) + process_af_init_data(af_init_data); + } +} + +/* Construct our own user interface. */ + +const body = document.createElement("body"); +const folders = document.createElement("div"); +const files = document.createElement("div"); +const folders_heading = document.createElement("h2"); +const files_heading = document.createElement("h2"); + +folders_heading.textContent = "Folders"; +files_heading.textContent = "Files"; + +let has_folders = false; +let has_files = false; + +folders.appendChild(folders_heading); +files.appendChild(files_heading); + +body.setAttribute("style", "width: 100vw; height: 100vh; overflow: scroll; color: #555; margin: 15px; -webkit-user-select: initial;"); + +const drive_folder_regex = /application\/vnd.google-apps.(folder|shortcut)/; + +function add_item_to_view(item_data) +{ + const item_div = document.createElement("div"); + const item_heading = document.createElement("h4"); + + item_div.setAttribute("style", "border: 2px solid #999; border-radius: 8px; padding: 10px; display: inline-block; margin: 2px;"); + + let item_heading_style = "margin: 8px 4px;"; + + if (item_data.filename) { + item_heading.textContent = item_data.filename; + } else { + item_heading.textContent = "(no name)"; + item_heading_style += " font-style:italic;"; + } + item_heading.setAttribute("style", item_heading_style); + item_div.appendChild(item_heading); + + const mime_info = get_mime_info(item_data.mime); + + if (mime_info.type) { + const type_div = document.createElement("div"); + type_div.setAttribute("style", "margin-bottom: 5px;"); + type_div.textContent = mime_info.type; + + item_div.appendChild(type_div); + } + + if (mime_info.is_folder) + has_folders = true; + else + has_files = true; + + const links = {}; + if (item_data.id) + Object.assign(links, mime_info.links(item_data.id)); + + if (links.view) { + const view_button = document.createElement("a"); + view_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 5px;"); + view_button.textContent = "view"; + view_button.href = links.view; + + item_div.appendChild(view_button); + } + + if (links.download) { + const download_button = document.createElement("a"); + download_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 5px;"); + download_button.textContent = "download"; + download_button.href = links.download; + download_button.type = mime_info.new_mime; + + item_div.appendChild(download_button); + } + + (mime_info.is_folder ? folders : files).appendChild(item_div); +} + +for (const item of content.values()) + add_item_to_view(item); + +if (this_folder.filename) { + const heading = document.createElement("h1"); + heading.textContent = this_folder.filename; + body.appendChild(heading); +} +if (this_folder.folders && this_folder.folders.length > 0) { + for (const parent_id of this_folder.folders) { + const up_button = document.createElement("a"); + let text = "go up"; + + up_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 5px;"); + up_button.href = `https://drive.google.com/drive/folders/${parent_id}`; + if (this_folder.folders.length > 1) + text = `${text} (${parent_id})`; + up_button.textContent = text; + + body.appendChild(up_button); + } +} +if (has_folders) + body.appendChild(folders); +if (has_files) + body.appendChild(files); +if (!(has_files || has_folders)) { + const no_files_message = document.createElement("h3"); + no_files_message.textContent = "No files found."; + body.appendChild(no_files_message); +} + +if (document.body.firstChild.id !== "af-error-container") + document.documentElement.replaceChild(body, document.body); diff --git a/src/google_forms.js b/src/google_forms.js new file mode 100644 index 0000000..5d8826d --- /dev/null +++ b/src/google_forms.js @@ -0,0 +1,66 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * (Incomplete) Fix for Google Forms + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var form = document.forms[0]; +for (let div of form.querySelectorAll('div[data-params]')) { + var data = JSON.parse('[' + div.dataset.params.substring(4)); + var name = 'entry.' + data[0][4][0][0]; + var input = div.querySelector('input'); + + if (input.name === name + '_sentinel') { // Radio + for (input of div.querySelectorAll('.appsMaterialWizToggleRadiogroupElContainer')) { + div = document.createElement('input'); + div.type = 'radio'; + div.name = name; + div.value = input.nextElementSibling.innerText.trim(); + input.parentNode.replaceChild(div, input); + } + } else { + input.removeAttribute('disabled'); + input.name = name; + } +} + +for (div of document.querySelectorAll('.quantumWizTextinputPaperinputPlaceholder')) + div.remove(); + +function goToNext() +{ + var next = document.createElement('input'); + next.type = 'hidden'; + next.name = 'continue'; + next.value = '1'; + form.appendChild(next); + form.submit(); +} + +for (div of document.querySelectorAll('.freebirdFormviewerViewNavigationNoSubmitButton')) { + input = document.createElement('button'); + + data = div.innerText.trim(); + input.innerText = data; + if (data.toLowerCase() === 'next') + input.onclick = goToNext; + else if (data.toLowerCase() === 'submit') + input.type = 'submit'; + div.parentNode.replaceChild(input, div); +} + +// TODO: back, instate previous entries diff --git a/src/google_sheets_download.js b/src/google_sheets_download.js new file mode 100644 index 0000000..24149ca --- /dev/null +++ b/src/google_sheets_download.js @@ -0,0 +1,210 @@ +/** + * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions + * + * Make spreadsheets on drive.google.com browsable without nonfree js + * + * Copyright (C) 2021 Wojtek Kosior + * + * 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 . + * + * 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://docs.google.com/spreadsheets/d/** */ + +/* Make the view scrollable. */ + +document.body.setAttribute("style", + "width: 100vw; height: 100vh; overflow: scroll;" + + (document.body.getAttribute("style") || "")); + +let container = document.querySelectorAll(".waffle")[0]; +let main_gid = null; + +while (container) { + container = container.parentElement; + console.log(container); + if (container === document.body || !container) + break; + + const match = /([0-9]+)-grid-container/.exec(container.id); + if (match) + main_gid = match[1]; + + container.setAttribute("style", + "width: fit-content; width: -moz-fit-content;"); +} + + +/* Remove editor toolbars and bottom bar - these don't work anyway. */ + +const docs_chrome = document.getElementById("docs-chrome"); +if (docs_chrome) + docs_chrome.remove() +const grid_bottom_bar = document.getElementById("grid-bottom-bar"); +if (grid_bottom_bar) + grid_bottom_bar.remove() + + +/* Remove no Javascript warning. */ + +for (const no_js_warning of document.querySelectorAll("noscript")) + no_js_warning.remove(); + + +/* Get opengraph data. */ + +const og = {}; + +for (const node of document.head.childNodes) { + if (node.tagName === "STYLE") { + document.head.removeChild(node); + continue; + } + + if (node.tagName !== "META") + continue; + + const match = /^og:(.+)/.exec(node.getAttribute("property")); + if (!match) + continue; + + og[match[1]] = node.getAttribute("content"); +} + + +/* Construct download link. */ + +let download_link = null; + +const match = new RegExp("/spreadsheets/d/([^/]+)").exec(document.URL); +if (match) + download_link = `https://docs.google.com/spreadsheets/d/${match[1]}/export`; + + +/* Add title bar with sheet name and download button. */ + +const title_bar = document.createElement("div"); +const title_heading = document.createElement("h1"); +const title_text = document.createElement("span"); +const main_download_button = document.createElement("a"); + +main_download_button.textContent = "download"; +main_download_button.setAttribute("style", "border-radius: 10px; padding: 20px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block;"); + +if (og.title) { + title_text.textContent = og.title; + title_heading.appendChild(title_text); +} + +title_text.setAttribute("style", "margin-right: 10px;"); + +if (download_link) { + main_download_button.setAttribute("href", download_link); + title_heading.appendChild(main_download_button); +} + +title_bar.setAttribute("style", "padding: 0 20px; color: #555;"); + +title_bar.appendChild(title_heading); + +document.body.insertBefore(title_bar, document.body.firstElementChild); + + +/* Extract sheet data from a script that sets the `bootstrapData' variable. */ + +let data = null; +for (const script of document.scripts) { + const match = /bootstrapData = ({([^;]|[^}];)+})/.exec(script.textContent); + if (!match) + continue; + data = JSON.parse(match[1]); +} + +/* + * Add download buttons for individual sheets belonging to this spreadsheet. + * Data schema has been observed by looking at various spreadsheets. + */ + +function add_sheet_download(data) +{ + if (!Array.isArray(data)) + return; + + const gid = data[2]; + if (!["string", "number"].includes(typeof gid)) + return; + + const sheet_download_link = `${download_link}?gid=${gid}`; + const sheet_download_button = document.createElement("a"); + + sheet_download_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 0 5px 5px 0;"); + sheet_download_button.setAttribute("href", sheet_download_link); + + let sheet_name = null; + if (Array.isArray(data[3]) && + data[3][0] && typeof data[3][0] === "object" + && Array.isArray(data[3][0][1]) && + Array.isArray(data[3][0][1][0]) && + typeof data[3][0][1][0][2] === "string") { + + const sheet_name = data[3][0][1][0][2]; + sheet_download_button.textContent = sheet_name; + if (gid == main_gid) + title_text.textContent = `${title_text.textContent} - ${sheet_name}`; + } else { + sheet_download_button.textContent = ``; + } + + title_bar.appendChild(sheet_download_button); +} + +if (download_link) { + for (const entry of data.changes.topsnapshot) { + if (!Array.isArray(entry) || entry[0] !== 21350203 || + typeof entry[1] !== "string") + continue; + + let entry_data = null; + + try { + entry_data = JSON.parse(entry[1]); + } catch (e) { + console.log(e); + continue; + } + + add_sheet_download(entry_data); + } +} diff --git a/src/internet_archive_video.js b/src/internet_archive_video.js new file mode 100644 index 0000000..f7d7198 --- /dev/null +++ b/src/internet_archive_video.js @@ -0,0 +1,91 @@ +/** + * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions + * + * Make videos on archive.org playable inline without relying on site-served js + * + * Copyright (C) 2021 Wojtek Kosior + * + * 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 . + * + * 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. + */ + +const theatre_ia = document.getElementById("theatre-ia"); + +let srcs = []; + +let ogv_src = null; +let webm_src = null; +let mp4_src = null; + +function process_link(link) +{ + ogv_src = ogv_src || (/\.ogv$/.test(link) && link); + webm_src = webm_src || (/\.webm$/.test(link) && link); + mp4_src = mp4_src || (/\.mp4$/.test(link) && link); +} + +if (theatre_ia) { + for (const a of + document.querySelectorAll(".item-download-options a.download-pill")) + process_link(a.href); + + for (const link of document.querySelectorAll("link[itemprop=contentUrl]")) + process_link(link.href); + + srcs = [ + {src: ogv_src, type: "video/ogg"}, + {src: webm_src, type: "video/webm"}, + {src: mp4_src, type: "video/mp4"} + ].filter(src => src.src); +} + +if (srcs.length > 0) { + const video = document.createElement("video"); + + for (const src of srcs) { + const source = document.createElement("source"); + Object.assign(source, src); + video.appendChild(source); + } + + video.setAttribute("width", "100%"); + video.setAttribute("height", "auto"); + video.setAttribute("controls", ""); + + for (const child of theatre_ia.children) + child.remove(); + + theatre_ia.appendChild(video); +} 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 + * + * 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 . + * + * 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 + * 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(); +} diff --git a/src/opencores.js b/src/opencores.js new file mode 100644 index 0000000..d5b6376 --- /dev/null +++ b/src/opencores.js @@ -0,0 +1,46 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * View OpenCores projects list without nonfree js + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + */ + +/* use with https://opencores.org/projects */ + +let data = JSON.parse(document.getElementById("__NEXT_DATA__").textContent); +let sections = {}; +for (let h1 of document.getElementsByClassName("cMJCrc")) { + let ul = document.createElement("ul"); + if (h1.nextElementSibling !== null) + h1.parentNode.insertBefore(ul, h1.nextElementSibling); + else + h1.parentNode.appendChild(ul); + + sections[h1.children[1].firstChild.textContent] = ul; +} + +for (let prop of data.props.pageProps.list) { + let ul = sections[prop.category]; + if (ul === undefined) { + console.log(`unknown category "${prop.category}" for project "${prop.title}"`); + continue; + } + + let li = document.createElement("li"); + let a = document.createElement("a"); + a.setAttribute("href", "/projects/" + prop.slug); + a.textContent = prop.title; + + li.appendChild(a); + ul.appendChild(li); +} diff --git a/src/pcspecialist_cookie_notice.js b/src/pcspecialist_cookie_notice.js new file mode 100644 index 0000000..bd61bf7 --- /dev/null +++ b/src/pcspecialist_cookie_notice.js @@ -0,0 +1,21 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const jbc = document.querySelector('.cc-policy'); + +jbc.querySelector('.cc-buttons').onclick = () => jbc.parentNode.removeChild(jbc); diff --git a/src/pcspecialist_display_prices.js b/src/pcspecialist_display_prices.js new file mode 100644 index 0000000..c28cb47 --- /dev/null +++ b/src/pcspecialist_display_prices.js @@ -0,0 +1,57 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const formId = document.querySelector('input[name="page_id"]').value; +const form = document.querySelector('form[name="specbuilder"]'); +const noVat = document.getElementById('running_total_ex'); +const incVat = document.getElementById('running_total_inc'); + +function updatePrice() { + const xhr = new XMLHttpRequest(); + + var names = [], values = []; + for (var inputElement of form.querySelectorAll('select, input[type="radio"]')) { + if (inputElement.name && (inputElement.checked || inputElement.tagName === 'SELECT')) { + names.push(inputElement.name); + values.push(inputElement.value); + } + } + + const url = 'https://www.pcspecialist.co.uk/ajax/running_total.php?categories=' + names.join('%2C') + + '%2C&products=' + values.join('%2C') + '%2C&q=' + form.querySelector('input[name="q"]').value + '&form_id=' + formId; + + xhr.onreadystatechange = priceUpdated; + xhr.open('GET', url, true); + xhr.send(); +} + +function priceUpdated() { + if (this.readyState === 4) { + if (this.status === 200) { + const parts = this.responseText.split("'"); + noVat.innerText = parts[parts.length - 6]; + incVat.innerText = parts[parts.length - 2]; + } + else alert('Failed to get data: HTTP status code ' + this.status); + } +} + +const button = document.createElement('button'); +button.innerText = 'Update Prices'; +button.onclick = updatePrice; +document.querySelector('.price-holder.price-finance-holder').append(button); diff --git a/src/phoronix_benchmarks.js b/src/phoronix_benchmarks.js new file mode 100644 index 0000000..a332cc0 --- /dev/null +++ b/src/phoronix_benchmarks.js @@ -0,0 +1,38 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * Fix benchmarks in phoronix.com articles + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + */ + +/* Use with https://www.phoronix.com/*** */ + +/* + * Phoronix normally includes scripts that call document.write() to inject + * tags. The most obvious way o code a fix would then be do download and + * parse the contents of those scripts. CORS, however, doesn't allow this. + * Instead, we notice that the openbenchmarking embed script url is related to + * the actual image url we need, so we can create 's from it straight away. + */ +for (const script of document.scripts) { + const match = /openbenchmarking.org\/+(embed.php\?.*)p=0$/.exec(script.src); + if (!match) continue; + + const img = document.createElement("img"); + img.src = `https://openbenchmarking.org/${match[1]}p=2`; + img.setAttribute("type", "image/svg+xml"); + img.setAttribute("width", "100%"); + img.setAttribute("height", "auto"); + + script.parentElement.insertBefore(img, script); +} diff --git a/src/royal_geographical_society.js b/src/royal_geographical_society.js new file mode 100644 index 0000000..0e983b0 --- /dev/null +++ b/src/royal_geographical_society.js @@ -0,0 +1,20 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +document.documentElement.style.visibility = 'visible'; +document.documentElement.style.opacity = '100'; diff --git a/src/santander_centrum24.js b/src/santander_centrum24.js new file mode 100644 index 0000000..793888f --- /dev/null +++ b/src/santander_centrum24.js @@ -0,0 +1,25 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * Fix SMS code submission on https://acsv.centrum24.pl/ACS/servlet/ACSAuthoriz + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + */ + +const submit_button = document.getElementById("submit"); +submit_button.classList.remove("disabled"); +submit_button.removeAttribute("disabled"); + +console.log(document.querySelectorAll("noscript")); + +for (const noscript_element of document.querySelectorAll("noscript")) + noscript_element.remove(); diff --git a/src/stack_exchange_cookienotice.js b/src/stack_exchange_cookienotice.js new file mode 100644 index 0000000..37a42ac --- /dev/null +++ b/src/stack_exchange_cookienotice.js @@ -0,0 +1,20 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const jcb = document.querySelector('.js-consent-banner'); +document.querySelector('.js-accept-cookies').onclick = e => jcb.parentNode.removeChild(jcb); diff --git a/src/sumofus.js b/src/sumofus.js new file mode 100644 index 0000000..7d0b5a6 --- /dev/null +++ b/src/sumofus.js @@ -0,0 +1,56 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function submitFormItem() { + var name, val, queryString = '', xhr = new content.XMLHttpRequest(); + for (var formItem of this.querySelectorAll('select, input:not([type="radio"]):not([type="checkbox"])' + + ':not([type="submit"]):not([type="reset"])')) { + queryString += (queryString && '&') + formItem.name + '=' + encodeURIComponent(formItem.value); + } + + xhr.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) location.href = JSON.parse(this.responseText).follow_up_url; + else if (this.status === 422) { + var failMessage = [], response = JSON.parse(this.responseText); + for (field in response.errors) for (error of response.errors[field]) { + failMessage.push('Field "' + field + '" ' + error); + } + alert(failMessage.join('\n')); + } + else alert('Submission failed: response code ' + this.status); + } + } + + xhr.open('POST', this.action, true); // Manually add the domain, as it's not properly handled in extensions + xhr.setRequestHeader('X-CSRF-Token', csrf); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.send(queryString); + return false; +} + +// Apply CSS as necessary +if (notice = document.querySelector('#petition-bar-main > span')) notice.style.display = 'none'; // Hide the totally mistaken (even without this extension) anti-anti-JS warning +document.querySelector('.script-dependent').style.display = 'block'; +document.querySelector('.button-wrapper').style.position = 'static'; // Stop the "submit" button obscuring the form + + + +csrf = document.querySelector('meta[name="csrf-token"]').content +for (var button of document.querySelectorAll('button[type="submit"].button.action-form__submit-button')) button.form.onsubmit = submitFormItem; diff --git a/src/vaticannews_videos.js b/src/vaticannews_videos.js new file mode 100644 index 0000000..0967ed5 --- /dev/null +++ b/src/vaticannews_videos.js @@ -0,0 +1,24 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * Watch vaticannews.va embedded YouTube videos on yewtu.be instead + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + * Available under the terms of Creative Commons Zero. + */ + +/* Use with https://www.vaticannews.va/*** */ + +for (const iframe of document.querySelectorAll("iframe[data-src]")) { + const youtube_url = iframe.getAttribute("data-src"); + iframe.setAttribute("src", make_yewtube_url(youtube_url)); +} diff --git a/src/worldcat.js b/src/worldcat.js new file mode 100644 index 0000000..0a4b531 --- /dev/null +++ b/src/worldcat.js @@ -0,0 +1,67 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright © 2021 jahoti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var pathParts = location.pathname.split('/'), itemRef = pathParts[pathParts.length - 1]; + +// Generate a function which, when invoked, loads the catalog holdings starting at i (one-indexed) focused on loc +function generateGoTo(i, set_loc) { + return function () { + ; // If this is a new search, "set_loc" won't be set; set it + var xhr = new content.XMLHttpRequest(), loc = set_loc || encodeURIComponent(locInput.value); + xhr.onreadystatechange = function () { + if (this.readyState === 4) { + if (this.status === 200) { + retrieved.innerHTML = this.responseText; + + var i, node = document.getElementById('libslocator'); + node.parentNode.removeChild(node); + for (node of retrieved.querySelectorAll('a[href^="javascript:findLibs(\'\', "]')) { + i = parseInt(node.href.split(',', 2)[1]); + node.onclick = generateGoTo(i, loc); + } + } + else alert('Search failed: response code ' + this.status); + } + } + + xhr.open('GET', 'https://www.worldcat.org/wcpa/servlet/org.oclc.lac.ui.ajax.ServiceServlet?wcoclcnum=' + itemRef + '&start_holding=' + + i + '&serviceCommand=holdingsdata&loc=' + loc, true); + xhr.send(); + return false; // Make sure the browser doesn't try to submit any holding form + }; +} + + +var retriever = document.querySelector('.retrieving'), retrieved = document.getElementById('donelocator'); + +var locForm = document.createElement('form'), locLabel = document.createElement('label'), locInput = document.createElement('input'), + locSubmit = document.createElement('input'); + +locForm.appendChild(locLabel); +locForm.appendChild(locInput); +locForm.appendChild(locSubmit); + +locInput.name = locLabel.htmlFor = 'cat_location'; +locInput.type = 'text'; +locInput.required = 'yes'; +locLabel.innerText = 'Find copies closest to: '; +locSubmit.value = 'Go'; +locSubmit.type = 'submit'; +locForm.onsubmit = generateGoTo(1); + +retriever.parentNode.replaceChild(locForm, retriever); diff --git a/src/yewtube_urls.js b/src/yewtube_urls.js new file mode 100644 index 0000000..9eca21f --- /dev/null +++ b/src/yewtube_urls.js @@ -0,0 +1,58 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * Library to convert youtube.com URLs to yewtu.be URLs. + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + */ + +function decode_query_options(url) +{ + const query_options = {}; + const match = /^[^?]*\?(.*)/.exec(url); + if (!match) + return query_options; + + for (const opt of match[1].split("&")) { + const [key, val] = + /^([^=]*)=?(.*)/.exec(opt).splice(1, 2).map(decodeURIComponent); + query_options[key] = val; + } + + return query_options; +} + +function encode_query_options(query_options) +{ + return Object.entries(query_options) + .map(ar => ar.map(encodeURIComponent).join("=")).join("&"); +} + +function make_yewtube_url(youtube_url) +{ + const query_options = decode_query_options(youtube_url); + + let endpoint = ""; + + const match = /^(?:(?:https?:)?\/\/)?[^/]*\/([^?]*)/.exec(youtube_url); + if (match) + endpoint = match[1]; + + if (/^embed\//.test(query_options.v)) + endpoint = query_options.v; + + if (/^embed\/.+/.test(endpoint)) + delete query_options.v; + + const encoded_options = encode_query_options(query_options); + return `https://yewtu.be/${endpoint}?${encoded_options}`; +} diff --git a/src/youtube_yewtube_redirection.js b/src/youtube_yewtube_redirection.js new file mode 100644 index 0000000..5b90776 --- /dev/null +++ b/src/youtube_yewtube_redirection.js @@ -0,0 +1,21 @@ +/** + * SPDX-License-Identifier: CC0-1.0 + * + * Redirect youtube.com to yewtu.be + * + * Copyright (C) 2021 Wojtek Kosior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the CC0 1.0 Universal License as published by + * the Creative Commons Corporation. + * + * 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 + * CC0 1.0 Universal License for more details. + * Available under the terms of Creative Commons Zero. + */ + +/* Use with https://www.youtube.com/*** */ + +window.location.href = make_yewtube_url(document.URL); -- cgit v1.2.3