// ==UserScript== // @name Plex web album and artist name swap // @namespace http://tampermonkey.net/ // @version 2024-02-09 // @description Swaps the album and artist names so the album name is on top // @author frondonson // @license MIT // @match https://app.plex.tv/desktop/* // @icon https://www.google.com/s2/favicons?sz=64&domain=plex.tv // @grant none // @downloadURL none // ==/UserScript== //Wait for Library to load waitForAndRun('.DirectoryListPageContent-pageContentScroller-O3oHlt', initLibrary, 50); //Wait for Recomended to load waitForAndRun('.DirectoryHubsPageContent-pageContentScroller-jceJrG', initRecomended, 50); function initLibrary() { //Direct parrent of album [data-testid="cellItem"] divs let allAlbumsParent = document.querySelector('div.DirectoryListPageContent-pageContentScroller-O3oHlt>div'); //Set up mutation observer var config = { attributes: false, childList: true, subtree: false }; var observer = new MutationObserver(handleLibraryMutations); observer.observe(allAlbumsParent, config); //Do first batch of albums from page load let firstLoadAlbumDivs = allAlbumsParent.querySelectorAll('div[data-testid="cellItem"]'); doSwaps(firstLoadAlbumDivs); } function initRecomended() { //Parrent of ribbon divs let allRecomendedParent = document.querySelector('div.DirectoryHubsPageContent-pageContentScroller-jceJrG>div'); //Set up mutation observer, subtree true becase albums are spread out in several parent divs var config = { attributes: false, childList: true, subtree: true }; var observer = new MutationObserver(handleRecomendedMutations); observer.observe(allRecomendedParent, config); //Do first batch of albums from page load let firstLoadAlbumDivs = allRecomendedParent.querySelectorAll('div[data-testid="cellItem"]'); doSwaps(firstLoadAlbumDivs); } function handleLibraryMutations(mutations) { let newAlbumDivs = mutations[0].target.children; doSwaps(newAlbumDivs); } function handleRecomendedMutations(mutations) { let newAlbumDivs = mutations[0].target.querySelectorAll('div[data-testid="cellItem"]'); doSwaps(newAlbumDivs); } function doSwaps(nodeList) { //parrent.children is a HTMLcollection not a nodeList, use iterable for each //https://stackoverflow.com/questions/35969974/foreach-is-not-a-function-error-with-javascript-array for (const albumDiv of nodeList) { if(albumDiv.childElementCount < 3 || albumDiv.lastChild.nodeName == ('SPAN')) { continue; } if(albumDiv.classList.contains('SwapAlbumNameUserScript-swapped')) { continue; } let artistNode = albumDiv.children[1]; //Classlist- MetadataPosterCardTitle-centeredSingleLineTitle-EuZHlc MetadataPosterCardTitle-singleLineTitle-lPd1B2 MetadataPosterCardTitle-title-ImAmGu Link-default-bdWb1S Link-isHrefLink-nk7Aiq let albumNode = albumDiv.children[2]; //Classlist- MetadataPosterCardTitle-centeredSingleLineTitle-EuZHlc MetadataPosterCardTitle-singleLineTitle-lPd1B2 MetadataPosterCardTitle-title-ImAmGu MetadataPosterCardTitle-isSecondary-gGuBpd Link-default-bdWb1S Link-isHrefLink-nk7Aiq artistNode.classList.add('MetadataPosterCardTitle-isSecondary-gGuBpd'); albumNode.classList.remove('MetadataPosterCardTitle-isSecondary-gGuBpd'); //Swap positions - (x, y) insert node X before node Y albumDiv.insertBefore(albumNode, artistNode); albumDiv.classList.add('SwapAlbumNameUserScript-swapped'); } } async function waitForAndRun(selector, callback, delay) { await waitForElm(selector).then(element => { setTimeout(callback, delay); }); } //https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists function waitForElm(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 observer.observe(document.body, { childList: true, subtree: true }); }); }