spicetify / cli

Command-line tool to customize Spotify client. Supports Windows, MacOS, and Linux.
https://spicetify.app
GNU Lesser General Public License v2.1
18.63k stars 732 forks source link

How can I run a script everytime a new page in spotify loads #162

Closed zjeffer closed 4 years ago

zjeffer commented 4 years ago

This is my script: It changes the popularity meter in album views so instead of seeing a bar, you see the number of plays each song has.

(function numberOfPlays() {
    function changeToNumber() {
        //if page is fully loaded or something idk
        if (!Spicetify.LocalStorage) {
            //if it isn't wait until it is
            setTimeout(changeToNumber, 1000);
            return;
        }

        //select all table rows
        let cells = document.querySelectorAll(".Table__table tbody tr.TableRow.TableRow td:last-child");

        //check if the current window is the album view or the artist view
        if (cells != undefined && cells.length != 0) {
            //for album view
            for (let cell of cells) {
                //get amount from attribute
                let plays = cell.firstChild.getAttribute("data-tooltip-text");
                //change cell value
                cell.innerHTML = plays.substring(0, plays.indexOf("plays") - 1);
            }
        } else {
            //for artist view
            //get document inside #app-artist iframe
            let doc = document.querySelector("#app-artist").contentWindow.document;
            //search doc for all cells
            cells = doc.querySelectorAll(".tl-body tr .tl-popularity:last-child");
            for(let cell of cells){
                //get amount from attribute
                let plays = cell.getAttribute("data-tooltip")
                //change cell value
                cell.innerHTML = plays.substring(0, plays.indexOf("plays") - 1);
            }
        }

        console.log(cells);
    }

    window.onload = () => {
        changeToNumber();
    };
})();

The problem is that when I select all tables with document.querySelectorAll(".Table__table tbody tr.TableRow.TableRow td:last-child");, it always returns an empty list, unless I reload the page with ctrl+shift+R.

How can I run my script everytime a new page in spotify loads (whenever I visit an album or an artist page with albums)?

zakybilfagih commented 4 years ago

Managed to make your script work, I added a MutationObserver so if the main-view div changes (new page load) it will trigger the function. I can't seem to make the Artist Page to work though since it doesn't seem to load the td element on first load but it will work if you resize the window.

(function numberOfPlays() {
    function changeToNumber() {
        //if page is fully loaded or something idk
        if (!Spicetify.LocalStorage) {
            //if it isn't wait until it is
            setTimeout(changeToNumber, 5000);
            return;
        }

        //select all table rows
        let cells = document.querySelectorAll(".Table__table tbody tr.TableRow.TableRow td:last-child > div");

        //check if the current window is the album view or the artist view
        if (cells != undefined && cells.length != 0) {
            //for album view
            console.log(cells)
            cells.forEach(c => {
                  //get amount from attribute
                  let plays = c.getAttribute("data-tooltip-text");
                  //change cell value
                  c.parentNode.innerHTML = plays.substring(0, plays.indexOf("plays") - 1);
            })
        } else {
            //for artist view
            //get document inside #app-artist iframe
            let doc = document.querySelector("#app-artist").contentWindow.document;
            //search doc for all cells
            cells = doc.querySelectorAll(".tl-body tr .tl-popularity:last-child");
            for(let cell of cells){
                //get amount from attribute
                let plays = cell.getAttribute("data-tooltip")
                //change cell value
                cell.innerHTML = plays.substring(0, plays.indexOf("plays") - 1);
            }
        }

        console.log(cells)
    }
    const toCheckMutate = document.getElementById('view-content');
    const config = { attributes: true, childList: true, subtree: true };

    let observerCallback = function(mutationsList, _) {
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            changeToNumber()
        }
        else if (mutation.type === 'attributes') {
            changeToNumber()
        }
      }
    };

    let observer = new MutationObserver(observerCallback)
    observer.observe(toCheckMutate, config)
})();
zjeffer commented 4 years ago

Thanks!

I think the reason it doesn't work is because the artist page is wrapped in an iframe element, creating a document within a document. I'll keep searching.

zjeffer commented 4 years ago
(function numberOfPlays() {
    function changeToNumber() {
        //if page is fully loaded or something idk what this actually does
        if (!Spicetify.LocalStorage) {
            //if it isn't try again every 1000 millis until it is
            setTimeout(changeToNumber, 1000);
            return;
        }

        //select all table rows
        let albumCells = document.querySelectorAll(".Table__table tbody tr.TableRow.TableRow td:last-child");

        //check if the current window is the album view or the artist view
        if (albumCells != undefined && albumCells.length != 0) {
            //for album view
            for (let cell of albumCells) {
                //get amount from attribute
                let plays = cell.firstChild.getAttribute("data-tooltip-text");
                if (plays != null) {
                    //change cell value
                    cell.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
                }
            }
        }
        //for artist view
        //get document inside #app-artist iframe
        let doc = document.querySelector("#app-artist");
        if (doc != undefined && doc != null) {
            doc = doc.contentWindow.document;
            //search doc for all artistCells
            let artistCells = doc.querySelectorAll(".tl-body tr .tl-popularity:last-child");
            for (let cell of artistCells) {
                //get amount from attribute
                let plays = cell.getAttribute("data-tooltip");
                //change cell value
                cell.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
            }
        }
    }

    const toCheckMutate = document.getElementById("view-content");
    const config = { attributes: true, childList: true, subtree: true };

    let observerCallback = function(mutationsList, _) {
        for (let mutation of mutationsList) {
            if (mutation.type === "childList" || mutation.type === "attributes") {
                changeToNumber();
            }
        }
    };

    let observer = new MutationObserver(observerCallback);
    observer.observe(toCheckMutate, config);

    const checkArtist = document.querySelector("#app-artist").contentDocument.body;
    let artistObserver = new MutationObserver(observerCallback);
    artistObserver.observe(checkArtist, config);
})();

I added a new MutationObserver for the body of the iframe object, but the problem is that document.querySelector("#app-artist") returns null.

Any ideas?

zakybilfagih commented 4 years ago

@zjeffer Hey, after checking on the Spicetify object wrapper and reading some other extensions you can add an event listener and listen on app change. The callback will receive an object with a container key which the value is the iframe of the current app.

{id: "playlist", uri: "spotify:app:playlist:37i9dQZF1Eaa1PQGW8c4gW", isEmbeddedApp: false, container: iframe#app-playlist.active}
     Spicetify.Player.addEventListener("appchange", ({"data": data}) => {
      if (data.id == "playlist") {
        console.log(data.container)
      }
    })
zjeffer commented 4 years ago

Thanks!

Updated script with appchange events:

(function numberOfPlays() {
    if (!Spicetify.LocalStorage) {
        setTimeout(numberOfPlays, 1000);
        return;
    }

    console.log("loaded");

    function albumPopularity(data) {
        //select all table rows
        let albumCells = data.querySelectorAll(".Table__table tbody tr.TableRow.TableRow td:last-child");
        console.log(albumCells);
        //check if the current window is the album view or the artist view
        if (albumCells != undefined && albumCells.length != 0) {
            //for album view
            for (let cell of albumCells) {
                //get amount from attribute
                let plays = cell.firstChild.getAttribute("data-tooltip-text");
                if (plays != null) {
                    //change cell value
                    cell.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
                }
            }
        }
    }

    function artistPopularity(data) {
        //search doc for all artistCells
        let artistCells = data.children[0].querySelectorAll(".tl-body tr .tl-popularity");
        console.log(artistCells);
        for (let cell of artistCells) {
            //get amount from attribute
            let plays = cell.getAttribute("data-tooltip");
            //change cell value
            cell.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
        }
    }

    function onChange(data) {
        if (data.id == "artist") {
            console.log("artistdata:");
            console.log(data);
            artistPopularity(data.container.contentDocument.body);
        } else if (data.id == "album") {
            console.log("albumdata:");
            console.log(data);
            albumPopularity(data.container.children[0]);
        }
    }

    Spicetify.Player.addEventListener("appchange", ({ data: data }) => {
        console.log("appchange");
        onChange(data);
    });

    //this doesn't do anything for some reason (wrong object to listen to?)
    Spicetify.Player.addEventListener("scroll", ({ data: data }) => {
        console.log("scroll");
        onChange(data);
    });

})();

Now when I go to an album with a lot of songs, scroll down and back up, the numbers disappear again.

On an artist's page, the list of cells is often empty for some reason, so the script doesn't find the popularity bars and doesn't do anything. It does sometimes work but I don't know how to make it always work. Scrolling down an artist's page also reveals new albums, but doesn't change the bars because revealing new albums doesn't trigger an appchange event.

I tried adding a scroll event listener to Spicetify.Player, but it never triggers.

I'll keep debugging.

khanhas commented 4 years ago
function onChange(data) {
    if (data.id == "artist") {
        console.log("artistdata:");
        console.log(data);
        artistPopularity(data.container.contentDocument.body);
    } else if (data.id == "album") {
        console.log("albumdata:");
        console.log(data);
        albumPopularity(data.container.children[0]);

        const observer = new MutationObserver(() => {
            albumPopularity(data.container.children[0]);
        });

        observer.observe(
            data.container.children[0].querySelector(".Table__table"),
            { 
                childList: true, 
                subtree: true 
            }
        );
    }
}
JulienMaille commented 4 years ago

Great idea! Looking forward for a debuged script

zjeffer commented 4 years ago

This is my current script:

It works on albums, but on artist's pages it only works on the first few albums, so it needs some fixing and optimizing.

Sadly I'm busy with school stuff, so that's why it's not done yet.

(function numberOfPlays() {
    if (!Spicetify.LocalStorage) {
        setTimeout(numberOfPlays, 1000);
        return;
    }

    console.log("loaded");

    function albumPopularity(data) {
        //select all table rows
        let albumCells = data.querySelectorAll(".Table__table tbody tr.TableRow.TableRow td:last-child");
        console.log(albumCells);
        //check if the current window is the album view or the artist view
        if (albumCells != undefined && albumCells.length != 0) {
            //for album view
            for (let cell of albumCells) {
                //get amount from attribute
                let plays = cell.firstChild.getAttribute("data-tooltip-text");
                if (plays != null) {
                    //change cell value
                    cell.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
                }
            }
        }
    }

    function artistPopularity(data) {
        //search doc for all artistCells
        let artistCells = data.children[0].querySelectorAll(".tl-body tr .tl-popularity");
        console.log(artistCells);
        for (let cell of artistCells) {
            //get amount from attribute
            let plays = cell.getAttribute("data-tooltip");
            //change cell value
            cell.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
        }
    }

    function onChange(data) {
        if (data.id == "artist") {
            console.log("artistdata:");
            console.log(data);
            artistPopularity(data.container.contentDocument);

            const observer = new MutationObserver(() => {
                artistPopularity(data.container.contentDocument);
            });

            observer.observe(data.container.children[0].querySelector(".albums ul"), {
                childList: true,
                subtree: true,
            });
        } else if (data.id == "album") {
            console.log("albumdata:");
            console.log(data);
            albumPopularity(data.container.children[0]);

            const observer = new MutationObserver(() => {
                albumPopularity(data.container.children[0]);
            });

            observer.observe(data.container.children[0].querySelector(".Table__table"), {
                childList: true,
                subtree: true,
            });
        }
    }

    Spicetify.Player.addEventListener("appchange", ({ data: data }) => {
        console.log("appchange");
        onChange(data);
    });

    // this function needs fixing
    const iframeInterval = setInterval(() => {
        /** @type {HTMLIFrameElement} */

        console.log("interval");

        const currentIframe = document.querySelector("iframe.active");

        if (!currentIframe) {
            return;
        }

        if (currentIframe.id === "app-collection-songs") {
            console.log("clearing interval");
            clearInterval(iframeInterval);
            return;
        }
        if (currentIframe.id !== "app-artist") {
            return;
        }

        let cells = currentIframe.contentDocument.querySelectorAll(".tl-body tr .tl-popularity");
        for (let c of cells) {
            //get amount from attribute
            let plays = c.getAttribute("data-tooltip");
            //change cell value
            c.innerHTML = `<div>${plays.substring(0, plays.indexOf("plays") - 1)}</div>`;
        }

        console.log("iframe cells:");
        console.log(cells);

        if (cells.length > 0) {
            // const observer = new MutationObserver(() => {
            //     artistPopularity(cells[0].ownerDocument);
            // });

            // let node = cells[0].ownerDocument.querySelector(".albums");
            // console.log(node);

            // observer.observe(node, {
            //     childList: true,
            //     subtree: true,
            // });
            console.log("clearing interval");
            clearInterval(iframeInterval);
        }
    }, 500);
})();
JulienMaille commented 4 years ago

@khanhas I have the same problem with my fork of Dribblish I'm listening to "appchange" but I noticed that sometime the event is not fired, exactly when I get a warning message

home
DevTools failed to load SourceMap: Could not load content for https://zlink.app.spotify.com/zlink.bundle.js.map: Connection error: net::ERR_NAME_NOT_RESOLVED
DevTools failed to load SourceMap: Could not load content for https://queue.app.spotify.com/queue.bundle.js.map: Connection error: net::ERR_NAME_NOT_RESOLVED
queue
radio-hub
DevTools failed to load SourceMap: Could not load content for https://genius.app.spotify.com/lyrics.bundle.js.map: Connection error: net::ERR_NAME_NOT_RESOLVED
genius
made-for-you
recently-played
home
DevTools failed to load SourceMap: Could not load content for https://search.app.spotify.com/search.bundle.js.map: Connection error: net::ERR_NAME_NOT_RESOLVED
reteps commented 3 years ago

For future reference, this will listen to navigation_request_state changes.

  window.addEventListener("message", ({ data: info }) => {
    if (info.type == "navigation_request_state" && info.method == "open") {
      console.log("Opened page:", JSON.parse(info.state).uri);
    }
  });
JulienMaille commented 3 years ago

@reteps I'm using your eventListener in Dribblish-Dynamic, however I noticed it's missing several page changes. For example the "Preferences" panel. Removing the test on info.type & info.method does the trick but then I end up triggering the attached function very often. Do you have any tips to share? Thanks