diff options
author | Wojtek Kosior <koszko@koszko.org> | 2021-08-02 12:53:52 +0200 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2021-08-02 12:53:52 +0200 |
commit | 566bfec8f3ad616dc03391ab1b4805d087351b20 (patch) | |
tree | 3143282024c527b2fde27f34c73764dcb9a9d32b | |
parent | 100530f79b3c94db87a337c798c74eed896574af (diff) | |
download | hachette_fixes_tmp-master.tar.gz hachette_fixes_tmp-master.zip |
* A workaround to display Videos with unicode (e.g. emojis) in names has been added.
* Video descriptions now appear with their original formatting, with HTML entities properly decoded.
* Videos whose signing channel info is missing now also display properly in search results
* The "load more" button now gets disabled during loading of search results.
-rw-r--r-- | phttps___odysee.com.json | 2 |
1 files changed, 1 insertions, 1 deletions
diff --git a/phttps___odysee.com.json b/phttps___odysee.com.json index bbf6384..64fcf37 100644 --- a/phttps___odysee.com.json +++ b/phttps___odysee.com.json @@ -1 +1 @@ -[{"sodysee":{"url":"","hash":"","text":"/**\n * Copyright 2021 Wojtek Kosior\n *\n * This program is free software; you can redistribute it\n * and/or modify it under the terms of either:\n * - the GNU General Public License as published by the Free\n * Software Foundation; either version 3 of the License, or (at\n * your option) any later version, or\n * - the \"A\" license: <https://koszko.org/alicense.txt>; explained\n * at: <https://koszko.org/en/articles/my-new-license.html>\n *\n * As additional permission under GNU GPL version 3 section 7, you\n * may distribute forms of that code without the copy of the GNU\n * GPL normally required by section 4, provided you include this\n * license notice and, in case of non-source distribution, a URL\n * through which recipients can access the Corresponding Source.\n * If you modify file(s) with this exception, you may extend this\n * exception to your version of the file(s), but you are not\n * obligated to do so. If you do not wish to do so, delete this\n * exception statement from your version. If you delete this\n * exception statement from all source files in the program, then\n * also delete it here.\n *\n * As a special exception to the GPL, any HTML file which merely\n * makes function calls to this code, and for that purpose\n * includes it by reference shall be deemed a separate work for\n * copyright law purposes. If you modify this code, you may extend\n * this exception to your version of the code, but you are not\n * obligated to do so. If you do not wish to do so, delete this\n * exception statement from your version.\n */\n\n/* use with https://odysee.com/*** */\n\n/* If we're on a video page, show the video. */\n\nlet data = null;\n\nfunction process_json_script(json_script)\n{\n try {\n\tdata = JSON.parse(json_script.textContent);\n } catch (e) {\n\tconsole.log(\"Error parsing content data\", e);\n }\n}\n\nfor (const json_script of document.querySelectorAll(\"head script\")) {\n if ([\"blocked-type\", \"type\"].map(a => json_script.getAttribute(a))\n\t.includes(\"application/ld+json\"))\n\tprocess_json_script(json_script);\n}\n\nconst body = document.createElement(\"body\");\n\nif (data) {\n if (data[\"@type\"] === \"VideoObject\" && data.contentUrl) {\n\tconst video = document.createElement(\"video\");\n\tconst source = document.createElement(\"source\");\n\n\tsource.src = data.contentUrl;\n\n\tvideo.setAttribute(\"width\", \"100%\");\n\tvideo.setAttribute(\"height\", \"auto\");\n\tvideo.setAttribute(\"controls\", \"\");\n\n\tvideo.appendChild(source);\n\n\tbody.appendChild(video);\n }\n\n if (data.name) {\n\tconst h1 = document.createElement(\"h1\");\n\n\th1.textContent = data.name;\n\th1.setAttribute(\"style\", \"color: #555;\");\n\n\tbody.appendChild(h1);\n }\n\n if (data.uploadDate) {\n\ttry {\n\t const date = new Date(data.uploadDate).toString();\n\t const date_div = document.createElement(\"div\");\n\n\t date_div.textContent = `Uploaded: ${date}`;\n\t date_div.setAttribute(\"style\", \"font-size: 14px; font-weight: bold;\");\n\n\t body.appendChild(date_div);\n\t} catch(e) {\n\t console.log(\"Error parsing content upload date\", e);\n\t}\n }\n\n if (data.description) {\n\tconst description_div = document.createElement(\"div\");\n\n\tdescription_div.textContent = data.description;\n\n\tbody.appendChild(description_div);\n }\n}\n\n/* Show search. */\n\nconst search_input = document.createElement(\"input\");\nconst search_submit = document.createElement(\"button\");\nconst search_form = document.createElement(\"form\");\nconst error_div = document.createElement(\"div\");\n\nsearch_submit.textContent = \"Search Odysee\";\n\nsearch_form.setAttribute(\"style\", \"margin: 15px 0 0 0;\");\n\nsearch_form.appendChild(search_input);\nsearch_form.appendChild(search_submit);\n\nerror_div.textContent = \"Failed to perform search :c\";\nerror_div.setAttribute(\"style\", \"display: none;\");\n\nbody.appendChild(search_form);\nbody.appendChild(error_div);\n\n/* Replace the UI. */\n\ndocument.documentElement.replaceChild(body, document.body);\n\n/* Add the logic of performing search and showing results. */\n\nfunction show_error()\n{\n error_div.setAttribute(\"style\", \"color: #b44;\");\n}\n\nfunction clear_error()\n{\n error_div.setAttribute(\"style\", \"display: none;\");\n}\n\nlet results_div = null;\nconst load_more_but = document.createElement(\"button\");\n\nload_more_but.textContent = \"Load more\";\n\nfunction ajax_callback(xhttp, cb)\n{\n if (xhttp.readyState === 4)\n\tcb(xhttp.response);\n}\n\nfunction perform_ajax(method, url, callback, data)\n{\n const xhttp = new XMLHttpRequest();\n xhttp.onreadystatechange = () => ajax_callback(xhttp, callback);\n xhttp.open(method, url, true);\n try {\n\txhttp.send(data);\n } catch(e) {\n\tconsole.log(\"Failed to query search server :c\", e);\n\tshow_error();\n\tenable_search_form();\n }\n}\n\nfunction show_search_entries(new_results_div, response)\n{\n try {\n\tvar results = Object.values(JSON.parse(response).result);\n } catch (e) {\n\tconsole.log(\"Failed to parse search response :c\",\n\t\t \"Bad response format from api.na-backend.odysee.com.\");\n\terror_div.show_error();\n\treturn;\n }\n\n for (const result of results) {\n\ttry {\n\t if (result.value_type !== \"stream\")\n\t\tcontinue;\n\n\t const channel_name = encodeURIComponent(result.signing_channel.name);\n\t const channel_id = result.signing_channel.claim_id[0];\n\t const video_name = encodeURIComponent(result.name);\n\t const video_id = result.claim_id[0];\n\n\t const result_a = document.createElement(\"a\");\n\t const thumbnail = document.createElement(\"img\");\n\t const title_span = document.createElement(\"span\");\n\t const uploader_div = document.createElement(\"div\");\n\t const description_div = document.createElement(\"div\");\n\n\t thumbnail.setAttribute(\"style\", \"width: 100px; height: auto;\");\n\t thumbnail.setAttribute(\"alt\", result.value.thumbnail.url);\n\t thumbnail.src = result.value.thumbnail.url;\n\n\t title_span.setAttribute(\"style\", \"font-weight: bold;\");\n\t title_span.textContent = result.value.title;\n\n\t uploader_div.setAttribute(\"style\", \"margin-left: 5px; font-size: 21px; color: #555;\");\n\t uploader_div.textContent = result.signing_channel.name;\n\n\t description_div.textContent = result.value.description;\n\n\t 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;\");\n\t result_a.href = `https://odysee.com/${channel_name}:${channel_id}/${video_name}:${video_id}`;\n\n\t if (result.value.thumbnail.url)\n\t\tresult_a.appendChild(thumbnail);\n\t result_a.appendChild(title_span);\n\t result_a.appendChild(uploader_div);\n\t result_a.appendChild(description_div);\n\n\t new_results_div.appendChild(result_a);\n\t}\n\tcatch(e) {\n\t console.log(e);\n\t}\n }\n\n clear_error();\n\n if (results_div)\n\tresults_div.remove();\n\n results_div = new_results_div;\n\n body.appendChild(results_div);\n body.appendChild(load_more_but);\n\n enable_search_form();\n}\n\nfunction get_detailed_search_entries(new_results_div, response)\n{\n /* TODO: Simplify JSON handling using sanitize_JSON.js from Hachette. */\n try {\n\tvar response_data = JSON.parse(response);\n\tif (!Array.isArray(response_data))\n\t throw \"Bad response format from lighthouse.odysee.com.\";\n } catch (e) {\n\tshow_error();\n\tconsole.log(\"Failed to parse search response :c\", e);\n\tenable_search_form();\n\treturn;\n }\n\n const callback = r => show_search_entries(new_results_div, r);\n const url = \"https://api.na-backend.odysee.com/api/v1/proxy?m=resolve\";\n const lbry_urls = [];\n\n for (const search_result of response_data) {\n\tif (!search_result.claimId || !search_result.name)\n\t continue;\n\tlbry_urls.push(`lbry://${search_result.name}#${search_result.claimId}`);\n }\n\n const payload = {\n\tjsonrpc: \"2.0\",\n\tmethod: \"resolve\",\n\tparams: {\n\t urls: lbry_urls,\n\t include_purchase_receipt: true\n\t},\n\tid: Math.round(Math.random() * 10**14)\n };\n\n console.log(\"payload\", payload);\n\n perform_ajax(\"POST\", url, callback, JSON.stringify(payload));\n}\n\nfunction get_search_entries(new_results_div, query, from)\n{\n const callback = r => get_detailed_search_entries(new_results_div, r);\n const url = `https://lighthouse.odysee.com/search?s=${encodeURIComponent(query)}&size=20&from=${from}&claimType=file,channel&nsfw=false&free_only=true`;\n\n new_results_div.setAttribute(\"data-fetched\", parseInt(from) + 20);\n\n perform_ajax(\"GET\", url, callback);\n}\n\nfunction search(event)\n{\n if (event)\n\tevent.preventDefault();\n console.log(\"search\", search_input.value);\n if (!/[^\\s]/.test(search_input.value))\n\treturn;\n\n disable_search_form();\n\n const new_results_div = document.createElement(\"div\");\n\n new_results_div.setAttribute(\"data-query\", search_input.value);\n\n get_search_entries(new_results_div, search_input.value, 0);\n}\n\nfunction search_more()\n{\n get_search_entries(results_div, results_div.getAttribute(\"data-query\"),\n\t\t results_div.getAttribute(\"data-fetched\"));\n}\n\nload_more_but.addEventListener(\"click\", search_more);\n\nfunction enable_search_form()\n{\n search_form.addEventListener(\"submit\", search);\n search_submit.removeAttribute(\"disabled\");\n if (results_div)\n\tload_more_but.removeAttribute(\"disabled\");\n}\n\nfunction disable_search_form()\n{\n search_form.removeEventListener(\"submit\", search);\n search_submit.setAttribute(\"disabled\", \"\");\n load_more_but.setAttribute(\"disabled\", \"\");\n}\n\n\nenable_search_form();\n\n\nconst match = /^[^?]*search\\?q=([^&]+)/.exec(document.URL)\nif (match) {\n search_input.value = decodeURIComponent(match[1]);\n search();\n}\n"}},{"phttps://odysee.com":{"components":["s","odysee"],"allow":false}},{"phttps://odysee.com/***":{"components":["s","odysee"],"allow":false}}] +[{"sodysee":{"url":"","hash":"","text":"/**\n * Copyright 2021 Wojtek Kosior\n *\n * This program is free software; you can redistribute it\n * and/or modify it under the terms of either:\n * - the GNU General Public License as published by the Free\n * Software Foundation; either version 3 of the License, or (at\n * your option) any later version, or\n * - the \"A\" license: <https://koszko.org/alicense.txt>; explained\n * at: <https://koszko.org/en/articles/my-new-license.html>\n *\n * As additional permission under GNU GPL version 3 section 7, you\n * may distribute forms of that code without the copy of the GNU\n * GPL normally required by section 4, provided you include this\n * license notice and, in case of non-source distribution, a URL\n * through which recipients can access the Corresponding Source.\n * If you modify file(s) with this exception, you may extend this\n * exception to your version of the file(s), but you are not\n * obligated to do so. If you do not wish to do so, delete this\n * exception statement from your version. If you delete this\n * exception statement from all source files in the program, then\n * also delete it here.\n *\n * As a special exception to the GPL, any HTML file which merely\n * makes function calls to this code, and for that purpose\n * includes it by reference shall be deemed a separate work for\n * copyright law purposes. If you modify this code, you may extend\n * this exception to your version of the code, but you are not\n * obligated to do so. If you do not wish to do so, delete this\n * exception statement from your version.\n */\n\n/* use with https://odysee.com/*** */\n\n/* Helper functions for ajax. */\n\nfunction ajax_callback(xhttp, cb)\n{\n cb(xhttp.response);\n}\n\nfunction perform_ajax(method, url, callback, err_callback, data)\n{\n const xhttp = new XMLHttpRequest();\n xhttp.onload = () => ajax_callback(xhttp, callback);\n xhttp.onerror = err_callback;\n xhttp.onabort = err_callback;\n xhttp.open(method, url, true);\n try {\n\txhttp.send(data);\n } catch(e) {\n\terr_callback();\n }\n}\n\n/* Helper functions for strings with HTML entities (e.g. `"'). */\nfunction HTML_decode(text)\n{\n const tmp_span = document.createElement(\"span\");\n tmp_span.innerHTML = text;\n return tmp_span.textContent;\n}\n\n/* Odysee API servers. */\n\nconst odysee_resolve_url = \"https://api.na-backend.odysee.com/api/v1/proxy?m=resolve\";\nconst lighthouse_search_url = \"https://lighthouse.odysee.com/search\";\n\n/*\n * If we're on a video page, show the video. Use JSON data embedded in <head>\n * if possible. If not - fetch video data using Odysee API.\n */\n\nlet data = null;\n\nfunction process_json_script(json_script)\n{\n try {\n\tdata = JSON.parse(json_script.textContent);\n } catch (e) {\n\tconsole.log(\"Error parsing content data\", e);\n }\n}\n\nfor (const json_script of document.querySelectorAll(\"head script\")) {\n if ([\"blocked-type\", \"type\"].map(a => json_script.getAttribute(a))\n\t.includes(\"application/ld+json\"))\n\tprocess_json_script(json_script);\n}\n\nconst body = document.createElement(\"body\");\nconst video_container = document.createElement(\"div\");\n\nbody.appendChild(video_container);\n\nfunction show_video(content_url, title, upload_date, description)\n{\n if (content_url) {\n\tconst video = document.createElement(\"video\");\n\tconst source = document.createElement(\"source\");\n\n\tsource.src = content_url;\n\n\tvideo.setAttribute(\"width\", \"100%\");\n\tvideo.setAttribute(\"height\", \"auto\");\n\tvideo.setAttribute(\"controls\", \"\");\n\n\tvideo.appendChild(source);\n\n\tvideo_container.appendChild(video);\n }\n\n if (title) {\n\tconst h1 = document.createElement(\"h1\");\n\n\th1.textContent = HTML_decode(title);\n\th1.setAttribute(\"style\", \"color: #555;\");\n\n\tvideo_container.appendChild(h1);\n }\n\n if (upload_date) {\n\ttry {\n\t const date = new Date(upload_date).toString();\n\t const date_div = document.createElement(\"div\");\n\n\t date_div.textContent = `Uploaded: ${date}`;\n\t date_div.setAttribute(\"style\", \"font-size: 14px; font-weight: bold; margin-bottom: 5px;\");\n\n\t video_container.appendChild(date_div);\n\t} catch(e) {\n\t console.log(\"Error parsing content upload date\", e);\n\t}\n }\n\n if (description) {\n\tconst description_div = document.createElement(\"div\");\n\n\tdescription_div.textContent = HTML_decode(description);\n\tdescription_div.setAttribute(\"style\", \"white-space: pre;\");\n\n\tvideo_container.appendChild(description_div);\n }\n}\n\nfunction show_video_from_query(response)\n{\n try {\n\tvar result = Object.values(JSON.parse(response).result)[0];\n\n\tif (result.value_type !== \"stream\")\n\t return;\n\n\tvar date = result.timestamp * 1000;\n\tvar description = result.value.description;\n\tvar title = result.value.title;\n\tconst name = encodeURIComponent(result.name);\n\tvar url = `https://odysee.com/$/stream/${name}/${result.claim_id}`;\n } catch (e) {\n\treturn;\n }\n\n show_video(url, title, date, description);\n}\n\nfunction fetch_show_video(name, claim_id)\n{\n const payload = {\n\tjsonrpc: \"2.0\",\n\tmethod: \"resolve\",\n\tparams: {\n\t urls: [`lbry://${decodeURIComponent(name)}#${claim_id}`],\n\t include_purchase_receipt: true\n\t},\n\tid: Math.round(Math.random() * 10**14)\n };\n\n perform_ajax(\"POST\", odysee_resolve_url, show_video_from_query,\n\t\t () => null, JSON.stringify(payload));\n}\n\nif (data && typeof data === \"object\" && data[\"@type\"] === \"VideoObject\") {\n show_video(data.contentUrl, data.name, data.uploadDate, data.description);\n} else {\n const match = /\\/([^/]+):([0-9a-f]+)$/.exec(document.URL);\n if (match)\n\tfetch_show_video(match[1], match[2]);\n}\n\n/* Show search. */\n\nconst search_input = document.createElement(\"input\");\nconst search_submit = document.createElement(\"button\");\nconst search_form = document.createElement(\"form\");\nconst error_div = document.createElement(\"div\");\n\nsearch_submit.textContent = \"Search Odysee\";\n\nsearch_form.setAttribute(\"style\", \"margin: 15px 0 0 0;\");\n\nsearch_form.appendChild(search_input);\nsearch_form.appendChild(search_submit);\n\nerror_div.textContent = \"Failed to perform search :c\";\nerror_div.setAttribute(\"style\", \"display: none;\");\n\nbody.appendChild(search_form);\nbody.appendChild(error_div);\n\n/* Replace the UI. */\n\ndocument.documentElement.replaceChild(body, document.body);\n\n/* Add the logic of performing search and showing results. */\n\nfunction show_error()\n{\n error_div.setAttribute(\"style\", \"color: #b44;\");\n}\n\nfunction clear_error()\n{\n error_div.setAttribute(\"style\", \"display: none;\");\n}\n\nlet results_div = null;\nconst load_more_but = document.createElement(\"button\");\n\nload_more_but.textContent = \"Load more\";\n\nfunction show_search_entries(new_results_div, response)\n{\n try {\n\tvar results = Object.values(JSON.parse(response).result);\n } catch (e) {\n\tconsole.log(\"Failed to parse search response :c\",\n\t\t \"Bad response format from api.na-backend.odysee.com.\");\n\tshow_error();\n\treturn;\n }\n\n for (const result of results) {\n\ttry {\n\t if (result.value_type !== \"stream\")\n\t\tcontinue;\n\n\t let channel_specifier = \"\";\n\t let channel_name = null;\n\t try {\n\t\tchannel_name = result.signing_channel.name;\n\t\tconst channel_name_enc = encodeURIComponent(channel_name);\n\t\tconst channel_digit = result.signing_channel.claim_id[0];\n\t\tchannel_specifier = `${channel_name_enc}:${channel_digit}`;\n\t } catch (e) {\n\t }\n\t const video_name = encodeURIComponent(result.name);\n\t const video_id = result.claim_id[0];\n\n\t const result_a = document.createElement(\"a\");\n\t const thumbnail = document.createElement(\"img\");\n\t const title_span = document.createElement(\"span\");\n\t const uploader_div = document.createElement(\"div\");\n\t const description_div = document.createElement(\"div\");\n\n\t thumbnail.setAttribute(\"style\", \"width: 100px; height: auto;\");\n\t thumbnail.setAttribute(\"alt\", result.value.thumbnail.url);\n\t thumbnail.src = result.value.thumbnail.url;\n\n\t title_span.setAttribute(\"style\", \"font-weight: bold;\");\n\t title_span.textContent = result.value.title;\n\n\t uploader_div.setAttribute(\"style\", \"margin-left: 5px; font-size: 21px; color: #555;\");\n\t uploader_div.textContent = channel_name;\n\n\t description_div.setAttribute(\"style\", \"white-space: pre;\");\n\t description_div.textContent = result.value.description;\n\n\t 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;\");\n\t result_a.href = `https://odysee.com${channel_specifier}/${video_name}:${video_id}`;\n\n\t if (result.value.thumbnail.url)\n\t\tresult_a.appendChild(thumbnail);\n\t result_a.appendChild(title_span);\n\t if (channel_name)\n\t\tresult_a.appendChild(uploader_div);\n\t result_a.appendChild(description_div);\n\n\t new_results_div.appendChild(result_a);\n\t}\n\tcatch(e) {\n\t console.log(e);\n\t}\n }\n\n clear_error();\n\n if (results_div)\n\tresults_div.remove();\n\n results_div = new_results_div;\n\n body.appendChild(results_div);\n body.appendChild(load_more_but);\n\n enable_search_form();\n}\n\nfunction search_ajax_error(url)\n{\n console.log(`Failed to query ${url} :c`);\n show_error();\n enable_search_form();\n}\n\nfunction get_detailed_search_entries(new_results_div, response)\n{\n /* TODO: Simplify JSON handling using sanitize_JSON.js from Hachette. */\n try {\n\tvar response_data = JSON.parse(response);\n\tif (!Array.isArray(response_data))\n\t throw \"Bad response format from lighthouse.odysee.com.\";\n } catch (e) {\n\tshow_error();\n\tconsole.log(\"Failed to parse search response :c\", e);\n\tenable_search_form();\n\treturn;\n }\n\n const callback = r => show_search_entries(new_results_div, r);\n const lbry_urls = [];\n\n for (const search_result of response_data) {\n\tif (!search_result.claimId || !search_result.name)\n\t continue;\n\tlbry_urls.push(`lbry://${search_result.name}#${search_result.claimId}`);\n }\n\n const payload = {\n\tjsonrpc: \"2.0\",\n\tmethod: \"resolve\",\n\tparams: {\n\t urls: lbry_urls,\n\t include_purchase_receipt: true\n\t},\n\tid: Math.round(Math.random() * 10**14)\n };\n\n const url = odysee_resolve_url;\n\n perform_ajax(\"POST\", url, callback, () => search_ajax_error(url),\n\t\t JSON.stringify(payload));\n}\n\nfunction get_search_entries(new_results_div, query, from)\n{\n const callback = r => get_detailed_search_entries(new_results_div, r);\n const url = `${lighthouse_search_url}?s=${encodeURIComponent(query)}&size=20&from=${from}&claimType=file,channel&nsfw=false&free_only=true`;\n\n new_results_div.setAttribute(\"data-fetched\", parseInt(from) + 20);\n\n perform_ajax(\"GET\", url, callback, () => search_ajax_error(url));\n}\n\nfunction search(event)\n{\n if (event)\n\tevent.preventDefault();\n\n if (!/[^\\s]/.test(search_input.value))\n\treturn;\n\n disable_search_form();\n\n const new_results_div = document.createElement(\"div\");\n\n new_results_div.setAttribute(\"data-query\", search_input.value);\n\n get_search_entries(new_results_div, search_input.value, 0);\n}\n\nfunction search_more()\n{\n disable_search_form();\n\n get_search_entries(results_div, results_div.getAttribute(\"data-query\"),\n\t\t results_div.getAttribute(\"data-fetched\"));\n}\n\nload_more_but.addEventListener(\"click\", search_more);\n\nfunction enable_search_form()\n{\n search_form.addEventListener(\"submit\", search);\n search_submit.removeAttribute(\"disabled\");\n if (results_div)\n\tload_more_but.removeAttribute(\"disabled\");\n}\n\nfunction disable_search_form()\n{\n search_form.removeEventListener(\"submit\", search);\n search_submit.setAttribute(\"disabled\", \"\");\n load_more_but.setAttribute(\"disabled\", \"\");\n}\n\n\nenable_search_form();\n\n\nconst match = /^[^?]*search\\?q=([^&]+)/.exec(document.URL)\nif (match) {\n search_input.value = decodeURIComponent(match[1]);\n search();\n}\n"}},{"phttps://odysee.com":{"allow":false,"components":["s","odysee"]}},{"phttps://odysee.com/***":{"components":["s","odysee"],"allow":false}}] |