YouTube / Spotify Playlists Converter

Convert your music playlists between YouTube & Spotify with a single click.

目前为 2024-02-15 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube / Spotify Playlists Converter
// @version      1.6
// @description  Convert your music playlists between YouTube & Spotify with a single click.
// @author       bobsaget1990
// @match        https://www.youtube.com/*
// @match        https://music.youtube.com/*
// @match        https://open.spotify.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM.xmlHttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @connect      spotify.com
// @connect      youtube.com
// @connect      accounts.google.com
// @icon64       https://i.imgur.com/zjGIQn4.png
// @compatible   chrome
// @compatible   edge
// @compatible   firefox
// @license      GNU GPLv3
// @namespace https://gf.qytechs.cn/users/1254768
// ==/UserScript==
(async () => {
    // UI FUNCTIONS:
    function createUI(operations) {
        // Remove existing UI
        const oldUI = document.querySelector('div.floating-div');
        if (!!oldUI) oldUI.remove();

        const floatingDiv = document.createElement('div');
        floatingDiv.classList.add('floating-div');

        const centerDiv = document.createElement('div');
        centerDiv.classList.add('center-div');

        const cancelButton = document.createElement('button');
        cancelButton.classList.add('cancel-button');
        cancelButton.textContent = 'Cancel';

        function reload() {
            location.reload();
        }
        cancelButton.onclick = reload;

        const closeButton = document.createElement('button');
        closeButton.classList.add('close-button');
        closeButton.innerHTML = '×'; // Unicode character for the close symbol
        closeButton.onclick = reload;

        // Add UI to the page
        document.body.appendChild(floatingDiv);
        floatingDiv.appendChild(centerDiv);
        floatingDiv.appendChild(cancelButton);
        floatingDiv.appendChild(closeButton);
        floatingDiv.style.display = 'flex';
        // Add opertations
        let spans = generateSpans(operations);
        centerDiv.append(...spans);

        // CSS
        const css = `
.floating-div {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9999;
  width: 400px;
  height: auto;
  display: none;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  border-radius: 10px;
  box-shadow: 0 0 0 1px #3a3a3a;
  background-color: #0f0f0f;
  line-height: 50px;
}

.center-div span {
  display: block;
  height: 30px;
  margin: 10px;
  font-family: 'Roboto', sans-serif;
  font-size: 14px;
  color: white;
  opacity: 0.3;
}

.cancel-button {
  width: auto;
  height: 30px;
  padding-left: 25px;
  padding-right: 25px;
  margin-top: 20px;
  margin-bottom: 20px;
  background-color: white;
  color: #0f0f0f;
  border-radius: 50px;
  border: unset;
  font-family: 'Roboto', sans-serif;
  font-size: 16px;
}

.cancel-button:hover {
  box-shadow: inset 0px 0px 0 2000px rgba(0,0,0,0.25);
}

.cancel-button:active {
  box-shadow: inset 0px 0px 0 2000px rgba(0,0,0,0.5);
}

.close-button {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 25px;
    height: 25px;
    border-radius: 50%;
    background-color: #393939;
    color: #7e7e7e;
    border: unset;
    font-family: math;
    font-size: 17px;
    text-align: center;
}

.close-button:hover {
  box-shadow: inset 0px 0px 0 2000px rgba(255,255,255,0.05);
}

.close-button:active {
  box-shadow: inset 0px 0px 0 2000px rgba(255,255,255,0.1);
}

`;

        // Add the CSS to the page
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);

        return {
            floatingDiv: floatingDiv,
            centerDiv: centerDiv,
            cancelButton: cancelButton,
            closeButton: closeButton
        };
    }

    function generateSpans(spanTextContent) {
        let spans = [];
        for (let i = 0; i < spanTextContent.length; i++) {
            let span = document.createElement("span");
            span.textContent = spanTextContent[i];
            span.classList.add(`op-${i + 1}`);
            spans.push(span);
        }
        return spans;
    }

    function closeConfirmation(event) {
        event.preventDefault();
        event.returnValue = null;
        return null;
    }

    function CustomError(obj) {
        this.message = obj.message;
        this.url = obj.url;
        this.code = obj.code;
    }
    CustomError.prototype = Error.prototype;

    function errorHandler(error) {
        const errorCode = error.code;
        if (isYouTube || isYouTubeMusic) {
            if (errorCode == 10) alert(`⛔ Could not get Spotify token: Make sure you are signed in to Spotify and try again..`);
            if (errorCode == 11) alert(`⛔ Could not get Spotify User ID: Make sure you are signed in to Spotify and try again..`);
            if (errorCode == 2 || errorCode == 3) alert(`⛔ Could not get YouTube playlist info..`);
            if (errorCode == 8) alert(`⛔ Could not create Spotify playlist..`);
            if (errorCode == 9) alert(`⛔ Could not add songs to Spotify playlist..`);
        }
        if (isSpotify) {
            if (errorCode == 0) window.location.href = 'https://www.youtube.com/#go_back_fragment';
            if (errorCode == 1) alert('⛔ Could not get playlist info: The playlist is empty!');
            if (errorCode == 10) alert('⛔ Could not get Spotify token: Make sure you are signed in to Spotify and try again..');
            if (errorCode == 11) alert('⛔ Could not get Spotify User ID: Make sure you are signed in to Spotify and try again..');
            if (errorCode == 7) alert('⛔ Could not get Spotify playlist info..');
            if (errorCode == 5) alert('⛔ Could not get YouTube User ID: Make sure you are signed in to YouTube and try again..');
            if (errorCode == 6) alert('⛔ Could not create YouTube playlist..');
        }
    }




    // GLOBALS:
    let address = window.location.href;
    const subdomain = address.slice(8).split('.')[0];
    let isYouTube, isYouTubeMusic, isSpotify, isYouTubePlaylist, isSpotifyPlaylist;
    const ytPlaylistIdRegEx = /list=(.{34})/;
    const spotifyPlaylistRegEx = /playlist\/(.{22})/;

    function addressChecker(address) {
        if (address.includes('www.youtube.com')) isYouTube = true;
        if (address.includes('music.youtube.com')) isYouTubeMusic = true;
        if (address.includes('open.spotify.com')) isSpotify = true;

        if (isYouTube || isYouTubeMusic) {
            if (ytPlaylistIdRegEx.test(address)) isYouTubePlaylist = true;
        }
        if (isSpotify) {
            if (spotifyPlaylistRegEx.test(address)) isSpotifyPlaylist = true;
        }
    }
    addressChecker(address);

    function stringCleanup(string) {
        // Remove diacritics (accent marks) https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
        string = string.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");

        string = string.toLowerCase();

        // Remove words between brackets (including brackets)
        string = string.replace(/\[[^\]]+\]/g, "");

        // Remove unwanted characters
        string = string.replace(/[^a-zA-Z0-9\s]+/g, "");

        // Remove extra spaces
        string = string.replace(/ {2,}/g, " ");

        string = string.trim();

        return string;
    }

    const userAgent = navigator.userAgent + ",gzip(gfe)";
    const ytClient = {
        "userAgent": userAgent,
        "clientName": "WEB",
        "clientVersion": "2.20240123.06.00",
    };
    const ytmClient = {
        "userAgent": userAgent,
        "clientName": "WEB_REMIX",
        "clientVersion": "1.20240205.00.00"
    };
    const APIs = {
        // YouTube:
        ytUserId: 'https://www.youtube.com/account',
        ytGetPlaylist: `https://${subdomain}.youtube.com/youtubei/v1/browse`,
        ytGetMusicTrackInfo: 'https://music.youtube.com/youtubei/v1/search?key=&prettyPrint=false',
        ytCreatePlaylist: 'https://www.youtube.com/youtubei/v1/playlist/create?key=&prettyPrint=false',
        // Spotify:
        spotifyUserId: 'https://api.spotify.com/v1/me',
        spotifyToken: 'https://open.spotify.com/',
        spotifySearch: 'https://api.spotify.com/v1/search',
        spotifySearchProprietary: 'https://api-partner.spotify.com/pathfinder/v1/query',
        spotifyGetTrackInfo: 'https://api.spotify.com/v1/tracks',
        spotifyPlaylistContent: 'https://api.spotify.com/v1/playlists/playlistId/tracks',
        spotifyCreatePlaylist: 'https://api.spotify.com/v1/users/userId/playlists',
        spotifyAddPlaylist: 'https://api.spotify.com/v1/playlists/playlistId/tracks'
    };

    let SPOTIFY_AUTH_TOKEN, SPOTIFY_USER_ID;
    let YT_SAPISIDHASH = await GM_getValue('YT_SAPISIDHASH');
    let YTM_SAPISIDHASH = await GM_getValue('YTM_SAPISIDHASH');
    if (isYouTube || isYouTubeMusic) {
        try {
            YT_SAPISIDHASH = await getSApiSidHash('https://www.youtube.com');
            GM_setValue('YT_SAPISIDHASH', YT_SAPISIDHASH);

            YTM_SAPISIDHASH = await getSApiSidHash('https://music.youtube.com');
            GM_setValue('YTM_SAPISIDHASH', YTM_SAPISIDHASH);
        } catch (error) {
            console.log(error);
        }
    }
    console.log(`'YT_SAPISIDHASH:\n${YT_SAPISIDHASH}\n\nYTM_SAPISIDHASH:\n${YTM_SAPISIDHASH}`);


    let goBackFragment = '#go_back_fragment';
    let autoRunFragment = '#sty_autorun';
    if (address.includes(goBackFragment)) window.location.href = await GM_getValue('backUrl');



    // MENU SETUP:
    let menuTitles = {
        YtS: '🔄 YouTube to Spotify 🔄',
        StY: '🔄 Spotify to YouTube 🔄'
    };
    let YtS_ID, StY_ID;
    const callback = () => {
        addressChecker(address);
        if (isYouTubePlaylist) {
            YtS_ID = GM_registerMenuCommand(menuTitles.YtS, YtS);
        } else {
            GM_unregisterMenuCommand(YtS_ID);
        }

        if (isSpotifyPlaylist) {
            StY_ID = GM_registerMenuCommand(menuTitles.StY, StY);
        } else {
            GM_unregisterMenuCommand(StY_ID);
        }
    };
    callback();

    // Register/unregister menu functions on address change, and auto run logic
    let autoRunConditions;
    const observer = new MutationObserver(() => {
        autoRunConditions = document.querySelector('[data-testid="entityTitle"]') && GM_getValue('backUrl');
        if (autoRunConditions) {
            GM_setValue('backUrl', undefined); // Clear backUrl
            StY();
        }
        if (location.href !== address) {
            address = location.href;
            callback();
        }
    });
    observer.observe(document, {
        subtree: true,
        childList: true
    });


    let UI, ytUserId, operations;
    // YouTube to Spotify
    async function YtS() {
        try {
            // Get the title of the YouTube playlist
            let yt_playlistTitle = await getYtPlaylistTitle();
            console.log('YouTube Playlist Title:', yt_playlistTitle);


            // User confirmation
            if (confirm(`Convert "${yt_playlistTitle}" to Spotify?`)) {
                // Add close tab confirmation
                window.addEventListener("beforeunload", closeConfirmation);
                // Unregister the menu command
                GM_unregisterMenuCommand(YtS_ID);
                // Set the operations
                operations = [
                    `Getting Spotify tokens`,
                    `Getting YouTube playlist songs`,
                    `Converting songs to Spotify`,
                    `Adding playlist to Spotify`
                ];
                // Create the UI
                UI = createUI(operations);



                // OP-1
                UI.centerDiv.querySelector('.op-1').style.opacity = 1;
                // Spotify tokens
                const spotifyTokens = await getSpotifyTokens();
                SPOTIFY_USER_ID = spotifyTokens.usernameId;
                SPOTIFY_AUTH_TOKEN = spotifyTokens.accessToken;
                UI.centerDiv.querySelector('.op-1').textContent += ' ✅';



                // OP-2
                UI.centerDiv.querySelector('.op-2').style.opacity = 1;
                // YouTube Playlist ID
                const yt_playlistId = address.match(ytPlaylistIdRegEx)[1];
                console.log('YouTube Playlist ID:', yt_playlistId);
                // YouTube User ID (Needed for multiple accounts)
                ytUserId = await getYtUserId();
                console.log('YouTube User ID:', ytUserId);
                // Playlist video titles
                const yt_playlistTrackTitles = await getYtPlaylistContent(yt_playlistId);
                const yt_totalTracks = yt_playlistTrackTitles.length;
                console.log('YouTube Playlist Titles:', yt_playlistTrackTitles);
                UI.centerDiv.querySelector('.op-2').textContent += ` ✅`;



                // OP-3
                UI.centerDiv.querySelector('.op-3').style.opacity = 1;
                const spotify_trackIds = [];
                const spotify_NotFound = [];
                for (let [index, entry] of yt_playlistTrackTitles.entries()) {
                    UI.centerDiv.querySelector('.op-3').textContent = `${operations[2]} (${index + 1}/${yt_totalTracks})`;
                    let ytm_trackInfo = await getYtMusicTrackInfo(stringCleanup(entry));
                    let spotify_trackId;
                    console.log('YouTube Music Track Info:', ytm_trackInfo);
                    if (ytm_trackInfo.artist) { // If audio only track found on YouYube Music
                        console.log('Spotify Query:', ytm_trackInfo);
                        spotify_trackId = await searchSpotify(ytm_trackInfo);
                        // Try proprietary search if regular API search fails
                        if (spotify_trackId == null) {
                            const spotifyQuery = stringCleanup(ytm_trackInfo.title + ' ' + ytm_trackInfo.artist);
                            console.log('Spotify Proprietary Query:', spotifyQuery);
                            spotify_trackId = await searchSpotifyProprietary(spotifyQuery);
                        }
                    } else {
                        let spotifyQuery = entry.replace(/-[^-]*$/, ''); // Remove YouTube channel name
                        spotifyQuery = stringCleanup(spotifyQuery);
                        console.log('Spotify Query:', spotifyQuery);
                        spotify_trackId = await searchSpotifyProprietary(spotifyQuery);
                    }

                    console.log('Spotify Track ID:', spotify_trackId);

                    if (spotify_trackId) {
                        spotify_trackIds.push(spotify_trackId);
                    } else {
                        spotify_NotFound.push(yt_playlistTrackTitles[index]);
                        console.log('NOT FOUND ON SPOTIFY:', yt_playlistTrackTitles[index]);
                    }
                }
                UI.centerDiv.querySelector('.op-3').textContent += ' ✅';
                console.log('Spotify Tracks Found:', spotify_trackIds);
                console.log('NOT FOUND ON SPOTIFY:', spotify_NotFound);

                // OP-4
                UI.centerDiv.querySelector('.op-4').style.opacity = 1;
                // Create the Spotify playlist
                const spotify_playlistId = await createSpotifyPlaylist(yt_playlistTitle);
                console.log('Spotify Playlist ID Created:', spotify_playlistId);
                // Add the tracks to the Spotify playlist
                await addToSpotifyPlaylist(spotify_playlistId, spotify_trackIds);
                UI.centerDiv.querySelector('.op-4').textContent += ' ✅';



                // Update cancel button
                UI.cancelButton.onclick = () => {
                    window.open(`https://open.spotify.com/playlist/${spotify_playlistId.replace('spotify:playlist:','')}`);
                };
                UI.closeButton.onclick = () => {
                    UI.floatingDiv.remove();
                };
                UI.cancelButton.style.backgroundColor = '#1ed55f';
                UI.cancelButton.textContent = 'Open in Spotify!';


                // Re-register the menu command
                YtS_ID = GM_registerMenuCommand(menuTitles.YtS, YtS);
                // Remove close tab confirmation
                window.removeEventListener("beforeunload", closeConfirmation);
            }
        } catch (error) {
            console.log('🔄🔄🔄', error);
            errorHandler(error);
        }
    }

    // Spotify to YouTube
    async function StY() {
        try {
            // SAPISIDHASH check
            if (YT_SAPISIDHASH == undefined) {
                alert(`To collect a token this page will be redirected to YouTube then back here after you click 'Ok' (this will only be needed once), please make sure you are signed in to YouTube for this to work..`);
                GM_setValue('backUrl', address + autoRunFragment); // Add autorun fragment
                throw new CustomError({message:'SAPISIDHASH not found', url:'', code:0});
            }

            // Spotify Playlist ID
            let spotify_playlistTitle = document.querySelector('[data-testid="entityTitle"]').innerText;
            console.log('Spotify Playlist Title:', spotify_playlistTitle);

            // User confirmation
            if (confirm(`Convert playlist "${spotify_playlistTitle}" to YouTube?`)) {
                // Add close tab confirmation
                window.addEventListener("beforeunload", closeConfirmation);
                // Unregister the menu command
                GM_unregisterMenuCommand(StY_ID);
                // Set the operations
                operations = [
                    `Getting Spotify tokens`,
                    `Getting Spotify playlist info`,
                    `Converting songs to YouTube`,
                    `Adding playlist to YouTube`
                ];
                // Create the UI
                UI = createUI(operations);



                // OP-1
                UI.centerDiv.querySelector('.op-1').style.opacity = 1;
                // Spotify tokens
                const spotifyTokens = await getSpotifyTokens();
                SPOTIFY_USER_ID = spotifyTokens.usernameId;
                SPOTIFY_AUTH_TOKEN = spotifyTokens.accessToken;
                UI.centerDiv.querySelector('.op-1').textContent += ' ✅';



                // OP-2
                UI.centerDiv.querySelector('.op-2').style.opacity = 1;
                const spotify_playlistId = address.match(spotifyPlaylistRegEx)[1];
                console.log('Spotify Playlist ID:', spotify_playlistId);
                // Get the Spotify playlist content
                const spotify_trackIds = await getSpotifyPlaylistContent(spotify_playlistId);
                if (spotify_trackIds.length == 0) throw new CustomError({message:'Empty playlist', url:'', code:1});
                console.log('Spotify Track IDs:', spotify_trackIds);
                UI.centerDiv.querySelector('.op-2').textContent += ` (${spotify_trackIds.length}/${spotify_trackIds.length}) ✅`;

                // OP-3
                UI.centerDiv.querySelector('.op-3').style.opacity = 1;
                const yt_trackIds = [];
                const yt_NotFound = [];
                for (const [index, entry] of spotify_trackIds.entries()) {
                    UI.centerDiv.querySelector('.op-3').textContent = `${operations[2]} (${index + 1}/${spotify_trackIds.length})`;
                    const spotifY_trackInfo = await getSpotifyTrackInfo(entry.replace('spotify:track:', ''));
                    console.log('Spotify Track Info:', spotifY_trackInfo);
                    const ytmQuery = spotifY_trackInfo.title + ' ' + spotifY_trackInfo.artist;
                    console.log('YouTube Music Query:', ytmQuery);
                    const ytm_trackId = (await getYtMusicTrackInfo(ytmQuery)).ytmId;
                    console.log('YouTube Music Track ID:', ytm_trackId);
                    if (ytm_trackId) {
                        yt_trackIds.push(ytm_trackId);
                    } else {
                        yt_NotFound.push(ytmQuery);
                    }
                }
                UI.centerDiv.querySelector('.op-3').textContent += ' ✅';
                console.log('YouTube Tracks Found:', yt_trackIds);
                console.log('NOT FOUND ON YOUTUBE:', yt_NotFound);

                // OP-4
                UI.centerDiv.querySelector('.op-4').style.opacity = 1;
                // YouTube User ID (Needed for multiple accounts)
                ytUserId = await getYtUserId();
                console.log('YouTube User ID:', ytUserId);
                // Create the YouTube playlist
                const yt_playlistId = await createYtPlaylist(spotify_playlistTitle, yt_trackIds);
                console.log('YouTube Playlist ID:', yt_playlistId);
                UI.centerDiv.querySelector('.op-4').textContent += ' ✅';



                // Update cancel button
                UI.cancelButton.onclick = () => {
                    window.open(`https://www.youtube.com/playlist?list=${yt_playlistId}`);
                };
                UI.closeButton.onclick = () => {
                    UI.floatingDiv.remove();
                };
                UI.cancelButton.style.backgroundColor = '#ff0000';
                UI.cancelButton.style.color = '#ffffff';
                UI.cancelButton.textContent = 'Open in YouTube!';


                // Re-register the menu command
                StY_ID = GM_registerMenuCommand(menuTitles.StY, StY);
                // Remove close tab confirmation
                window.removeEventListener("beforeunload", closeConfirmation);
            }
        } catch (error) {
            console.log('🔄🔄🔄', error);
            errorHandler(error);
        }
    }



    // YOUTUBE FUNCTIONS:
    async function getYtPlaylistTitle() {
        // Static playlist page
        let playlistTitle = document.querySelector('.metadata-wrapper yt-formatted-string') || document.querySelector('#header .title');
        // Playing playlist page
        if (address.includes('watch?v=')) playlistTitle = document.querySelector('#header-description a[href*="playlist?list="]') || document.querySelector('#tab-renderer .subtitle');
        playlistTitle = playlistTitle.innerText;
        return playlistTitle;
    }

    async function getYtPlaylistContent(playlistId) {
        let authorization;
        if (isYouTube) authorization = `SAPISIDHASH ${YT_SAPISIDHASH}`;
        if (isYouTubeMusic) authorization = `SAPISIDHASH ${YTM_SAPISIDHASH}`;
        const requestUrl = APIs.ytGetPlaylist;
        const headers = {
            "accept": "*/*",
            "authorization": authorization,
            "x-goog-authuser": ytUserId,
        };
        const context = {
            "client": ytmClient
        };
        let index = 1;
        let trackNames = [];
        playlistId = 'VL' + playlistId;
        let continuation, matches;
        const trackNamesRegEx = /(?:"label":"Play\s)(.+?)(?:\s-\s\d+\s(?:minutes?|hours?))/g;
        const continuationRegEx = /(?<="continuation":").+?(?=")/g;
        const response = await fetch(`${requestUrl}?key=&prettyPrint=false`, {
            method: "POST",
            headers: headers,
            body: JSON.stringify({
                "context": context,
                "browseId": playlistId
            })
        });
        if (response.status == 200) {
            const responseText = await response.text();
            matches = responseText.matchAll(trackNamesRegEx);
            continuation = responseText.match(continuationRegEx);
            if (continuation.length == 1) {
                continuation = false;
            } else {
                continuation = continuation[0];
            }
            for (let title of matches) {
                document.querySelector('.op-2').textContent = `${operations[1]} (${index})`;
                trackNames.push(title[1]);
                index++;
            }
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:2});
        }
        while (continuation) {
            const continuationResponse = await fetch(`${requestUrl}?ctoken=${continuation}&continuation=${continuation}&type=next&prettyPrint=false`, {
                method: "POST",
                headers: headers,
                body: JSON.stringify({
                    "context": context
                })
            });
            if (continuationResponse.status == 200) {
                const continuationResponseText = await continuationResponse.text();
                if (continuationResponseText.includes('musicPlaylistShelfContinuation')) matches = continuationResponseText.matchAll(trackNamesRegEx);
                continuation = continuationResponseText.match(continuationRegEx);
                for (let title of matches) {
                    document.querySelector('.op-2').textContent = `${operations[1]} (${index})`;
                    trackNames.push(title[1]);
                    index++;
                }
            } else {
                console.log(continuationResponse);
                throw new CustomError({message:'', url:continuationResponse.finalUrl, code:3});
            }
        }
        return trackNames;
    }

    async function getYtMusicTrackInfo(query) {
        query = stringCleanup(query);
        const response = await
        GM.xmlHttpRequest({
            method: "POST",
            url: APIs.ytGetMusicTrackInfo,
            headers: {
                "content-type": "application/json",
            },
            data: JSON.stringify({
                "context": {
                    "client": ytmClient
                },
                "query": query,
                "params": "EgWKAQIIAWoKEAMQBBAKEBEQEA%3D%3D" // Songs only
            })
        });
        if (response.status == 200) {
            const responseText = response.responseText;
            const ytmId = responseText.match(/(?:videoId":")(.+?)(?:")/);
            const trackMatch = responseText.match(/"accessibilityData":{"label":"Play (.+?)"/);
            let ytmTitle, ytmArtist;
            if (trackMatch) {
                let words = trackMatch[1].split(' - ');
                ytmArtist = words.pop();
                ytmTitle = words.join(' - ');
            }
            if (query.includes(stringCleanup(ytmTitle))) return {ytmId: ytmId[1], title: ytmTitle, artist: ytmArtist};
            return {ytmId: null, title: query, artist: null};
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:4});
        }
    }

    async function getYtUserId() { // Needed for multiple accounts
        const response = await
        GM.xmlHttpRequest({
            method: "GET",
            url: APIs.ytUserId,
        });
        if (response.finalUrl == APIs.ytUserId) {
            const responseText = response.responseText;
            const userId = responseText.match(/myaccount\.google\.com\/u\/(\d)/);
            if (userId) return userId[1];
            return 0;
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:5});
        }
    }

    async function createYtPlaylist(playlistTitle, videoIds) {
        const response = await GM.xmlHttpRequest({
            method: "POST",
            url: APIs.ytCreatePlaylist,
            "headers": {
                "accept": "*/*",
                "accept-language": "en-US,en;q=0.9",
                "authorization": `SAPISIDHASH ${YT_SAPISIDHASH}`,
                "content-type": "application/json",
                "x-goog-authuser": ytUserId,
                "x-origin": "https://www.youtube.com",
                "x-youtube-bootstrap-logged-in": "true"
            },
            data: JSON.stringify({
                "context": {
                    "client": ytClient
                },
                "title": playlistTitle,
                "videoIds": videoIds
            })
        });
        if (response.status == 200) {
            const responseJson = JSON.parse(response.responseText);
            const playlistId = responseJson.playlistId;
            return playlistId;
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:6});
        }
    }

    async function getSApiSidHash(origin) { // https://gist.github.com/eyecatchup/2d700122e24154fdc985b7071ec7764a
        function sha1(str) {
            return window.crypto.subtle.digest("SHA-1", new TextEncoder("utf-8").encode(str)).then(buf => {
                return Array.prototype.map.call(new Uint8Array(buf), x => (('00' + x.toString(16)).slice(-2))).join('');
            });
        }
        const TIMESTAMP_MS = Date.now();
        const digest = await sha1(`${TIMESTAMP_MS} ${document.cookie.split('SAPISID=')[1].split('; ')[0]} ${origin}`);
        return `${TIMESTAMP_MS}_${digest}`;
    }



    // SPOTIFY FUNCTIONS:
    async function getSpotifyPlaylistContent(playlistId) { // For multiple accounts
        let requestUrl = APIs.spotifyPlaylistContent.replace('playlistId', playlistId);
        const limit = 100;
        const offset = 0;
        const params = `?offset=${offset}&limit=${limit}`;
        let next = requestUrl + params;
        let trackIds = [];
        while (next) { // Keep looping until next page is null
            const response = await
            GM.xmlHttpRequest({
                method: "GET",
                url: next,
                headers: {
                    'Authorization': `Bearer ${SPOTIFY_AUTH_TOKEN}`,
                    'Content-Type': 'application/json'
                }
            });
            if (response.status == 200) {
                const responseJson = JSON.parse(response.responseText);
                next = responseJson.next; // Next page of tracks
                trackIds = trackIds.concat((responseJson.items.map(item => item.track.uri)));
            } else {
                console.log(response);
                throw new CustomError({message:'', url:APIs.spotifyPlaylistContent, code:7});
            }
        }
        return trackIds;
    }

    async function createSpotifyPlaylist(playlistTitle) {
        let requestUrl = APIs.spotifyCreatePlaylist.replace('userId', SPOTIFY_USER_ID);
        const playlistData = JSON.stringify({
            name: playlistTitle,
            description: '',
            public: false,
        });
        const response = await GM.xmlHttpRequest({
            method: "POST",
            url: requestUrl,
            headers: {
                'Authorization': `Bearer ${SPOTIFY_AUTH_TOKEN}`,
                'Content-Type': 'application/json'
            },
            data: playlistData
        });
        if (response.status == 201) {
            const responseJson = JSON.parse(response.responseText);
            const playlistId = responseJson.uri.replace('spotify:playlist:', '');
            return playlistId;
        } else {
            console.log(response);
            throw new CustomError({message:'', url:APIs.spotifyCreatePlaylist, code:8});
        }
    }

    async function addToSpotifyPlaylist(playlistId, trackIds) {
        let requestUrl = APIs.spotifyAddPlaylist.replace('playlistId', playlistId);
        while (trackIds.length) { // Keep looping until array is empty
            const trackData = JSON.stringify({
                uris: trackIds.splice(0, 100), // Get first 100 tracks
            });
            const response = await GM.xmlHttpRequest({
                method: "POST",
                url: requestUrl,
                headers: {
                    'Authorization': `Bearer ${SPOTIFY_AUTH_TOKEN}`,
                    'Content-Type': 'application/json'
                },
                data: trackData
            });
            if (response.status == 201) {
                const responseJson = JSON.parse(response.responseText);
            } else {
                console.log(response);
                throw new CustomError({message:'', url:APIs.spotifyAddPlaylist, code:9});
            }
        }
    }

    async function getSpotifyTokens() {
        let accessToken;
        if (isYouTube || isYouTubeMusic) {
            const tokenResponse = await
            GM.xmlHttpRequest({
                method: "GET",
                url: APIs.spotifyToken
            });
            if (tokenResponse.status == 200) {
                const tokenResponseText = await tokenResponse.responseText;
                const parser = new DOMParser();
                const htmlDoc = parser.parseFromString(tokenResponseText, 'text/html');
                accessToken = JSON.parse(htmlDoc.querySelector('script#session').innerHTML).accessToken;
            } else {
                console.log(tokenResponse);
                throw new CustomError({message:'', url:tokenResponse.finalUrl, code:10});
            }
        } else {
            accessToken = JSON.parse(document.querySelector('script#session').innerHTML).accessToken;
        }
        const usernameResponse = await GM.xmlHttpRequest({
            method: 'GET',
            url: APIs.spotifyUserId,
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        });
        if (usernameResponse.status == 200) {
            const usernameId = JSON.parse(usernameResponse.responseText).id;
            return {
                usernameId: usernameId,
                accessToken: accessToken
            };
        } else {
            console.log(usernameResponse);
            throw new CustomError({message:'', url:usernameResponse.finalUrl, code:11});
        }
    }

    async function searchSpotifyProprietary(query) {
        const variables = escape(`{"searchTerm":"${query}","offset":0,"limit":10,"numberOfTopResults":0,"includeAudiobooks":false}`);
        const extensions = escape(`{"persistedQuery":{"version":1,"sha256Hash":"16c02d6304f5f721fc2eb39dacf2361a4543815112506a9c05c9e0bc9733a679"}}`);
        const response = await GM.xmlHttpRequest({
            method: "GET",
            url: `${APIs.spotifySearchProprietary}?operationName=searchTracks&variables=${variables}&extensions=${extensions}`,
            headers: {
                accept: "application/json",
                authorization: `Bearer ${SPOTIFY_AUTH_TOKEN}`,
                "content-type": "application/json;charset=UTF-8",
                "sec-ch-ua": "\"Not A(Brand\";v=\"99\", \"Google Chrome\";v=\"121\", \"Chromium\";v=\"121\"",
                "spotify-app-version": "1.2.31.0-unknown"
            }
        });
        if (response.status == 200) {
            const trackId = response.responseText.match(/spotify:track:.{22}/)[0];
            return trackId;
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:12});
        }
    }

    async function searchSpotify(query) {
        let trackId;
        const response = await GM.xmlHttpRequest({
            method: "GET",
            url: `${APIs.spotifySearch}?q=track:${query.title} artist:${query.artist}&type=track,artist`,
            headers: {
                'Authorization': `Bearer ${SPOTIFY_AUTH_TOKEN}`,
            }
        });
        if (response.status == 200) {
            const resonseText = response.responseText;
            const responseJson = JSON.parse(resonseText);
            if (responseJson.tracks.items.length !== 0) trackId = responseJson.tracks.items[0].uri;
            if (trackId) return trackId;
            return null;
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:13});
        }
    }

    async function getSpotifyTrackInfo(trackId) {
        const response = await GM.xmlHttpRequest({
            method: "GET",
            url: `${APIs.spotifyGetTrackInfo}/${trackId}`,
            headers: {
                'Authorization': `Bearer ${SPOTIFY_AUTH_TOKEN}`,
            }
        });
        if (response.status == 200) {
            const responseJson = JSON.parse(response.responseText);
            const spotifyTitle = responseJson.name;
            const spotifyArtist = responseJson.artists[0].name;
            if (spotifyTitle && spotifyArtist) return {title: spotifyTitle, artist: spotifyArtist};
            return null;
        } else {
            console.log(response);
            throw new CustomError({message:'', url:response.finalUrl, code:14});
        }
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址