diff options
author | ManeraKai <manerakai@protonmail.com> | 2022-05-30 01:02:59 +0300 |
---|---|---|
committer | ManeraKai <manerakai@protonmail.com> | 2022-05-30 01:02:59 +0300 |
commit | c5a6623e0f320b2a789e0d2667ccfe2e1bd4335f (patch) | |
tree | dabfa0f2cecf137771f276996f9368922c5aeb64 /src/assets/javascripts/youtube | |
parent | Fixed typo for CopyRaw #291 (diff) | |
download | libredirect-c5a6623e0f320b2a789e0d2667ccfe2e1bd4335f.zip |
Fixed settings theme and translation flashing #286
Diffstat (limited to 'src/assets/javascripts/youtube')
5 files changed, 536 insertions, 0 deletions
diff --git a/src/assets/javascripts/youtube/get_pipedMaterial_preferences.js b/src/assets/javascripts/youtube/get_pipedMaterial_preferences.js new file mode 100644 index 00000000..9d052f62 --- /dev/null +++ b/src/assets/javascripts/youtube/get_pipedMaterial_preferences.js @@ -0,0 +1,7 @@ +window.browser = window.browser || window.chrome; + +browser.storage.local.set( + { + 'pipedMaterial_PREFERENCES': localStorage.getItem("PREFERENCES") + } +) \ No newline at end of file diff --git a/src/assets/javascripts/youtube/get_piped_preferences.js b/src/assets/javascripts/youtube/get_piped_preferences.js new file mode 100644 index 00000000..417d64df --- /dev/null +++ b/src/assets/javascripts/youtube/get_piped_preferences.js @@ -0,0 +1,24 @@ +window.browser = window.browser || window.chrome; + +browser.storage.local.set( + { + 'piped_bufferGoal': localStorage.getItem("bufferGoal"), + 'piped_comments': localStorage.getItem("comments"), + 'piped_disableLBRY': localStorage.getItem("disableLBRY"), + 'piped_enabledCodecs': localStorage.getItem("enabledCodecs"), + 'piped_hl': localStorage.getItem("hl"), + 'piped_homepage': localStorage.getItem("homepage"), + 'piped_instance': localStorage.getItem("instance"), + 'piped_listen': localStorage.getItem("listen"), + 'piped_minimizeDescription': localStorage.getItem("minimizeDescription"), + 'piped_playerAutoPlay': localStorage.getItem("playerAutoPlay"), + 'piped_proxyLBRY': localStorage.getItem("proxyLBRY"), + 'piped_quality': localStorage.getItem("quality"), + 'piped_region': localStorage.getItem("region"), + 'piped_selectedSkip': localStorage.getItem("selectedSkip"), + 'piped_sponsorblock': localStorage.getItem("sponsorblock"), + 'piped_theme': localStorage.getItem("theme"), + 'piped_volume': localStorage.getItem("volume"), + 'piped_watchHistory': localStorage.getItem("watchHistory"), + } +) diff --git a/src/assets/javascripts/youtube/set_pipedMaterial_preferences.js b/src/assets/javascripts/youtube/set_pipedMaterial_preferences.js new file mode 100644 index 00000000..7415255a --- /dev/null +++ b/src/assets/javascripts/youtube/set_pipedMaterial_preferences.js @@ -0,0 +1,10 @@ +window.browser = window.browser || window.chrome; + +browser.storage.local.get( + "pipedMaterial_PREFERENCES", + r => { + if (r.pipedMaterial_PREFERENCES !== undefined) localStorage.setItem("PREFERENCES", r.pipedMaterial_PREFERENCES) + + window.close(); + } +) \ No newline at end of file diff --git a/src/assets/javascripts/youtube/set_piped_preferences.js b/src/assets/javascripts/youtube/set_piped_preferences.js new file mode 100644 index 00000000..d5ce49ff --- /dev/null +++ b/src/assets/javascripts/youtube/set_piped_preferences.js @@ -0,0 +1,44 @@ +window.browser = window.browser || window.chrome; + +browser.storage.local.get( + [ + "piped_bufferGoal", + "piped_comments", + "piped_disableLBRY", + "piped_enabledCodecs", + "piped_homepage", + "piped_listen", + "piped_minimizeDescription", + "piped_playerAutoPlay", + "piped_proxyLBRY", + "piped_quality", + "piped_region", + "piped_selectedSkip", + "piped_sponsorblock", + "piped_theme", + "piped_volume", + "piped_watchHistory", + ], + r => { + if (r.piped_bufferGoal !== undefined) localStorage.setItem("bufferGoal", r.piped_bufferGoal); + if (r.piped_comments !== undefined) localStorage.setItem("comments", r.piped_comments); + if (r.piped_disableLBRY !== undefined) localStorage.setItem("disableLBRY", r.piped_disableLBRY); + if (r.piped_hl !== undefined) localStorage.setItem("hl", r.piped_hl); + if (r.piped_enabledCodecs !== undefined) localStorage.setItem("enabledCodecs", r.piped_enabledCodecs); + if (r.piped_homepage !== undefined) localStorage.setItem("homepage", r.piped_homepage); + if (r.piped_instance !== undefined) localStorage.setItem("instance", r.piped_instance); + if (r.piped_listen !== undefined) localStorage.setItem("listen", r.piped_listen); + if (r.piped_minimizeDescription !== undefined) localStorage.setItem("minimizeDescription", r.piped_minimizeDescription); + if (r.piped_playerAutoPlay !== undefined) localStorage.setItem("playerAutoPlay", r.piped_playerAutoPlay); + if (r.piped_proxyLBRY !== undefined) localStorage.setItem("proxyLBRY", r.piped_proxyLBRY); + if (r.piped_quality !== undefined) localStorage.setItem("quality", r.piped_quality); + if (r.piped_region !== undefined) localStorage.setItem("region", r.piped_region); + if (r.piped_selectedSkip !== undefined) localStorage.setItem("selectedSkip", r.piped_selectedSkip); + if (r.piped_sponsorblock !== undefined) localStorage.setItem("sponsorblock", r.piped_sponsorblock); + if (r.piped_theme !== undefined) localStorage.setItem("theme", r.piped_theme); + if (r.piped_volume !== undefined) localStorage.setItem("volume", r.piped_volume); + if (r.piped_watchHistory !== undefined) localStorage.setItem("watchHistory", r.piped_watchHistory); + + window.close(); + } +) \ No newline at end of file diff --git a/src/assets/javascripts/youtube/youtube.js b/src/assets/javascripts/youtube/youtube.js new file mode 100644 index 00000000..f8227e90 --- /dev/null +++ b/src/assets/javascripts/youtube/youtube.js @@ -0,0 +1,451 @@ +"use strict"; + +window.browser = window.browser || window.chrome; + +import utils from '../utils.js' + +const targets = [ + /^https?:\/{2}(www\.|music\.|m\.|)youtube\.com(\/.*|$)/, + + /^https?:\/{2}img\.youtube\.com\/vi\/.*\/..*/, // https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api + /^https?:\/{2}(i|s)\.ytimg\.com\/vi\/.*\/..*/, + + /^https?:\/{2}(www\.|music\.|)youtube\.com\/watch\?v\=..*/, + + /^https?:\/{2}youtu\.be\/..*/, + + /^https?:\/{2}(www\.|)(youtube|youtube-nocookie)\.com\/embed\/..*/, +]; +let redirects = { + "invidious": { + "normal": [], + "tor": [] + }, + "piped": { + "normal": [], + "tor": [] + }, + "pipedMaterial": { + "normal": [ + "https://piped-material.xn--17b.net", + "https://piped-material.ftp.sh", + ], + "tor": [] + } +}; +function setRedirects(val) { + browser.storage.local.get('cloudflareList', r => { + redirects.invidious = val.invidious; + redirects.piped = val.piped; + invidiousNormalRedirectsChecks = [...redirects.invidious.normal]; + pipedNormalRedirectsChecks = [...redirects.piped.normal]; + for (const instance of r.cloudflareList) { + const a = invidiousNormalRedirectsChecks.indexOf(instance); + if (a > -1) invidiousNormalRedirectsChecks.splice(a, 1); + + const b = pipedNormalRedirectsChecks.indexOf(instance); + if (b > -1) pipedNormalRedirectsChecks.splice(b, 1); + } + browser.storage.local.set({ + youtubeRedirects: redirects, + invidiousNormalRedirectsChecks, + invidiousTorRedirectsChecks: redirects.invidious.tor, + pipedNormalRedirectsChecks, + pipedTorRedirectsChecks: redirects.piped.tor, + }) + }) +} + +let + disableYoutube, + OnlyEmbeddedVideo, + youtubeFrontend, + youtubeProtocol, + youtubeEmbedFrontend, + youtubeRedirects, + invidiousNormalRedirectsChecks, + invidiousNormalCustomRedirects, + invidiousTorRedirectsChecks, + invidiousTorCustomRedirects, + pipedNormalRedirectsChecks, + pipedNormalCustomRedirects, + pipedTorRedirectsChecks, + pipedTorCustomRedirects, + pipedMaterialNormalRedirectsChecks, + pipedMaterialNormalCustomRedirects, + pipedMaterialTorRedirectsChecks, + pipedMaterialTorCustomRedirects; + +function init() { + return new Promise(resolve => { + browser.storage.local.get( + [ + "disableYoutube", + "OnlyEmbeddedVideo", + "youtubeFrontend", + "youtubeProtocol", + "youtubeEmbedFrontend", + "youtubeRedirects", + "invidiousNormalRedirectsChecks", + "invidiousNormalCustomRedirects", + "invidiousTorRedirectsChecks", + "invidiousTorCustomRedirects", + "pipedNormalRedirectsChecks", + "pipedNormalCustomRedirects", + "pipedTorRedirectsChecks", + "pipedTorCustomRedirects", + "pipedMaterialNormalRedirectsChecks", + "pipedMaterialNormalCustomRedirects", + "pipedMaterialTorRedirectsChecks", + "pipedMaterialTorCustomRedirects", + ], + r => { + disableYoutube = r.disableYoutube; + OnlyEmbeddedVideo = r.OnlyEmbeddedVideo; + youtubeFrontend = r.youtubeFrontend; + youtubeProtocol = r.youtubeProtocol; + youtubeEmbedFrontend = r.youtubeEmbedFrontend; + youtubeRedirects = r.youtubeRedirects; + invidiousNormalRedirectsChecks = r.invidiousNormalRedirectsChecks; + invidiousNormalCustomRedirects = r.invidiousNormalCustomRedirects; + invidiousTorRedirectsChecks = r.invidiousTorRedirectsChecks; + invidiousTorCustomRedirects = r.invidiousTorCustomRedirects; + pipedNormalRedirectsChecks = r.pipedNormalRedirectsChecks; + pipedNormalCustomRedirects = r.pipedNormalCustomRedirects; + pipedTorRedirectsChecks = r.pipedTorRedirectsChecks; + pipedTorCustomRedirects = r.pipedTorCustomRedirects; + pipedMaterialNormalRedirectsChecks = r.pipedMaterialNormalRedirectsChecks; + pipedMaterialNormalCustomRedirects = r.pipedMaterialNormalCustomRedirects; + pipedMaterialTorRedirectsChecks = r.pipedMaterialTorRedirectsChecks; + pipedMaterialTorCustomRedirects = r.pipedMaterialTorCustomRedirects; + resolve(); + } + ) + }) +} + +init(); +browser.storage.onChanged.addListener(init) + +function all() { + return [ + ...youtubeRedirects.invidious.normal, + ...youtubeRedirects.invidious.tor, + + ...youtubeRedirects.piped.normal, + ...youtubeRedirects.piped.tor, + + ...youtubeRedirects.pipedMaterial.normal, + ...youtubeRedirects.pipedMaterial.tor, + + ...invidiousNormalCustomRedirects, + ...invidiousTorCustomRedirects, + + ...pipedNormalCustomRedirects, + ...pipedTorCustomRedirects, + + ...pipedMaterialNormalCustomRedirects, + ...pipedMaterialTorCustomRedirects, + ]; +} + +function redirect(url, details, initiator) { + if (disableYoutube) return; + if (!targets.some(rx => rx.test(url.href))) return; + if (initiator && all().includes(initiator.origin)) return 'BYPASSTAB'; + + const isInvidious = youtubeFrontend == 'invidious'; + const isPiped = youtubeFrontend == 'piped'; + const isPipedMaterial = youtubeFrontend == 'pipedMaterial' + const isFreetube = youtubeFrontend == 'freetube'; + const isYatte = youtubeFrontend == 'yatte'; + + const isFrontendYoutube = youtubeEmbedFrontend == "youtube"; + const isFrontendInvidious = youtubeEmbedFrontend == 'invidious'; + const isFrontendPiped = youtubeEmbedFrontend == 'piped'; + const isFrontendPipedMaterial = youtubeEmbedFrontend == 'pipedMaterial'; + + const isOnlyEmbeddedVideo = OnlyEmbeddedVideo == 'onlyNotEmbedded'; + const isOnlyNotEmbedded = OnlyEmbeddedVideo == 'onlyNotEmbedded' + + const is_main_frame = details.type === "main_frame"; + const is_sub_frame = details.type === "sub_frame"; + + if (url.pathname.match(/iframe_api/) || url.pathname.match(/www-widgetapi/)) return; // Don't redirect YouTube Player API. + + if (youtubeFrontend == 'yatte' && is_main_frame) + return url.href.replace(/^https?:\/{2}/, 'yattee://'); + + else if (isFreetube && is_main_frame) + return `freetube://https:${url.pathname}${url.search}`; + + else if (isFreetube && params && isFrontendYoutube) + return; + + else if (isInvidious || ((isFreetube || isYatte) && isFrontendInvidious && is_sub_frame)) { + + if (isOnlyEmbeddedVideo && !is_sub_frame) return; + if (isOnlyNotEmbedded && params && !((isFreetube || isYatte) && isFrontendInvidious && is_sub_frame)) return; + + let instancesList; + if (youtubeProtocol == 'normal') instancesList = [...invidiousNormalRedirectsChecks, ...invidiousNormalCustomRedirects]; + else if (youtubeProtocol == 'tor') instancesList = [...invidiousTorRedirectsChecks, ...invidiousTorCustomRedirects]; + if (instancesList.length === 0) return; + let randomInstance = utils.getRandomInstance(instancesList); + + return `${randomInstance}${url.pathname}${url.search}`; + } else if (isPiped || ((isFreetube || isYatte) && isFrontendPiped && is_sub_frame)) { + + if (isOnlyEmbeddedVideo && !is_sub_frame) return; + if ( + isOnlyNotEmbedded && params && + !((isFreetube || isYatte) && isFrontendPiped && is_sub_frame) + ) return; + + let instancesList; + if (youtubeProtocol == 'normal') instancesList = [...pipedNormalRedirectsChecks, ...pipedNormalCustomRedirects]; + else if (youtubeProtocol == 'tor') instancesList = [...pipedTorRedirectsChecks, ...pipedTorCustomRedirects]; + if (instancesList.length === 0) return; + + const randomInstance = utils.getRandomInstance(instancesList); + return `${randomInstance}${url.pathname}${url.search}`; + } + else if (isPipedMaterial || ((isFreetube || isYatte) && isFrontendPipedMaterial && is_sub_frame)) { + if (isOnlyEmbeddedVideo && details.type !== "sub_frame") return; + if ( + isOnlyNotEmbedded && params && + !((isFreetube || isYatte) && isFrontendPipedMaterial && is_sub_frame) + ) return; + + let instancesList; + if (youtubeProtocol == 'normal') instancesList = [...pipedMaterialNormalRedirectsChecks, ...pipedMaterialNormalCustomRedirects]; + else if (youtubeProtocol == 'tor') instancesList = [...pipedMaterialTorRedirectsChecks, ...pipedMaterialTorCustomRedirects]; + + const randomInstance = utils.getRandomInstance(instancesList); + return `${randomInstance}${url.pathname}${url.search}`; + } + else return 'CANCEL'; +} + +function reverse(url) { + return new Promise(async resolve => { + await init(); + const protocolHost = utils.protocolHost(url); + if (!all().includes(protocolHost)) { resolve(); return; } + resolve(`https://youtube.com${url.pathname}${url.search}`); + }) +} + +function switchInstance(url) { + return new Promise(async resolve => { + await init(); + const protocolHost = utils.protocolHost(url); + const instances = all(); + if (!instances.includes(protocolHost)) { resolve(); return; } + + let instancesList; + if (youtubeProtocol == 'normal') { + if (youtubeFrontend == 'invidious') instancesList = [...invidiousNormalRedirectsChecks, ...invidiousNormalCustomRedirects]; + else if (youtubeFrontend == 'piped') instancesList = [...pipedNormalRedirectsChecks, ...pipedNormalCustomRedirects]; + else if (youtubeFrontend == 'pipedMaterial') instancesList = [...pipedMaterialNormalRedirectsChecks, ...pipedMaterialNormalCustomRedirects]; + } + else if (youtubeProtocol == 'tor') { + if (youtubeFrontend == 'invidious') instancesList = [...invidiousTorRedirectsChecks, ...invidiousTorCustomRedirects]; + else if (youtubeFrontend == 'piped') instancesList = [...pipedTorRedirectsChecks, ...pipedTorCustomRedirects]; + else if (youtubeFrontend == 'pipedMaterial') instancesList = [...pipedMaterialTorRedirectsChecks, ...pipedMaterialTorCustomRedirects]; + } + + const i = instancesList.indexOf(protocolHost); + if (i > -1) instancesList.splice(i, 1); + if (instancesList.length == 0) { resolve(); return; } + + const randomInstance = utils.getRandomInstance(instancesList); + resolve(`${randomInstance}${url.pathname}${url.search}`); + }) +} + +function initDefaults() { + return new Promise(async resolve => { + fetch('/instances/data.json').then(response => response.text()).then(async data => { + let dataJson = JSON.parse(data); + redirects.invidious = dataJson.invidious; + redirects.piped = dataJson.piped; + browser.storage.local.get('cloudflareList', async r => { + + invidiousNormalRedirectsChecks = [...redirects.invidious.normal]; + pipedNormalRedirectsChecks = [...redirects.piped.normal]; + pipedMaterialNormalRedirectsChecks = [...redirects.pipedMaterial.normal]; + + for (const instance of r.cloudflareList) { + let i; + + i = invidiousNormalRedirectsChecks.indexOf(instance); + if (i > -1) invidiousNormalRedirectsChecks.splice(i, 1); + + i = pipedNormalRedirectsChecks.indexOf(instance); + if (i > -1) pipedNormalRedirectsChecks.splice(i, 1); + + i = pipedMaterialNormalRedirectsChecks.indexOf(instance); + if (i > -1) pipedMaterialNormalRedirectsChecks.splice(i, 1); + } + + await browser.storage.local.set({ + disableYoutube: false, + enableYoutubeCustomSettings: false, + OnlyEmbeddedVideo: 'both', + + youtubeRedirects: redirects, + + youtubeFrontend: 'invidious', + + invidiousNormalRedirectsChecks: invidiousNormalRedirectsChecks, + invidiousNormalCustomRedirects: [], + + invidiousTorRedirectsChecks: [...redirects.invidious.tor], + invidiousTorCustomRedirects: [], + + pipedNormalRedirectsChecks: pipedNormalRedirectsChecks, + pipedNormalCustomRedirects: [], + + pipedTorRedirectsChecks: [...redirects.piped.tor], + pipedTorCustomRedirects: [], + + pipedMaterialNormalRedirectsChecks: pipedMaterialNormalRedirectsChecks, + pipedMaterialNormalCustomRedirects: [], + + pipedMaterialTorRedirectsChecks: [...redirects.pipedMaterial.tor], + pipedMaterialTorCustomRedirects: [], + + youtubeEmbedFrontend: 'invidious', + youtubeProtocol: 'normal', + }) + resolve(); + }) + }) + }) +} + +function initInvidiousCookies(test, from) { + return new Promise(async resolve => { + await init(); + if (disableYoutube || youtubeFrontend != 'invidious') { resolve(); return; } + const protocolHost = utils.protocolHost(from); + if (![ + ...invidiousNormalRedirectsChecks, + ...invidiousTorRedirectsChecks, + ...invidiousNormalCustomRedirects, + ...invidiousTorCustomRedirects, + ].includes(protocolHost)) { resolve(); return; } + if (!test) { + let checkedInstances; + if (youtubeProtocol == 'normal') checkedInstances = [...invidiousNormalRedirectsChecks, ...invidiousNormalCustomRedirects] + else if (youtubeProtocol == 'tor') checkedInstances = [...invidiousTorRedirectsChecks, ...invidiousTorCustomRedirects] + const i = checkedInstances.indexOf(protocolHost); + if (i !== -1) checkedInstances.splice(i, 1); + for (const to of checkedInstances) + utils.copyCookie('invidious', from, to, 'PREFS'); + } + resolve(true); + }) +} + +function setInvidiousCookies() { + return new Promise(async resolve => { + await init(); + if (disableYoutube || youtubeFrontend != 'invidious') { resolve(); return; } + let checkedInstances; + if (youtubeProtocol == 'normal') checkedInstances = [...invidiousNormalRedirectsChecks, ...invidiousNormalCustomRedirects] + else if (youtubeProtocol == 'tor') checkedInstances = [...invidiousTorRedirectsChecks, ...invidiousTorCustomRedirects] + for (const to of checkedInstances) + utils.getCookiesFromStorage('invidious', to, 'PREFS'); + resolve(); + }) +} + +function initPipedLocalStorage(test, url, tabId) { + return new Promise(async resolve => { + await init(); + if (disableYoutube || youtubeFrontend != 'piped') { resolve(); return; } + const protocolHost = utils.protocolHost(url); + if (![ + ...pipedNormalCustomRedirects, + ...pipedNormalRedirectsChecks, + ...pipedTorRedirectsChecks, + ...pipedTorCustomRedirects, + ].includes(protocolHost)) { resolve(); return; } + + if (!test) { + + browser.tabs.executeScript(tabId, { file: "/assets/javascripts/youtube/get_piped_preferences.js", runAt: "document_start" }); + + let checkedInstances; + if (youtubeProtocol == 'normal') checkedInstances = [...pipedNormalCustomRedirects, ...pipedNormalRedirectsChecks] + else if (youtubeProtocol == 'tor') checkedInstances = [...pipedTorRedirectsChecks, ...pipedTorCustomRedirects] + const i = checkedInstances.indexOf(protocolHost); + if (i !== -1) checkedInstances.splice(i, 1); + for (const to of checkedInstances) { + browser.tabs.create({ url: to }, + tab => browser.tabs.executeScript(tab.id, { file: "/assets/javascripts/youtube/set_piped_preferences.js", runAt: "document_start" })) + } + } + resolve(true); + }) +} + +function initPipedMaterialLocalStorage(test, url, tabId,) { + return new Promise(async resolve => { + await init(); + if (disableYoutube || youtubeFrontend != 'pipedMaterial') { resolve(); return; } + const protocolHost = utils.protocolHost(url); + if (![ + ...pipedMaterialNormalRedirectsChecks, + ...pipedMaterialNormalCustomRedirects, + ...pipedMaterialTorRedirectsChecks, + ...pipedMaterialTorCustomRedirects, + ].includes(protocolHost)) { resolve(); return; } + + if (!test) { + browser.tabs.executeScript(tabId, { file: "/assets/javascripts/youtube/get_pipedMaterial_preferences.js", runAt: "document_start" }); + + let checkedInstances; + if (youtubeProtocol == 'normal') checkedInstances = [...pipedMaterialNormalRedirectsChecks, ...pipedMaterialNormalCustomRedirects] + else if (youtubeProtocol == 'tor') checkedInstances = [...pipedMaterialTorRedirectsChecks, ...pipedMaterialTorCustomRedirects] + const i = checkedInstances.indexOf(protocolHost); + if (i !== -1) checkedInstances.splice(i, 1); + for (const to of checkedInstances) + browser.tabs.create( + { url: to }, + tab => browser.tabs.executeScript(tab.id, { file: "/assets/javascripts/youtube/set_pipedMaterial_preferences.js", runAt: "document_start" }) + ); + } + resolve(true); + }) +} + +function removeXFrameOptions(e) { + const url = new URL(e.url); + let protocolHost = utils.protocolHost(url); + const instances = all(); + if (!instances.includes(protocolHost) || e.type != 'sub_frame') return; + + let isChanged = false; + for (const i in e.responseHeaders) + if (e.responseHeaders[i].name == 'x-frame-options') { + e.responseHeaders.splice(i, 1); + isChanged = true; + } + if (isChanged) return { responseHeaders: e.responseHeaders }; +} + +export default { + setRedirects, + initPipedLocalStorage, + initPipedMaterialLocalStorage, + initInvidiousCookies, + setInvidiousCookies, + redirect, + reverse, + switchInstance, + initDefaults, + removeXFrameOptions, +}; |