BetterYTM.user.js 19 KB

123456789101112131415161718192021222324252627282930313233343536
  1. // ==UserScript==
  2. // @name BetterYTM
  3. // @homepageURL https://github.com/Sv443/BetterYTM#readme
  4. // @namespace https://github.com/Sv443/BetterYTM
  5. // @version 1.0.0
  6. // @description Improvements for YouTube Music
  7. // @description:de Verbesserungen für YouTube Music
  8. // @license MIT
  9. // @author Sv443
  10. // @copyright Sv443 <[email protected]>
  11. // @match https://music.youtube.com/*
  12. // @match https://www.youtube.com/*
  13. // @icon https://raw.githubusercontent.com/Sv443/BetterYTM/main/resources/icon/v2.1_200.png
  14. // @run-at document-start
  15. // @grant GM.getValue
  16. // @grant GM.setValue
  17. // @connect self
  18. // @connect youtube.com
  19. // @connect github.com
  20. // @connect githubusercontent.com
  21. // @downloadURL https://github.com/Sv443/BetterYTM/raw/main/BetterYTM.user.js
  22. // @updateURL https://github.com/Sv443/BetterYTM/raw/main/BetterYTM.user.js
  23. // ==/UserScript==
  24. /*
  25. ▄▄▄ ▄ ▄▄▄▄▄▄ ▄
  26. █ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
  27. █▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
  28. █▄▄▀ █▄▄ █▄▄ █▄▄ █▄▄ █ █ █ █ █
  29. Made with ❤️ by Sv443
  30. I welcome every contribution on GitHub! */
  31. /* Disclaimer: I am not affiliated with YouTube, Google, Alphabet, Genius or anyone else */
  32. /* C&D this 🖕 */
  33. !function(){"use strict";var e=function(e,t,n,o){return new(n||(n=Promise))((function(r,i){function l(e){try{c(o.next(e))}catch(e){i(e)}}function s(e){try{c(o.throw(e))}catch(e){i(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(l,s)}c((o=o.apply(e,t||[])).next())}))};e(void 0,void 0,void 0,(function*(){const t=!0,n=t?"develop":"main",o={arrowKeySupport:{desc:"Arrow keys skip forwards and backwards by 10 seconds",type:"toggle",default:!0},removeUpgradeTab:{desc:'Remove the "Upgrade" / YT Music Premium tab',type:"toggle",default:!0},switchBetweenSites:{desc:"Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",type:"toggle",default:!0},geniusLyrics:{desc:"Add a button to the media controls to open the current song's lyrics on genius.com in a new tab",type:"toggle",default:!0},lyricsButtonsOnSongQueue:{desc:'TODO: Add a lyrics button to each song in the queue ("up next" tab)',type:"toggle",default:!0},volumeSliderSize:{desc:"The width of the volume slider in pixels",type:"number",min:10,max:1e3,step:5,default:160},volumeSliderStep:{desc:"Volume slider sensitivity - the smaller this number, the finer the volume control",type:"slider",min:1,max:20,default:2},watermarkEnabled:{desc:"Show a BetterYTM watermark under the YTM logo",type:"toggle",default:!0}},r=Object.keys(o).reduce(((e,t)=>(e[t]=o[t].default,e)),{}),i=yield E(),l=Object.assign(Object.assign({},r),i);yield M(l);const s=50,c=150,d="https://api.sv443.net/geniurl",a=`${d}/search/top`,u=Object.freeze({name:GM.info.script.name,version:GM.info.script.version,namespace:GM.info.script.namespace});function m(){return e(this,void 0,void 0,(function*(){const r=k();t&&console.log(`BetterYTM: Initializing features for domain '${r}'`);try{if("ytm"===r&&(l.arrowKeySupport&&(document.addEventListener("keydown",p),t&&console.log("BetterYTM: Added key press listener")),l.removeUpgradeTab&&f(),l.watermarkEnabled&&function(){const e=document.createElement("span");e.id="betterytm-watermark",e.className="style-scope ytmusic-nav-bar",e.innerText=u.name,e.title="Open menu",e.addEventListener("click",(()=>function(){const e=document.querySelector("#betterytm-menu-bg");e.style.visibility="visible",e.style.display="block"}())),S(" #betterytm-watermark {\n font-size: 10px;\n display: inline-block;\n position: absolute;\n left: 45px;\n top: 46px;\n z-index: 10;\n color: white;\n text-decoration: none;\n cursor: pointer;\n }\n\n @media(max-width: 615px) {\n #betterytm-watermark {\n display: none;\n }\n }\n\n #betterytm-watermark:hover {\n text-decoration: underline;\n }","watermark"),T(document.querySelector("#left-content"),e),t&&console.log("BetterYTM: Added watermark element:",e)}(),l.geniusLyrics&&(yield v()),l.lyricsButtonsOnSongQueue&&(yield function(){return e(this,void 0,void 0,(function*(){}))}()),"number"==typeof l.volumeSliderSize&&function(){const{volumeSliderSize:e}=l;"number"!=typeof e||isNaN(Number(e))||S(`.volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {\n width: ${e}px !important;\n}`,"vol_slider_size")}(),document.querySelector("tp-yt-paper-slider#volume-slider").setAttribute("step",String(l.volumeSliderStep))),["ytm","yt"].includes(r)){l.switchBetweenSites&&function(e){document.addEventListener("keydown",(n=>{"F9"==n.key&&function(e){var n;try{let o;if("ytm"===e?o="music":"yt"===e&&(o="www"),!o)throw new TypeError(`Unrecognized domain '${e}'`);const{pathname:r,search:i,hash:l}=new URL(location.href),s=null!==(n=function(){var e;const t=k();try{return"ytm"===t?null!==(e=document.querySelector("#progress-bar").value)&&void 0!==e?e:null:"yt"===t?0:null}catch(e){return console.error("BetterYTM: Couldn't get video time due to error:",e),null}}())&&void 0!==n?n:0;t&&console.log(`BetterYTM: Found video time of ${s} seconds`);const c=`https://${o}.youtube.com${r}${i.includes("?")?`${i}&t=${s}`:`?t=${s}`}${l}`;console.info(`BetterYTM - switching to domain '${e}' at ${c}`),location.href=c}catch(e){console.error("BetterYTM: Error while switching site:",e)}}("yt"===e?"ytm":"yt")})),t&&console.log("BetterYTM: Initialized site switch listener")}(r);try{!function(){var r;const i=document.createElement("div");i.id="betterytm-menu-bg",i.title="Click here to close the menu",i.style.visibility="hidden",i.style.display="none",i.addEventListener("click",(e=>{"betterytm-menu-bg"===e.target.id&&y()}));const s=document.createElement("div");s.title="",s.id="betterytm-menu",s.style.borderRadius="15px",s.style.display="flex",s.style.flexDirection="column",s.style.justifyContent="space-between";const c=document.createElement("div");c.style.padding="8px 20px 15px 8px",c.style.display="flex",c.style.justifyContent="space-between",c.id="betterytm-menu-titlecont";const d=document.createElement("h2");d.id="betterytm-menu-title",d.innerText="BetterYTM - Configuration";const a=document.createElement("div");a.id="betterytm-menu-linkscont";const m=(e,t,n)=>{const o=document.createElement("a");o.className="betterytm-menu-link",o.rel="noopener noreferrer",o.target="_blank",o.href=t,o.title=n,o.style.marginLeft="10px";const r=document.createElement("img");r.className="betterytm-menu-img",r.src=e,r.style.width="32px",r.style.height="32px",o.appendChild(r),a.appendChild(o)};m(`https://raw.githubusercontent.com/Sv443/BetterYTM/${n}/resources/external/github.png`,u.namespace,`${u.name} on GitHub`),m(`https://raw.githubusercontent.com/Sv443/BetterYTM/${n}/resources/external/greasyfork.png`,"https://greasyfork.org/xyz",`${u.name} on GreasyFork`);const p=document.createElement("img");p.id="betterytm-menu-close",p.src=`https://raw.githubusercontent.com/Sv443/BetterYTM/${n}/resources/icon/close.png`,p.title="Click to close the menu",p.style.marginLeft="50px",p.style.width="32px",p.style.height="32px",p.addEventListener("click",y),a.appendChild(p),c.appendChild(d),c.appendChild(a);const g=document.createElement("div");g.id="betterytm-menu-opts",g.style.display="flex",g.style.flexDirection="column";const f=(n,o,r)=>e(this,void 0,void 0,(function*(){t&&console.info(`BetterYTM: Feature config changed, key '${n}' from value '${o}' to '${r}'`);const e=Object.assign({},yield E());e[n]=r,yield M(e),t&&console.log("BetterYTM: Saved feature config changes"),t&&console.log("#DEBUG",yield GM.getValue("betterytm-config"))})),h=Object.keys(l);for(const e of h){const t=o[e];if(!t)continue;const{desc:n,type:i,default:s}=t,c=null!==(r=null==t?void 0:t.step)&&void 0!==r?r:void 0,d=l[e]||s||void 0,a=document.createElement("div");a.id=`betterytm-ftconf-${e}`,a.style.display="flex",a.style.flexDirection="row",a.style.justifyContent="space-between",a.style.padding="8px 20px";{const e=document.createElement("span");e.style.display="inline-block",e.style.fontSize="15px",e.innerText=n,a.appendChild(e)}{let n="text";switch(i){case"toggle":n="checkbox";break;case"slider":n="range";break;case"number":n="number"}const o=`betterytm-ftconf-${e}-input`,r=document.createElement("span");r.style.display="inline-block",r.style.whiteSpace="nowrap";const l=document.createElement("input");l.id=o,l.style.marginRight="37px",l.type=n,"toggle"===i&&(l.style.marginLeft="5px"),void 0!==d&&(l.value=String(d)),"number"===i&&c&&(l.step=c),t.min&&t.max&&(l.min=t.min,l.max=t.max),"toggle"===i&&void 0!==d&&(l.checked=Boolean(d));const u=e=>String(e),m=e=>e?"On":"Off";let y;"slider"===i?(y=document.createElement("label"),y.classList.add("betterytm-ftconf-label"),y.style.marginRight="20px",y.style.fontSize="16px",y.htmlFor=o,y.innerText=u(d),l.addEventListener("change",(()=>{y&&(y.innerText=u(parseInt(l.value)))}))):"toggle"===i&&void 0!==d&&(y=document.createElement("label"),y.classList.add("betterytm-ftconf-label"),y.style.paddingLeft="10px",y.style.paddingRight="5px",y.style.fontSize="16px",y.htmlFor=o,y.innerText=m(Boolean(d)),l.addEventListener("change",(()=>{y&&(y.innerText=m(l.checked))}))),l.addEventListener("change",(({currentTarget:t})=>{const n=t;let o=parseInt(n.value);isNaN(o)&&(o=Number(n.value)),void 0!==d&&f(e,d,"toggle"!==i?o:n.checked)}));const p=document.createElement("button");p.innerText="Reset",p.addEventListener("click",(()=>{l["toggle"!==i?"value":"checked"]=s,y&&(y.innerText="toggle"===i?m(l.checked):u(parseInt(l.value))),void 0!==d&&f(e,d,s)})),y&&r.appendChild(y),r.appendChild(l),r.appendChild(p),a.appendChild(r)}g.appendChild(a)}const b=document.createElement("div");b.style.marginTop="20px",b.style.fontSize="17px",b.style.textDecoration="underline",b.style.padding="8px 20px",b.innerText="You need to reload the page to apply changes.";const v=document.createElement("button");v.style.marginLeft="20px",v.innerText="Reload now",v.title="Click to reload the page",v.addEventListener("click",(()=>location.reload())),b.appendChild(v),g.appendChild(b);const x=document.createElement("div");x.id="betterytm-menu-body",x.appendChild(c),x.appendChild(g);const w=document.createElement("div");w.style.display="flex",w.style.justifyContent="space-around",w.style.fontSize="1.15em",w.style.marginTop="10px",w.style.marginBottom="5px";const k=document.createElement("span");k.id="betterytm-menu-version",k.innerText=`v${u.version}`,w.appendChild(k),g.appendChild(w),s.appendChild(x),s.appendChild(w),i.appendChild(s),document.body.appendChild(i);t&&console.log("BetterYTM: Added menu elem:",i),S(" #betterytm-menu-bg {\n display: block;\n position: fixed;\n width: 100vw;\n height: 100vh;\n top: 0;\n left: 0;\n z-index: 15;\n background-color: rgba(0, 0, 0, 0.6);\n }\n\n #betterytm-menu {\n display: inline-block;\n position: fixed;\n width: 50vw;\n height: auto;\n min-height: 500px;\n left: 25vw;\n top: 25vh;\n z-index: 16;\n overflow: auto;\n padding: 8px;\n color: #fff;\n background-color: #212121;\n }\n\n #betterytm-menu-titlecont {\n display: flex;\n }\n\n #betterytm-menu-title {\n font-size: 20px;\n margin-top: 5px;\n margin-bottom: 8px;\n }\n\n #betterytm-menu-linkscont {\n display: flex;\n }\n\n .betterytm-menu-link {\n display: inline-block;\n }\n\n /*.betterytm-menu-img {\n\n }*/\n\n #betterytm-menu-close {\n cursor: pointer;\n }\n\n .betterytm-ftconf-label {\n user-select: none;\n }\n ","menu")}()}catch(e){console.error("BetterYTM: Couldn't add menu:",e)}}}catch(e){console.error("BetterYTM: General error while executing feature:",e)}}))}function y(){const e=document.querySelector("#betterytm-menu-bg");e.style.visibility="hidden",e.style.display="none"}function p(e){var n,o;if(["ArrowLeft","ArrowRight"].includes(e.code)){if(["INPUT","TEXTAREA","SELECT"].includes(null!==(o=null===(n=document.activeElement)||void 0===n?void 0:n.tagName)&&void 0!==o?o:"_"))return t&&console.info(`BetterYTM: Captured valid key but the current active element is <${document.activeElement.tagName.toLowerCase()}>, so the keypress is ignored`);t&&console.log(`BetterYTM: Captured key '${e.code}' in proxy listener`);const r={altKey:!1,ctrlKey:!1,metaKey:!1,shiftKey:!1,target:document.body,currentTarget:document.body,originalTarget:document.body,explicitOriginalTarget:document.body,srcElement:document.body,type:"keydown",bubbles:!0,cancelBubble:!1,cancelable:!0,isTrusted:!0,repeat:!1};let i=!1,l={};switch(e.code){case"ArrowLeft":l={code:"KeyH",key:"h",keyCode:72,which:72};break;case"ArrowRight":l={code:"KeyL",key:"l",keyCode:76,which:76};break;default:i=!0}if(i)t&&console.warn(`BetterYTM: Captured key '${e.code}' has no defined behavior`);else{const n=Object.assign(Object.assign({code:""},r),l);document.body.dispatchEvent(new KeyboardEvent("keydown",n)),t&&console.log(`BetterYTM: Dispatched proxy keydown event: [${e.code}] -> [${n.code}]`)}}}let g=0;function f(){const e=document.querySelector('.ytmusic-nav-bar ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]');e?(e.remove(),t&&console.log(`BetterYTM: Removed upgrade tab after ${g} tries`)):g<s?(setTimeout(f,c),g++):console.error(`BetterYTM: Couldn't find upgrade tab to remove after ${g} tries`)}let h="",b=0;function v(){return e(this,void 0,void 0,(function*(){const n=document.querySelector(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer");if(!n)return b++,b<s?setTimeout(v,c):console.error(`BetterYTM: Couldn't find element to append lyrics buttons to after ${b} tries`);const o=document.querySelector(".content-info-wrapper > yt-formatted-string"),r=yield x(),i=document.createElement("a");i.id="betterytm-lyrics-button",i.className="ytmusic-player-bar",i.title=r?"Click to open this song's lyrics in a new tab":"Loading...",r&&(i.href=r),i.target="_blank",i.rel="noopener noreferrer",i.style.visibility=r?"initial":"hidden",i.style.display=r?"inline-flex":"none",S(" #betterytm-lyrics-button {\n align-items: center;\n justify-content: center;\n position: relative;\n vertical-align: middle;\n\n margin-left: 8px;\n width: 40px;\n height: 40px;\n border-radius: 100%;\n background-color: transparent;\n }\n\n #betterytm-lyrics-button:hover {\n background-color: #383838;\n }\n\n #betterytm-lyrics-img {\n display: inline-block;\n z-index: 10;\n width: 24px;\n height: 24px;\n padding: 5px;\n }","lyrics");const l=document.createElement("img");l.id="betterytm-lyrics-img",l.src="https://raw.githubusercontent.com/Sv443/BetterYTM/main/resources/external/genius.png",i.appendChild(l),t&&console.log(`BetterYTM: Inserted genius button after ${b} tries:`,i),T(n,i),h=o.title,new MutationObserver((n=>{var o,r,i;return e(this,void 0,void 0,(function*(){var e,l,s,c;try{for(o=!0,r=function(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e="function"==typeof __values?__values(e):e[Symbol.iterator](),t={},o("next"),o("throw"),o("return"),t[Symbol.asyncIterator]=function(){return this},t);function o(n){t[n]=e[n]&&function(t){return new Promise((function(o,r){!function(e,t,n,o){Promise.resolve(o).then((function(t){e({value:t,done:n})}),t)}(o,r,(t=e[n](t)).done,t.value)}))}}}(n);!(e=(i=yield r.next()).done);){c=i.value,o=!1;try{const e=c.target.title;if(e!=h&&e.length>0){const n=document.querySelector("#betterytm-lyrics-button");if(!n)return;t&&console.log(`BetterYTM: Song title changed from '${h}' to '${e}'`),n.style.cursor="wait",n.style.pointerEvents="none",h=e;const o=yield x();if(!o)continue;n.href=o,n.title="Click to open this song's lyrics in a new tab",n.style.cursor="pointer",n.style.visibility="initial",n.style.display="inline-flex",n.style.pointerEvents="initial"}}finally{o=!0}}}catch(e){l={error:e}}finally{try{o||e||!(s=r.return)||(yield s.call(r))}finally{if(l)throw l.error}}}))})).observe(o,{attributes:!0,attributeFilter:["title"]})}))}function x(){var t,n;return e(this,void 0,void 0,(function*(){try{const o="string"==typeof(null===(t=document.querySelector("ytmusic-player"))||void 0===t?void 0:t.getAttribute("video-mode_")),r=document.querySelector(".content-info-wrapper > yt-formatted-string"),i=document.querySelector("span.subtitle > yt-formatted-string:first-child");if(!r||!i||!r.title)return null;const l=e=>e.replace(/\(.+\)/gim,"").replace(/\[.+\]/gim,"").trim(),s=e=>((e=e.split(/\s*\u2022\s*/gimu)[0]).match(/&/)&&(e=e.split(/\s*&\s*/gm)[0]),e.match(/,/)&&(e=e.split(/,\s*/gm)[0]),e.trim()),c=l(r.title),d=s(i.title),a=()=>e(this,void 0,void 0,(function*(){if(!c.includes("-"))return yield w(d,c);const[e,...t]=c.split("-").map((e=>e.trim()));return yield w(e,t.join(" "))}));return o?yield a():null!==(n=yield w(d,c))&&void 0!==n?n:yield a()}catch(e){return console.error("BetterYTM: Couldn't resolve genius.com URL:",e),null}}))}function w(n,o){return e(this,void 0,void 0,(function*(){try{const e=`${a}?artist=${encodeURIComponent(n)}&song=${encodeURIComponent(o)}`;t&&console.log(`BetterYTM: Requesting URL from geniURL at '${e}'`);const r=yield(yield fetch(e)).json();if(r.error)return void console.error("BetterYTM: Couldn't fetch genius.com URL:",r.message);const i=r.url;return t&&console.info(`BetterYTM: Found genius URL: ${i}`),i}catch(e){return void console.error("BetterYTM: Couldn't get genius URL due to error:",e)}}))}function k(){const{hostname:e}=new URL(location.href);if(e.includes("music.youtube"))return"ytm";if(e.includes("youtube"))return"yt";throw new Error("BetterYTM is running on an unexpected website")}function T(e,t){var n;return null===(n=e.parentNode)||void 0===n||n.insertBefore(t,e.nextSibling),t}function S(e,n){"string"==typeof n&&0!==n.length||(n=String(Math.floor(1e4*Math.random())));const o=document.createElement("style");o.id=`betterytm-style-${n}`,o.innerHTML=e,document.querySelector("head").appendChild(o),t&&console.log(`BetterYTM: Inserted global style with ref '${n}':`,o)}function E(){return e(this,void 0,void 0,(function*(){const e=Object.freeze(Object.assign({},r));try{const t=yield GM.getValue("betterytm-config");return"string"!=typeof t?(yield C(),e):Object.freeze(t?JSON.parse(t):{})}catch(t){return yield C(),e}}))}function M(e){if(!e||"object"!=typeof e)throw new TypeError("Feature config not provided or invalid");return GM.setValue("betterytm-config",JSON.stringify(e))}function C(){return GM.setValue("betterytm-config",JSON.stringify(r))}!function(){try{console.log(`${u.name} v${u.version} - ${u.namespace}`),console.log(`Powered by lots of ambition and my song metadata API called geniURL: ${d}`),document.addEventListener("DOMContentLoaded",m)}catch(e){console.error("BetterYTM - General Error:",e)}}()}))}();