// ==UserScript==
// @name BetterYTM
// @homepageURL https://github.com/Sv443/BetterYTM#readme
// @namespace https://github.com/Sv443/BetterYTM
// @version 1.0.0
// @description Configurable layout and UX improvements for YouTube Music
// @description:de Konfigurierbares Layout und UX-Verbesserungen für YouTube Music
// @license MIT
// @author Sv443
// @copyright Sv443 (https://github.com/Sv443)
// @icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon_48.png
// @match https://music.youtube.com/*
// @match https://www.youtube.com/*
// @run-at document-start
// @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
// @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
// @connect api.sv443.net
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.getResourceUrl
// @grant unsafeWindow
// @noframes
// @resource icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon_48.png
// @resource close https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/close.png
// @resource delete https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/delete.svg
// @resource error https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/error.svg
// @resource lyrics https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/lyrics.svg
// @resource spinner https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/spinner.svg
// @resource arrow_down https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/arrow_down.svg
// @resource github https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/github.png
// @resource greasyfork https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/greasyfork.png
// @grant GM.deleteValue
// @grant GM.registerMenuCommand
// @grant GM.listValues
// ==/UserScript==
/*
▄▄▄ ▄ ▄▄▄▄▄▄ ▄
█ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
█▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
█▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
Made with ❤️ by Sv443
I welcome every contribution on GitHub!
https://github.com/Sv443/BetterYTM
*/
/* Disclaimer: I am not affiliated with YouTube, Google, Alphabet, Genius or anyone else */
/* C&D this 🖕 */
/******/ var __webpack_modules__ = ({
/***/ "./changelog.md":
/*!**********************!*\
!*** ./changelog.md ***!
\**********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// Module
var code = "
BetterYTM Changelog
\n
\n\nHistory:
\n\n
\n
\n1.0.0
\nTODO:
\n\n- Added menu to configure features
\n- New configurable features:
\n- Make volume slider bigger
\n- Choose step of volume slider for finer control
\n- Add lyrics button to each song in a playlist
\n
\n \n- Changes / Fixes:
\n- Now the lyrics button will directly link to the lyrics (using my API geniURL)
\n- Site switch with F9 will now keep the video time
\n
\n \n
\n
\n\n0.2.0
\n\n- Added Features:
\n- Switch between YouTube and YT Music (with F9 by default)
\n- Search for song lyrics with new button in media controls
\n- Remove "Upgrade to YTM Premium" tab
\n
\n \n
\n
\n\n0.1.0
\n\n- Added support for arrow keys to skip forward or backward (currently only by fixed 10 second interval)
\n
\n";
// Exports
/* harmony default export */ __webpack_exports__["default"] = (code);
/***/ }),
/***/ "./src/features/menu/menu.html":
/*!*************************************!*\
!*** ./src/features/menu/menu.html ***!
\*************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// Module
var code = "\n";
// Exports
/* harmony default export */ __webpack_exports__["default"] = (code);
/***/ }),
/***/ "./src/features/layout.css":
/*!*********************************!*\
!*** ./src/features/layout.css ***!
\*********************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// extracted by mini-css-extract-plugin
/***/ }),
/***/ "./src/features/menu/menu.css":
/*!************************************!*\
!*** ./src/features/menu/menu.css ***!
\************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// extracted by mini-css-extract-plugin
/***/ }),
/***/ "./src/features/menu/menu_old.css":
/*!****************************************!*\
!*** ./src/features/menu/menu_old.css ***!
\****************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// extracted by mini-css-extract-plugin
/***/ }),
/***/ "./src/config.ts":
/*!***********************!*\
!*** ./src/config.ts ***!
\***********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ clearConfig: function() { return /* binding */ clearConfig; },
/* harmony export */ defaultConfig: function() { return /* binding */ defaultConfig; },
/* harmony export */ getFeatures: function() { return /* binding */ getFeatures; },
/* harmony export */ initConfig: function() { return /* binding */ initConfig; },
/* harmony export */ saveFeatures: function() { return /* binding */ saveFeatures; },
/* harmony export */ setDefaultFeatures: function() { return /* binding */ setDefaultFeatures; }
/* harmony export */ });
/* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "./node_modules/@sv443-network/userutils/dist/index.mjs");
/* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/** If this number is incremented, the features object data will be migrated to the new format */
const formatVersion = 3;
const migrations = {
// 1 -> 2
2: (oldData) => {
const queueBtnsEnabled = Boolean(oldData.queueButtons);
delete oldData.queueButtons;
return Object.assign(Object.assign({}, oldData), { deleteFromQueueButton: queueBtnsEnabled, lyricsQueueButton: queueBtnsEnabled });
},
// 2 -> 3
3: (oldData) => (Object.assign(Object.assign({}, oldData), { removeShareTrackingParam: true, numKeysSkipToTime: true })),
};
const defaultConfig = Object.keys(_features_index__WEBPACK_IMPORTED_MODULE_1__.featInfo)
.reduce((acc, key) => {
acc[key] = _features_index__WEBPACK_IMPORTED_MODULE_1__.featInfo[key].default;
return acc;
}, {});
const cfgMgr = new _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.ConfigManager({
id: "bytm-config",
formatVersion,
defaultConfig,
migrations,
});
/** Initializes the ConfigManager instance and loads persistent data into memory */
function initConfig() {
return __awaiter(this, void 0, void 0, function* () {
const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${cfgMgr.id}`, NaN));
const data = yield cfgMgr.loadData();
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Initialized ConfigManager (format version = ${cfgMgr.formatVersion})`);
if (isNaN(oldFmtVer))
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.info)("Config data initialized with default values");
else if (oldFmtVer !== cfgMgr.formatVersion)
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.info)(`Config data migrated from version ${oldFmtVer} to ${cfgMgr.formatVersion}`);
return data;
});
}
/** Returns the current feature config from the in-memory cache */
function getFeatures() {
return cfgMgr.getData();
}
/** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
function saveFeatures(featureConf) {
return __awaiter(this, void 0, void 0, function* () {
yield cfgMgr.setData(featureConf);
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.info)("Saved new feature config:", featureConf);
});
}
/** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
function setDefaultFeatures() {
return __awaiter(this, void 0, void 0, function* () {
yield cfgMgr.saveDefaultData();
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.info)("Reset feature config to its default values");
});
}
/** Clears the feature config from the persistent storage */
function clearConfig() {
return __awaiter(this, void 0, void 0, function* () {
yield cfgMgr.deleteConfig();
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.info)("Deleted config from persistent storage");
});
}
/***/ }),
/***/ "./src/constants.ts":
/*!**************************!*\
!*** ./src/constants.ts ***!
\**************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ branch: function() { return /* binding */ branch; },
/* harmony export */ logLevel: function() { return /* binding */ logLevel; },
/* harmony export */ mode: function() { return /* binding */ mode; },
/* harmony export */ scriptInfo: function() { return /* binding */ scriptInfo; }
/* harmony export */ });
const modeRaw = "development";
const branchRaw = "develop";
/** The mode in which the script was built (production or development) */
const mode = (modeRaw.match(/^{{.+}}$/) ? "production" : modeRaw);
/** The branch to use in various URLs that point to the GitHub repo */
const branch = (branchRaw.match(/^{{.+}}$/) ? "main" : branchRaw);
/**
* How much info should be logged to the devtools console
* 0 = Debug (show everything) or 1 = Info (show only important stuff)
*/
const logLevel = mode === "production" ? 1 : 0;
/** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
const scriptInfo = {
name: GM.info.script.name,
version: GM.info.script.version,
namespace: GM.info.script.namespace,
lastCommit: "41a5a2c", // assert as generic string instead of literal
};
/***/ }),
/***/ "./src/events.ts":
/*!***********************!*\
!*** ./src/events.ts ***!
\***********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ initSiteEvents: function() { return /* binding */ initSiteEvents; },
/* harmony export */ removeAllObservers: function() { return /* binding */ removeAllObservers; },
/* harmony export */ siteEvents: function() { return /* binding */ siteEvents; }
/* harmony export */ });
/* harmony import */ var nanoevents__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! nanoevents */ "./node_modules/nanoevents/index.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/** EventEmitter instance that is used to detect changes to the site */
const siteEvents = (0,nanoevents__WEBPACK_IMPORTED_MODULE_1__.createNanoEvents)();
let observers = [];
/** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
function removeAllObservers() {
observers.forEach((observer, i) => {
observer.disconnect();
delete observers[i];
});
observers = [];
}
/** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
function initSiteEvents() {
return __awaiter(this, void 0, void 0, function* () {
try {
//#SECTION queue
// the queue container always exists so it doesn't need the extra init function
const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
if (addedNodes.length > 0 || removedNodes.length > 0) {
(0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
siteEvents.emit("queueChanged", target);
}
});
// only observe added or removed elements
queueObs.observe(document.querySelector(".side-panel.modular #contents.ytmusic-player-queue"), {
childList: true,
});
const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
if (addedNodes.length > 0 || removedNodes.length > 0) {
(0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
siteEvents.emit("autoplayQueueChanged", target);
}
});
autoplayObs.observe(document.querySelector(".side-panel.modular ytmusic-player-queue #automix-contents"), {
childList: true,
});
//#SECTION home page observers
initHomeObservers();
(0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)("Successfully initialized SiteEvents observers");
observers = observers.concat([
queueObs,
autoplayObs,
]);
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_0__.error)("Couldn't initialize SiteEvents observers due to an error:\n", err);
}
});
}
/**
* The home page might not exist yet if the site was accessed through any path like /watch directly.
* This function will keep waiting for when the home page exists, then create the necessary MutationObservers.
*/
function initHomeObservers() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let interval;
// hidden="" attribute is only present if the content of the page doesn't exist yet
// so this pauses execution until that attribute is removed
if ((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden")) {
yield new Promise((res) => {
interval = setInterval(() => {
var _a;
if (!((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden"))) {
clearInterval(interval);
res();
}
}, 50);
});
}
siteEvents.emit("homePageLoaded");
(0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)("Initialized home page observers");
//#SECTION carousel shelves
const shelfContainerObs = new MutationObserver(([{ addedNodes, removedNodes }]) => {
if (addedNodes.length > 0 || removedNodes.length > 0) {
(0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)("Detected carousel shelf container change - added nodes:", addedNodes.length, "- removed nodes:", removedNodes.length);
siteEvents.emit("carouselShelvesChanged", { addedNodes, removedNodes });
}
});
shelfContainerObs.observe(document.querySelector("#contents.ytmusic-section-list-renderer"), {
childList: true,
});
observers.push(shelfContainerObs);
});
}
/***/ }),
/***/ "./src/features/index.ts":
/*!*******************************!*\
!*** ./src/features/index.ts ***!
\*******************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ addAnchorImprovements: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addAnchorImprovements; },
/* harmony export */ addConfigMenuOption: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addConfigMenuOption; },
/* harmony export */ addLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addLyricsCacheEntry; },
/* harmony export */ addMediaCtrlLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addMediaCtrlLyricsBtn; },
/* harmony export */ addMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.addMenu; },
/* harmony export */ addWatermark: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addWatermark; },
/* harmony export */ categoryNames: function() { return /* binding */ categoryNames; },
/* harmony export */ closeMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.closeMenu; },
/* harmony export */ createLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.createLyricsBtn; },
/* harmony export */ disableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.disableBeforeUnload; },
/* harmony export */ enableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.enableBeforeUnload; },
/* harmony export */ featInfo: function() { return /* binding */ featInfo; },
/* harmony export */ geniUrlBase: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.geniUrlBase; },
/* harmony export */ getCurrentLyricsUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getCurrentLyricsUrl; },
/* harmony export */ getGeniusUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getGeniusUrl; },
/* harmony export */ getLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getLyricsCacheEntry; },
/* harmony export */ initArrowKeySkip: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initArrowKeySkip; },
/* harmony export */ initAutoCloseToasts: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initAutoCloseToasts; },
/* harmony export */ initBeforeUnloadHook: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initBeforeUnloadHook; },
/* harmony export */ initMenu: function() { return /* reexport safe */ _menu_menu__WEBPACK_IMPORTED_MODULE_4__.initMenu; },
/* harmony export */ initNumKeysSkip: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initNumKeysSkip; },
/* harmony export */ initQueueButtons: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initQueueButtons; },
/* harmony export */ initSiteSwitch: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initSiteSwitch; },
/* harmony export */ initVolumeFeatures: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initVolumeFeatures; },
/* harmony export */ isMenuOpen: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.isMenuOpen; },
/* harmony export */ openMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu; },
/* harmony export */ preInitLayout: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.preInitLayout; },
/* harmony export */ removeShareTrackingParam: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.removeShareTrackingParam; },
/* harmony export */ removeUpgradeTab: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.removeUpgradeTab; },
/* harmony export */ sanitizeArtists: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeArtists; },
/* harmony export */ sanitizeSong: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeSong; },
/* harmony export */ splitVideoTitle: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.splitVideoTitle; }
/* harmony export */ });
/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
/* harmony import */ var _input__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./input */ "./src/features/input.ts");
/* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./layout */ "./src/features/layout.ts");
/* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
/* harmony import */ var _menu_menu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu/menu */ "./src/features/menu/menu.ts");
/* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
/** Mapping of feature category identifiers to readable strings */
const categoryNames = {
input: "Input",
layout: "Layout",
lyrics: "Lyrics",
};
/** Contains all possible features with their default values and other configuration */
const featInfo = {
//#SECTION layout
removeUpgradeTab: {
desc: "Remove the Upgrade / Premium tab",
type: "toggle",
category: "layout",
default: true,
},
volumeSliderLabel: {
desc: "Add a percentage label next to the volume slider",
type: "toggle",
category: "layout",
default: true,
},
volumeSliderSize: {
desc: "The width of the volume slider in pixels",
type: "number",
category: "layout",
min: 50,
max: 500,
step: 5,
default: 150,
unit: "px",
},
volumeSliderStep: {
desc: "Volume slider sensitivity (by how little percent the volume can be changed at a time)",
type: "slider",
category: "layout",
min: 1,
max: 25,
default: 2,
unit: "%",
},
watermarkEnabled: {
desc: `Show a ${_constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name} watermark under the site logo that opens this config menu`,
type: "toggle",
category: "layout",
default: true,
},
deleteFromQueueButton: {
desc: "Add a button to each song in the queue to quickly remove it",
type: "toggle",
category: "layout",
default: true,
},
closeToastsTimeout: {
desc: "After how many seconds to close permanent notifications - 0 to only close them manually (default behavior)",
type: "number",
category: "layout",
min: 0,
max: 30,
step: 0.5,
default: 0,
unit: "s",
},
removeShareTrackingParam: {
desc: "Remove the tracking parameter (&si=...) from links in the share popup",
type: "toggle",
category: "layout",
default: true,
},
//#SECTION input
arrowKeySupport: {
desc: "Use arrow keys to skip forwards and backwards by 10 seconds",
type: "toggle",
category: "input",
default: true,
},
switchBetweenSites: {
desc: "Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",
type: "toggle",
category: "input",
default: true,
},
switchSitesHotkey: {
hidden: true,
desc: "TODO(v1.1): Which hotkey needs to be pressed to switch sites?",
type: "hotkey",
category: "input",
default: {
key: "F9",
shift: false,
ctrl: false,
meta: false,
},
},
disableBeforeUnloadPopup: {
desc: "Prevent the confirmation popup that appears when trying to leave the site while a song is playing",
type: "toggle",
category: "input",
default: false,
},
anchorImprovements: {
desc: "TODO:FIXME: Add link elements all over the page so things can be opened in a new tab easier",
type: "toggle",
category: "input",
default: true,
},
numKeysSkipToTime: {
desc: "Enable skipping to a specific time in the video by pressing a number key (0-9)",
type: "toggle",
category: "input",
default: true,
},
//#SECTION lyrics
geniusLyrics: {
desc: "Add a button to the media controls of the currently playing song to open its lyrics on genius.com",
type: "toggle",
category: "lyrics",
default: true,
},
lyricsQueueButton: {
desc: "Add a button to each song in the queue to quickly open its lyrics page",
type: "toggle",
category: "lyrics",
default: true,
},
};
/***/ }),
/***/ "./src/features/input.ts":
/*!*******************************!*\
!*** ./src/features/input.ts ***!
\*******************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ disableBeforeUnload: function() { return /* binding */ disableBeforeUnload; },
/* harmony export */ enableBeforeUnload: function() { return /* binding */ enableBeforeUnload; },
/* harmony export */ initArrowKeySkip: function() { return /* binding */ initArrowKeySkip; },
/* harmony export */ initBeforeUnloadHook: function() { return /* binding */ initBeforeUnloadHook; },
/* harmony export */ initNumKeysSkip: function() { return /* binding */ initNumKeysSkip; },
/* harmony export */ initSiteSwitch: function() { return /* binding */ initSiteSwitch; }
/* harmony export */ });
/* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "./node_modules/@sv443-network/userutils/dist/index.mjs");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
/* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
//#MARKER arrow key skip
function initArrowKeySkip() {
document.addEventListener("keydown", (evt) => {
var _a, _b, _c;
if (!["ArrowLeft", "ArrowRight"].includes(evt.code))
return;
// discard the event when a (text) input is currently active, like when editing a playlist
if (["INPUT", "TEXTAREA", "SELECT"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : "_"))
return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Captured valid key to skip forward or backward but the current active element is <${(_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName.toLowerCase()}>, so the keypress is ignored`);
onArrowKeyPress(evt);
});
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Added arrow key press listener");
}
/** Called when the user presses any key, anywhere */
function onArrowKeyPress(evt) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Captured key '${evt.code}' in proxy listener`);
// ripped this stuff from the console, most of these are probably unnecessary but this was finnicky af and I am sick and tired of trial and error
const defaultProps = {
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
target: document.body,
currentTarget: document.body,
originalTarget: document.body,
explicitOriginalTarget: document.body,
srcElement: document.body,
type: "keydown",
bubbles: true,
cancelBubble: false,
cancelable: true,
isTrusted: true,
repeat: false,
// needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
view: (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
};
let invalidKey = false;
let keyProps = {};
switch (evt.code) {
case "ArrowLeft":
keyProps = {
code: "KeyH",
key: "h",
keyCode: 72,
which: 72,
};
break;
case "ArrowRight":
keyProps = {
code: "KeyL",
key: "l",
keyCode: 76,
which: 76,
};
break;
default:
invalidKey = true;
break;
}
if (!invalidKey) {
const proxyProps = Object.assign(Object.assign({ code: "" }, defaultProps), keyProps);
document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
}
else
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.warn)(`Captured key '${evt.code}' has no defined behavior`);
}
//#MARKER site switch
/** switch sites only if current video time is greater than this value */
const videoTimeThreshold = 3;
/** Initializes the site switch feature */
function initSiteSwitch(domain) {
document.addEventListener("keydown", (e) => {
if (e.key === "F9")
switchSite(domain === "yt" ? "ytm" : "yt");
});
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Initialized site switch listener");
}
/** Switches to the other site (between YT and YTM) */
function switchSite(newDomain) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (newDomain === "ytm" && !location.href.includes("/watch"))
return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.warn)("Not on a video page, so the site switch is ignored");
let subdomain;
if (newDomain === "ytm")
subdomain = "music";
else if (newDomain === "yt")
subdomain = "www";
if (!subdomain)
throw new Error(`Unrecognized domain '${newDomain}'`);
disableBeforeUnload();
const { pathname, search, hash } = new URL(location.href);
const vt = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getVideoTime)();
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Found video time of ${vt} seconds`);
const cleanSearch = search.split("&")
.filter((param) => !param.match(/^\??t=/))
.join("&");
const newSearch = typeof vt === "number" && vt > videoTimeThreshold ?
cleanSearch.includes("?")
? `${cleanSearch.startsWith("?")
? cleanSearch
: "?" + cleanSearch}&t=${vt - 1}`
: `?t=${vt - 1}`
: cleanSearch;
const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Switching to domain '${newDomain}' at ${newUrl}`);
location.assign(newUrl);
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Error while switching site:", err);
}
});
}
//#MARKER beforeunload popup
let beforeUnloadEnabled = true;
/** Disables the popup before leaving the site */
function disableBeforeUnload() {
beforeUnloadEnabled = false;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Disabled popup before leaving the site");
}
/** (Re-)enables the popup before leaving the site */
function enableBeforeUnload() {
beforeUnloadEnabled = true;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Enabled popup before leaving the site");
}
/**
* Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
* event listeners before they can be called by the site.
*/
function initBeforeUnloadHook() {
Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
(function (original) {
// @ts-ignore
window.__proto__.addEventListener = function (...args) {
const origListener = typeof args[1] === "function" ? args[1] : args[1].handleEvent;
args[1] = function (...a) {
if (!beforeUnloadEnabled && args[0] === "beforeunload")
return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Prevented beforeunload event listener from being called");
else
return origListener.apply(this, a);
};
original.apply(this, args);
};
// @ts-ignore
})(window.__proto__.addEventListener);
}
//#MARKER number keys skip to time
/** Adds the ability to skip to a certain time in the video by pressing a number key (0-9) */
function initNumKeysSkip() {
document.addEventListener("keydown", (e) => {
var _a, _b, _c, _d;
if (!e.key.trim().match(/^[0-9]$/))
return;
if (_menu_menu_old__WEBPACK_IMPORTED_MODULE_2__.isMenuOpen)
return;
// discard the event when a (text) input is currently active, like when editing a playlist or when the search bar is focused
if (document.activeElement !== document.body
&& !["progress-bar"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : "_")
&& !["BUTTON", "A"].includes((_d = (_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName) !== null && _d !== void 0 ? _d : "_"))
return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Captured valid key to skip video to but an unexpected element is focused, so the keypress is ignored");
skipToTimeKey(Number(e.key));
});
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Added number key press listener");
}
/** Emulates a click on the video progress bar at the position calculated from the passed time key (0-9) */
function skipToTimeKey(key) {
const getX = (timeKey, maxWidth) => {
if (timeKey >= 10)
return maxWidth;
return Math.floor((maxWidth / 10) * timeKey);
};
/** Calculate offsets of the bounding client rect of the passed element - see https://stackoverflow.com/a/442474/11187044 */
const getOffsetRect = (elem) => {
let left = 0;
let top = 0;
const rect = elem.getBoundingClientRect();
while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
left += elem.offsetLeft - elem.scrollLeft;
top += elem.offsetTop - elem.scrollTop;
elem = elem.offsetParent;
}
return {
top,
left,
width: rect.width,
height: rect.height,
};
};
// not technically a progress element but behaves pretty much the same
const progressElem = document.querySelector("tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar");
if (!progressElem)
return;
const rect = getOffsetRect(progressElem);
const x = getX(key, rect.width);
const y = rect.top - rect.height / 2;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Skipping to time key ${key} (x offset: ${x}px of ${rect.width}px)`);
const evt = new MouseEvent("mousedown", {
clientX: x,
clientY: y,
// @ts-ignore
layerX: x,
layerY: rect.height / 2,
target: progressElem,
bubbles: true,
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false,
button: 0,
buttons: 1,
which: 1,
isTrusted: true,
offsetX: 0,
offsetY: 0,
// needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
view: (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
});
progressElem.dispatchEvent(evt);
}
/***/ }),
/***/ "./src/features/layout.ts":
/*!********************************!*\
!*** ./src/features/layout.ts ***!
\********************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ addAnchorImprovements: function() { return /* binding */ addAnchorImprovements; },
/* harmony export */ addConfigMenuOption: function() { return /* binding */ addConfigMenuOption; },
/* harmony export */ addWatermark: function() { return /* binding */ addWatermark; },
/* harmony export */ initAutoCloseToasts: function() { return /* binding */ initAutoCloseToasts; },
/* harmony export */ initQueueButtons: function() { return /* binding */ initQueueButtons; },
/* harmony export */ initVolumeFeatures: function() { return /* binding */ initVolumeFeatures; },
/* harmony export */ preInitLayout: function() { return /* binding */ preInitLayout; },
/* harmony export */ removeShareTrackingParam: function() { return /* binding */ removeShareTrackingParam; },
/* harmony export */ removeUpgradeTab: function() { return /* binding */ removeUpgradeTab; }
/* harmony export */ });
/* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "./node_modules/@sv443-network/userutils/dist/index.mjs");
/* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
/* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../events */ "./src/events.ts");
/* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
/* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./index */ "./src/features/index.ts");
/* harmony import */ var _layout_css__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./layout.css */ "./src/features/layout.css");
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
let features;
function preInitLayout(feats) {
features = feats;
}
//#MARKER BYTM-Config buttons
let menuOpenAmt = 0, logoExchanged = false;
/** Adds a watermark beneath the logo */
function addWatermark() {
const watermark = document.createElement("a");
watermark.role = "button";
watermark.id = "bytm-watermark";
watermark.className = "style-scope ytmusic-nav-bar bytm-no-select";
watermark.innerText = _constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name;
watermark.title = "Open menu";
watermark.tabIndex = 1000;
improveLogo();
watermark.addEventListener("click", (e) => {
e.stopPropagation();
menuOpenAmt++;
if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
(0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_4__.openMenu)();
if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
exchangeLogo();
});
// when using the tab key to navigate
watermark.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.stopPropagation();
menuOpenAmt++;
if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
(0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_4__.openMenu)();
if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
exchangeLogo();
}
});
const logoElem = document.querySelector("#left-content");
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.insertAfter)(logoElem, watermark);
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Added watermark element");
}
/** Turns the regular `
`-based logo into inline SVG to be able to animate and modify parts of it */
function improveLogo() {
return __awaiter(this, void 0, void 0, function* () {
try {
const res = yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.fetchAdvanced)("https://music.youtube.com/img/on_platform_logo_dark.svg");
const svg = yield res.text();
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-logo a", {
listener: (logoElem) => {
var _a;
logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
logoElem.innerHTML = svg;
logoElem.querySelectorAll("ellipse").forEach((e) => {
e.classList.add("bytm-mod-logo-ellipse");
});
(_a = logoElem.querySelector("path")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-mod-logo-path");
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Swapped logo to inline SVG");
},
});
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't improve logo due to an error:", err);
}
});
}
/** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
function exchangeLogo() {
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(".bytm-mod-logo", {
listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
if (logoElem.classList.contains("bytm-logo-exchanged"))
return;
logoExchanged = true;
logoElem.classList.add("bytm-logo-exchanged");
const iconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getResourceUrl)("icon");
const newLogo = document.createElement("img");
newLogo.className = "bytm-mod-logo-img";
newLogo.src = iconUrl;
logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
document.head.querySelectorAll("link[rel=\"icon\"]").forEach((e) => {
e.href = iconUrl;
});
setTimeout(() => {
logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
}, 1000);
}),
});
}
/** Called whenever the menu exists to add a BYTM-Configuration button to the user menu popover */
function addConfigMenuOption(container) {
return __awaiter(this, void 0, void 0, function* () {
const cfgOptElem = document.createElement("div");
cfgOptElem.role = "button";
cfgOptElem.className = "bytm-cfg-menu-option";
const cfgOptItemElem = document.createElement("div");
cfgOptItemElem.className = "bytm-cfg-menu-option-item";
cfgOptItemElem.ariaLabel = cfgOptItemElem.title = "Click to open BetterYTM's configuration menu";
cfgOptItemElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
const settingsBtnElem = document.querySelector("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
settingsBtnElem === null || settingsBtnElem === void 0 ? void 0 : settingsBtnElem.click();
menuOpenAmt++;
yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(100);
if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
(0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_4__.openMenu)();
if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
exchangeLogo();
}));
const cfgOptIconElem = document.createElement("img");
cfgOptIconElem.className = "bytm-cfg-menu-option-icon";
cfgOptIconElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getResourceUrl)("icon");
const cfgOptTextElem = document.createElement("div");
cfgOptTextElem.className = "bytm-cfg-menu-option-text";
cfgOptTextElem.innerText = "BetterYTM Configuration";
cfgOptItemElem.appendChild(cfgOptIconElem);
cfgOptItemElem.appendChild(cfgOptTextElem);
cfgOptElem.appendChild(cfgOptItemElem);
container.appendChild(cfgOptElem);
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Added BYTM-Configuration button to menu popover");
});
}
//#MARKER remove upgrade tab
/** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
function removeUpgradeTab() {
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
listener: (tabElemLarge) => {
tabElemLarge.remove();
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Removed large upgrade tab");
},
});
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout #mini-guide ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
listener: (tabElemSmall) => {
tabElemSmall.remove();
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Removed small upgrade tab");
},
});
}
//#MARKER volume slider
function initVolumeFeatures() {
// not technically an input element but behaves pretty much the same
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("tp-yt-paper-slider#volume-slider", {
listener: (sliderElem) => {
const volSliderCont = document.createElement("div");
volSliderCont.id = "bytm-vol-slider-cont";
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(sliderElem, volSliderCont);
if (typeof features.volumeSliderSize === "number")
setVolSliderSize();
if (features.volumeSliderLabel)
addVolumeSliderLabel(sliderElem, volSliderCont);
setVolSliderStep(sliderElem);
},
});
}
/** Adds a percentage label to the volume slider and tooltip */
function addVolumeSliderLabel(sliderElem, sliderCont) {
const labelElem = document.createElement("div");
labelElem.className = "bytm-vol-slider-label";
labelElem.innerText = `${sliderElem.value}%`;
// prevent video from minimizing
labelElem.addEventListener("click", (e) => e.stopPropagation());
const getLabelTexts = (slider) => {
const labelShort = `${slider.value}%`;
const sensText = features.volumeSliderStep !== _index__WEBPACK_IMPORTED_MODULE_6__.featInfo.volumeSliderStep.default ? ` (Sensitivity: ${slider.step}%)` : "";
const labelFull = `Volume: ${labelShort}${sensText}`;
return { labelShort, labelFull };
};
const { labelFull } = getLabelTexts(sliderElem);
sliderCont.setAttribute("title", labelFull);
sliderElem.setAttribute("title", labelFull);
sliderElem.setAttribute("aria-valuetext", labelFull);
const updateLabel = () => {
const { labelShort, labelFull } = getLabelTexts(sliderElem);
sliderCont.setAttribute("title", labelFull);
sliderElem.setAttribute("title", labelFull);
sliderElem.setAttribute("aria-valuetext", labelFull);
const labelElem2 = document.querySelector(".bytm-vol-slider-label");
if (labelElem2)
labelElem2.innerText = labelShort;
};
sliderElem.addEventListener("change", () => updateLabel());
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("#bytm-vol-slider-cont", {
listener: (volumeCont) => {
volumeCont.appendChild(labelElem);
},
});
let lastSliderVal = Number(sliderElem.value);
// show label if hovering over slider or slider is focused
const sliderHoverObserver = new MutationObserver(() => {
if (sliderElem.classList.contains("on-hover") || document.activeElement === sliderElem)
labelElem.classList.add("bytm-visible");
else if (labelElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
labelElem.classList.remove("bytm-visible");
if (Number(sliderElem.value) !== lastSliderVal) {
lastSliderVal = Number(sliderElem.value);
updateLabel();
}
});
sliderHoverObserver.observe(sliderElem, {
attributes: true,
});
}
/** Sets the volume slider to a set size */
function setVolSliderSize() {
const { volumeSliderSize: size } = features;
if (typeof size !== "number" || isNaN(Number(size)))
return;
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addGlobalStyle)(`\
/* BetterYTM - set volume slider size */
#bytm-vol-slider-cont tp-yt-paper-slider#volume-slider {
width: ${size}px !important;
}`);
}
/** Sets the `step` attribute of the volume slider */
function setVolSliderStep(sliderElem) {
sliderElem.setAttribute("step", String(features.volumeSliderStep));
}
//#MARKER queue buttons
function initQueueButtons() {
const addQueueBtns = (evt) => {
let amt = 0;
for (const queueItm of evt.childNodes) {
if (!queueItm.classList.contains("bytm-has-queue-btns")) {
addQueueButtons(queueItm);
amt++;
}
}
if (amt > 0)
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added buttons to ${amt} new queue ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", amt)}`);
};
_events__WEBPACK_IMPORTED_MODULE_3__.siteEvents.on("queueChanged", addQueueBtns);
_events__WEBPACK_IMPORTED_MODULE_3__.siteEvents.on("autoplayQueueChanged", addQueueBtns);
const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
if (queueItems.length === 0)
return;
queueItems.forEach(itm => addQueueButtons(itm));
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added buttons to ${queueItems.length} existing queue ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", queueItems)}`);
}
/**
* Adds the buttons to each item in the current song queue.
* Also observes for changes to add new buttons to new items in the queue.
* @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
*/
function addQueueButtons(queueItem) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
//#SECTION general queue item stuff
const queueBtnsCont = document.createElement("div");
queueBtnsCont.className = "bytm-queue-btn-container";
const lyricsIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getResourceUrl)("lyrics");
const deleteIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getResourceUrl)("delete");
//#SECTION lyrics btn
let lyricsBtnElem;
if (features.lyricsQueueButton) {
lyricsBtnElem = yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.createLyricsBtn)(undefined, false);
lyricsBtnElem.title = "Open this song's lyrics in a new tab";
lyricsBtnElem.style.display = "inline-flex";
lyricsBtnElem.style.visibility = "initial";
lyricsBtnElem.style.pointerEvents = "initial";
lyricsBtnElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
e.stopPropagation();
const songInfo = queueItem.querySelector(".song-info");
if (!songInfo)
return;
const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
const song = songEl === null || songEl === void 0 ? void 0 : songEl.innerText;
const artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.innerText;
if (!song || !artist)
return;
let lyricsUrl;
const artistsSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.sanitizeArtists)(artist);
const songSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.sanitizeSong)(song);
const splitTitle = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.splitVideoTitle)(songSan);
const cachedLyricsUrl = songSan.includes("-")
? (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.getLyricsCacheEntry)(splitTitle.artist, splitTitle.song)
: (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.getLyricsCacheEntry)(artistsSan, songSan);
if (cachedLyricsUrl)
lyricsUrl = cachedLyricsUrl;
else if (!songInfo.hasAttribute("data-bytm-loading")) {
const imgEl = lyricsBtnElem.querySelector("img");
if (!cachedLyricsUrl) {
songInfo.setAttribute("data-bytm-loading", "");
imgEl.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getResourceUrl)("spinner");
imgEl.classList.add("bytm-spinner");
}
lyricsUrl = cachedLyricsUrl !== null && cachedLyricsUrl !== void 0 ? cachedLyricsUrl : yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.getGeniusUrl)(artistsSan, songSan);
const resetImgElem = () => {
imgEl.src = lyricsIconUrl;
imgEl.classList.remove("bytm-spinner");
};
if (!cachedLyricsUrl) {
songInfo.removeAttribute("data-bytm-loading");
// so the new image doesn't "blink"
setTimeout(resetImgElem, 100);
}
if (!lyricsUrl) {
resetImgElem();
if (confirm("Couldn't find a lyrics page for this song.\nDo you want to open genius.com to manually search for it?"))
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.openInNewTab)(`https://genius.com/search?q=${encodeURIComponent(`${artistsSan} ${songSan}`)}`);
return;
}
}
lyricsUrl && (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.openInNewTab)(lyricsUrl);
}));
}
//#SECTION delete from queue btn
let deleteBtnElem;
if (features.deleteFromQueueButton) {
deleteBtnElem = document.createElement("a");
Object.assign(deleteBtnElem, {
title: "Remove this song from the queue",
className: "ytmusic-player-bar bytm-delete-from-queue bytm-generic-btn",
role: "button",
});
deleteBtnElem.style.visibility = "initial";
deleteBtnElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
e.stopPropagation();
// container of the queue item popup menu - element gets reused for every queue item
let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
try {
// three dots button to open the popup menu of a queue item
const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape button");
if (queuePopupCont)
queuePopupCont.setAttribute("data-bytm-hidden", "true");
dotsBtnElem === null || dotsBtnElem === void 0 ? void 0 : dotsBtnElem.click();
yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(20);
queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.setAttribute("data-bytm-hidden", "true");
// a little bit janky and unreliable but the only way afaik
const removeFromQueueBtn = queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.querySelector("tp-yt-paper-listbox ytmusic-menu-service-item-renderer:nth-of-type(3)");
yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(10);
removeFromQueueBtn === null || removeFromQueueBtn === void 0 ? void 0 : removeFromQueueBtn.click();
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't remove song from queue due to error:", err);
}
finally {
queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
}
}));
const imgElem = document.createElement("img");
imgElem.className = "bytm-generic-btn-img";
imgElem.src = deleteIconUrl;
deleteBtnElem.appendChild(imgElem);
}
//#SECTION append elements to DOM
lyricsBtnElem && queueBtnsCont.appendChild(lyricsBtnElem);
deleteBtnElem && queueBtnsCont.appendChild(deleteBtnElem);
(_a = queueItem.querySelector(".song-info")) === null || _a === void 0 ? void 0 : _a.appendChild(queueBtnsCont);
queueItem.classList.add("bytm-has-queue-btns");
});
}
//#MARKER anchor improvements
// TODO: add to thumbnails in "songs" list on channel pages (/channel/$id)
// TODO: add to thumbnails in playlists (/playlist?list=$id)
// TODO:FIXME: only works for the first 7 items of each carousel shelf -> probably needs own mutation observer
/** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
function addAnchorImprovements() {
//#SECTION carousel shelves
try {
// home page
/** Only adds anchor improvements for carousel shelves that contain the regular list-item-renderer, not the two-row-item-renderer */
const condCarouselImprovements = (el) => {
const listItemRenderer = el.querySelector("ytmusic-responsive-list-item-renderer");
if (listItemRenderer) {
const itemsElem = el.querySelector("ul#items");
if (itemsElem) {
const improvedElems = improveCarouselAnchors(itemsElem);
improvedElems > 0 && (0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added anchor improvements to ${improvedElems} carousel shelf ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", improvedElems)}`);
}
}
};
// initial three shelves aren't included in the event fire
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-carousel-shelf-renderer", {
listener: () => {
const carouselShelves = document.body.querySelectorAll("ytmusic-carousel-shelf-renderer");
carouselShelves.forEach(condCarouselImprovements);
},
});
// every shelf that's loaded by scrolling:
_events__WEBPACK_IMPORTED_MODULE_3__.siteEvents.on("carouselShelvesChanged", ({ addedNodes }) => {
if (addedNodes && addedNodes.length > 0)
addedNodes.forEach(condCarouselImprovements);
});
// related tab in /watch
// TODO: items are lazy-loaded so this needs to be done differently
// maybe the onSelectorExists feature can be expanded to conditionally support continuous checking & querySelectorAll
const relatedTabAnchorImprovements = (tabElem) => {
const relatedCarouselShelves = tabElem === null || tabElem === void 0 ? void 0 : tabElem.querySelectorAll("ytmusic-carousel-shelf-renderer");
if (relatedCarouselShelves)
relatedCarouselShelves.forEach(condCarouselImprovements);
};
const relatedTabContentsSelector = "ytmusic-section-list-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] #contents";
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"]", {
listener: (relatedTabContainer) => {
const relatedTabObserver = new MutationObserver(([{ addedNodes, removedNodes }]) => {
if (addedNodes.length > 0 || removedNodes.length > 0)
relatedTabAnchorImprovements(document.querySelector(relatedTabContentsSelector));
});
relatedTabObserver.observe(relatedTabContainer, {
childList: true,
});
},
});
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(relatedTabContentsSelector, {
listener: (relatedTabContents) => {
relatedTabAnchorImprovements(relatedTabContents);
},
});
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't improve carousel shelf anchors due to an error:", err);
}
//#SECTION sidebar
try {
const addSidebarAnchors = (sidebarCont) => {
const items = sidebarCont.parentNode.querySelectorAll("ytmusic-guide-entry-renderer tp-yt-paper-item");
improveSidebarAnchors(items);
return items.length;
};
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
listener: (sidebarCont) => {
const itemsAmt = addSidebarAnchors(sidebarCont);
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added anchors around ${itemsAmt} sidebar ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", itemsAmt)}`);
},
});
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout #mini-guide ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
listener: (miniSidebarCont) => {
const itemsAmt = addSidebarAnchors(miniSidebarCont);
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added anchors around ${itemsAmt} mini sidebar ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", itemsAmt)}`);
},
});
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't add anchors to sidebar items due to an error:", err);
}
}
const sidebarPaths = [
"/",
"/explore",
"/library",
];
/**
* Adds anchors to the sidebar items so they can be opened in a new tab
* @param sidebarItem
*/
function improveSidebarAnchors(sidebarItems) {
sidebarItems.forEach((item, i) => {
var _a;
const anchorElem = document.createElement("a");
anchorElem.classList.add("bytm-anchor", "bytm-no-select");
anchorElem.role = "button";
anchorElem.target = "_self";
anchorElem.href = (_a = sidebarPaths[i]) !== null && _a !== void 0 ? _a : "#";
anchorElem.title = "Middle click to open in a new tab";
anchorElem.addEventListener("click", (e) => {
e.preventDefault();
});
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(item, anchorElem);
});
}
/**
* Actually adds the anchor improvements to carousel shelf items
* @param itemsElement The container with the selector `ul#items` inside of each `ytmusic-carousel`
*/
function improveCarouselAnchors(itemsElement) {
if (itemsElement.classList.contains("bytm-anchors-improved"))
return 0;
let improvedElems = 0;
try {
const allListItems = itemsElement.querySelectorAll("ytmusic-responsive-list-item-renderer");
for (const listItem of allListItems) {
const thumbnailElem = listItem.querySelector(".left-items");
const titleElem = listItem.querySelector(".title-column yt-formatted-string.title a");
if (!thumbnailElem || !titleElem) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't add carousel shelf anchor improvements because either the thumbnail or title element couldn't be found");
continue;
}
const thumbnailAnchor = document.createElement("a");
thumbnailAnchor.className = "bytm-carousel-shelf-anchor bytm-anchor";
thumbnailAnchor.href = titleElem.href;
thumbnailAnchor.target = "_self";
thumbnailAnchor.role = "button";
thumbnailAnchor.addEventListener("click", (e) => {
e.preventDefault();
});
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(thumbnailElem, thumbnailAnchor);
improvedElems++;
}
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't add anchor improvements due to error:", err);
}
finally {
itemsElement.classList.add("bytm-anchors-improved");
}
return improvedElems;
}
//#MARKER auto close toasts
/** Closes toasts after a set amount of time */
function initAutoCloseToasts() {
try {
const animTimeout = 300;
const closeTimeout = Math.max(features.closeToastsTimeout * 1000 + animTimeout, animTimeout);
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("tp-yt-paper-toast#toast", {
all: true,
continuous: true,
listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
var _a;
for (const toastElem of toastElems) {
if (!toastElem.hasAttribute("allow-click-through"))
continue;
if (toastElem.classList.contains("bytm-closing"))
continue;
toastElem.classList.add("bytm-closing");
yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(closeTimeout);
toastElem.classList.remove("paper-toast-open");
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Automatically closed toast '${(_a = toastElem.querySelector("#text-container yt-formatted-string")) === null || _a === void 0 ? void 0 : _a.innerText}' after ${features.closeToastsTimeout * 1000}ms`);
// wait for the transition to finish
yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(animTimeout);
toastElem.style.display = "none";
}
}),
});
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Initialized automatic toast closing");
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Error in automatic toast closing:", err);
}
}
//#MARKER remove share tracking param
/** Continuously removes the ?si tracking parameter from share URLs */
function removeShareTrackingParam() {
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("yt-copy-link-renderer input#share-url", {
continuous: true,
listener: (inputElem) => {
try {
const url = new URL(inputElem.value);
if (!url.searchParams.has("si"))
return;
url.searchParams.delete("si");
inputElem.value = String(url);
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Removed tracking parameter from share link: ${url}`);
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_2__.warn)("Couldn't remove tracking parameter from share link due to error:", err);
}
},
});
}
/***/ }),
/***/ "./src/features/lyrics.ts":
/*!********************************!*\
!*** ./src/features/lyrics.ts ***!
\********************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ addLyricsCacheEntry: function() { return /* binding */ addLyricsCacheEntry; },
/* harmony export */ addMediaCtrlLyricsBtn: function() { return /* binding */ addMediaCtrlLyricsBtn; },
/* harmony export */ createLyricsBtn: function() { return /* binding */ createLyricsBtn; },
/* harmony export */ geniUrlBase: function() { return /* binding */ geniUrlBase; },
/* harmony export */ getCurrentLyricsUrl: function() { return /* binding */ getCurrentLyricsUrl; },
/* harmony export */ getGeniusUrl: function() { return /* binding */ getGeniusUrl; },
/* harmony export */ getLyricsCacheEntry: function() { return /* binding */ getLyricsCacheEntry; },
/* harmony export */ sanitizeArtists: function() { return /* binding */ sanitizeArtists; },
/* harmony export */ sanitizeSong: function() { return /* binding */ sanitizeSong; },
/* harmony export */ splitVideoTitle: function() { return /* binding */ splitVideoTitle; }
/* harmony export */ });
/* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "./node_modules/@sv443-network/userutils/dist/index.mjs");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (undefined && undefined.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
/** Base URL of geniURL */
const geniUrlBase = "https://api.sv443.net/geniurl";
/** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
/**
* The threshold to pass to geniURL's fuzzy filtering.
* From fuse.js docs: At what point does the match algorithm give up. A threshold of 0.0 requires a perfect match (of both letters and location), a threshold of 1.0 would match anything.
* Set to undefined to use the default.
*/
const threshold = 0.55;
/** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
const geniUrlRatelimitTimeframe = 30;
const thresholdParam = threshold ? `&threshold=${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.clamp)(threshold, 0, 1)}` : "";
//#MARKER cache
/** Cache with key format `ARTIST - SONG` (sanitized) and lyrics URLs as values. Used to prevent extraneous requests to geniURL. */
const lyricsUrlCache = new Map();
/** How many cache entries can exist at a time - this is used to cap memory usage */
const maxLyricsCacheSize = 100;
/**
* Returns the lyrics URL from the passed un-/sanitized artist and song name, or undefined if the entry doesn't exist yet.
* **The passed parameters need to be sanitized first!**
*/
function getLyricsCacheEntry(artists, song) {
return lyricsUrlCache.get(`${artists} - ${song}`);
}
/** Adds the provided entry into the lyrics URL cache */
function addLyricsCacheEntry(artists, song, lyricsUrl) {
lyricsUrlCache.set(`${sanitizeArtists(artists)} - ${sanitizeSong(song)}`, lyricsUrl);
// delete oldest entry if cache gets too big
if (lyricsUrlCache.size > maxLyricsCacheSize)
lyricsUrlCache.delete([...lyricsUrlCache.keys()].at(-1));
}
//#MARKER media control bar
let mcCurrentSongTitle = "";
/** Adds a lyrics button to the media controls bar */
function addMediaCtrlLyricsBtn() {
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
}
// TODO: add error.svg if the request fails
/** Actually adds the lyrics button after the like button renderer has been verified to exist */
function addActualMediaCtrlLyricsBtn(likeContainer) {
return __awaiter(this, void 0, void 0, function* () {
const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
// run parallel without awaiting so the MutationObserver below can observe the title element in time
(() => __awaiter(this, void 0, void 0, function* () {
const gUrl = yield getCurrentLyricsUrl();
const linkElem = yield createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
linkElem.id = "betterytm-lyrics-button";
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Inserted lyrics button into media controls bar");
(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.insertAfter)(likeContainer, linkElem);
}))();
mcCurrentSongTitle = songTitleElem.title;
const spinnerIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("spinner");
const lyricsIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("lyrics");
const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
var _b, e_1, _c, _d;
try {
for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b; _a = true) {
_d = mutations_1_1.value;
_a = false;
const mut = _d;
const newTitle = mut.target.title;
if (newTitle !== mcCurrentSongTitle && newTitle.length > 0) {
const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
if (!lyricsBtn)
return;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Song title changed from '${mcCurrentSongTitle}' to '${newTitle}'`);
lyricsBtn.style.cursor = "wait";
lyricsBtn.style.pointerEvents = "none";
const imgElem = lyricsBtn.querySelector("img");
imgElem.src = spinnerIconUrl;
imgElem.classList.add("bytm-spinner");
mcCurrentSongTitle = newTitle;
const url = yield getCurrentLyricsUrl(); // can take a second or two
imgElem.src = lyricsIconUrl;
imgElem.classList.remove("bytm-spinner");
if (!url)
continue;
lyricsBtn.href = url;
lyricsBtn.title = "Open the current song's lyrics in a new tab";
lyricsBtn.style.cursor = "pointer";
lyricsBtn.style.visibility = "initial";
lyricsBtn.style.display = "inline-flex";
lyricsBtn.style.pointerEvents = "initial";
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
}
finally { if (e_1) throw e_1.error; }
}
}); };
// since YT and YTM don't reload the page on video change, MutationObserver needs to be used to watch for changes in the video title
const obs = new MutationObserver(onMutation);
obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
});
}
//#MARKER utils
/** Removes everything in parentheses from the passed song name */
function sanitizeSong(songName) {
const parensRegex = /\(.+\)/gmi;
const squareParensRegex = /\[.+\]/gmi;
// trim right after the song name:
const sanitized = songName
.replace(parensRegex, "")
.replace(squareParensRegex, "");
return sanitized.trim();
}
/** Removes the secondary artist (if it exists) from the passed artists string */
function sanitizeArtists(artists) {
artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at • [•] character
if (artists.match(/&/))
artists = artists.split(/\s*&\s*/gm)[0];
if (artists.match(/,/))
artists = artists.split(/,\s*/gm)[0];
return artists.trim();
}
/** Returns the lyrics URL from genius for the currently selected song */
function getCurrentLyricsUrl() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
// In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.hasAttribute("video-mode"));
const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string:first-child");
if (!songTitleElem || !songMetaElem || !songTitleElem.title)
return undefined;
const songNameRaw = songTitleElem.title;
const songName = sanitizeSong(songNameRaw);
const artistName = sanitizeArtists(songMetaElem.title);
/** Use when the current song is not a "real YTM song" with a static background, but rather a music video */
const getGeniusUrlVideo = () => __awaiter(this, void 0, void 0, function* () {
if (!songName.includes("-")) // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
return yield getGeniusUrl(artistName, songName);
const { artist, song } = splitVideoTitle(songName);
return yield getGeniusUrl(artist, song);
});
// TODO: artist might need further splitting before comma or ampersand
const url = isVideo ? yield getGeniusUrlVideo() : yield getGeniusUrl(artistName, songName);
return url;
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't resolve lyrics URL:", err);
return undefined;
}
});
}
/** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
function getGeniusUrl(artist, song) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
try {
const cacheEntry = getLyricsCacheEntry(artist, song);
if (cacheEntry) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL in cache: ${cacheEntry}`);
return cacheEntry;
}
const startTs = Date.now();
const fetchUrl = `${geniURLSearchTopUrl}?artist=${encodeURIComponent(artist)}&song=${encodeURIComponent(song)}${thresholdParam}`;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Requesting URL from geniURL at '${fetchUrl}'`);
const fetchRes = yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.fetchAdvanced)(fetchUrl);
if (fetchRes.status === 429) {
alert(`You are being rate limited.\nPlease wait ${(_a = fetchRes.headers.get("retry-after")) !== null && _a !== void 0 ? _a : geniUrlRatelimitTimeframe} seconds before requesting more lyrics.`);
return undefined;
}
else if (fetchRes.status < 200 || fetchRes.status >= 300) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)(`Couldn't fetch lyrics URL from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
return undefined;
}
const result = yield fetchRes.json();
if (typeof result === "object" && result.error) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't fetch lyrics URL:", result.message);
return undefined;
}
const url = result.url;
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
addLyricsCacheEntry(artist, song, url);
return url;
}
catch (err) {
(0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't get lyrics URL due to error:", err);
return undefined;
}
});
}
/** Creates the base lyrics button element */
function createLyricsBtn(geniusUrl, hideIfLoading = true) {
return __awaiter(this, void 0, void 0, function* () {
const linkElem = document.createElement("a");
linkElem.className = "ytmusic-player-bar bytm-generic-btn";
linkElem.title = geniusUrl ? "Click to open this song's lyrics in a new tab" : "Loading lyrics URL...";
if (geniusUrl)
linkElem.href = geniusUrl;
linkElem.role = "button";
linkElem.target = "_blank";
linkElem.rel = "noopener noreferrer";
linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
const imgElem = document.createElement("img");
imgElem.className = "bytm-generic-btn-img";
imgElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("lyrics");
linkElem.appendChild(imgElem);
return linkElem;
});
}
/** Splits a video title that contains a hyphen into an artist and song */
function splitVideoTitle(title) {
const [artist, ...rest] = title.split("-").map((v, i) => i < 2 ? v.trim() : v);
return { artist, song: rest.join("-") };
}
/***/ }),
/***/ "./src/features/menu/menu.ts":
/*!***********************************!*\
!*** ./src/features/menu/menu.ts ***!
\***********************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
/* harmony export */ initMenu: function() { return /* binding */ initMenu; },
/* harmony export */ openMenu: function() { return /* binding */ openMenu; },
/* harmony export */ setActiveTab: function() { return /* binding */ setActiveTab; }
/* harmony export */ });
/* harmony import */ var _changelog_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../../changelog.md */ "./changelog.md");
/* harmony import */ var _menu_html__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./menu.html */ "./src/features/menu/menu.html");
/* harmony import */ var _menu_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./menu.css */ "./src/features/menu/menu.css");
// REQUIREMENTS:
// - modal using the