diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/accuweather.js | 19 | ||||
-rw-r--r-- | src/bandcamp.js | 31 | ||||
-rw-r--r-- | src/box.js | 300 | ||||
-rw-r--r-- | src/fedoraaccounts.js | 37 | ||||
-rw-r--r-- | src/google_drive_files.js | 168 | ||||
-rw-r--r-- | src/google_drive_folders.js | 384 | ||||
-rw-r--r-- | src/google_forms.js | 66 | ||||
-rw-r--r-- | src/google_sheets_download.js | 210 | ||||
-rw-r--r-- | src/internet_archive_video.js | 91 | ||||
-rw-r--r-- | src/odysee.js | 425 | ||||
-rw-r--r-- | src/opencores.js | 46 | ||||
-rw-r--r-- | src/pcspecialist_cookie_notice.js | 21 | ||||
-rw-r--r-- | src/pcspecialist_display_prices.js | 57 | ||||
-rw-r--r-- | src/phoronix_benchmarks.js | 38 | ||||
-rw-r--r-- | src/royal_geographical_society.js | 20 | ||||
-rw-r--r-- | src/santander_centrum24.js | 25 | ||||
-rw-r--r-- | src/stack_exchange_cookienotice.js | 20 | ||||
-rw-r--r-- | src/sumofus.js | 56 | ||||
-rw-r--r-- | src/vaticannews_videos.js | 24 | ||||
-rw-r--r-- | src/worldcat.js | 67 | ||||
-rw-r--r-- | src/yewtube_urls.js | 58 | ||||
-rw-r--r-- | src/youtube_yewtube_redirection.js | 21 |
22 files changed, 2184 insertions, 0 deletions
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 <jahoti@tilde.team> + * + * 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 <jahoti@tilde.team> + * + * 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 <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. + */ + +// 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 = `\ +<!DOCTYPE html> +<html> + <head> + <style> + button, .button { + border-radius: 10px; + padding: 20px; + color: #333; + background-color: + lightgreen; + text-decoration: none; + display: inline-block; + } + button:hover, .button:hover { + box-shadow: -4px 8px 8px #888; + } + + .hide { + display: none; + } + + #download_button .unofficial, #download_button .red_note { + display: none; + } + #download_button.unofficial .unofficial { + display: inline; + } + #download_button.unofficial .red_note { + display: block; + } + .red_note { + font-size: 75%; + color: #c55; + font-style: italic; + text-align: center; + } + </style> + </head> + <body> + <h1 id="loading" class="hide">loading...</h1> + <h1 id="error" class="hide">error occured :(</h1> + <h1 id="title" class="hide"></h1> + <div id="single_file_section" class="hide"> + <a id="download_button" class="button"> + <span class="unofficial">unofficial</span> download + <aside class="red_note">(officially disallowed)</aside> + </a> + <aside></aside> + <h2>File info</h2> + <div id="file_info"></div> + </div> + </body> +</html> +`; + +/* + * 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 <koszko@koszko.org> + * + * 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 <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://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 <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://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 <jahoti@tilde.team> + * + * 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 <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://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 = `<sheet gid=${gid}>`; + } + + 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 <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. + */ + +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 <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(); +} 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 <koszko@koszko.org> + * + * 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 <jahoti@tilde.team> + * + * 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 <jahoti@tilde.team> + * + * 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 <koszko@koszko.org> + * + * 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 + * <img> 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 <img>'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 <jahoti@tilde.team> + * + * 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 <koszko@koszko.org> + * + * 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 <jahoti@tilde.team> + * + * 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 <jahoti@tilde.team> + * + * 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 <koszko@koszko.org> + * + * 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 <jahoti@tilde.team> + * + * 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 <koszko@koszko.org> + * + * 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 <koszko@koszko.org> + * + * 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); |