YouTube / Spotify Playlists Converter

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

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

// ==UserScript==
// @name         YouTube / Spotify Playlists Converter
// @version      1.0
// @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      artemislena.eu
// @connect      api.song.link
// @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() {
    // 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() {
        if (confirm(`Cancel conversion?`)) {
            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);


    // 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;
}

.floating-div .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;
}



// GLOBALS:
let address = window.location.href;
const userAgent = navigator.userAgent + ",gzip(gfe)";
var SPOTIFY_AUTH_TOKEN, SPOTIFY_USER_ID;
const APIs = {
    // Converter:
    platformConverter: 'https://api.song.link/v1-alpha.1/links?url=',
    // YouTube:
    ytUserId: 'https://www.youtube.com/account',
    ytMusicId: 'https://music.youtube.com/youtubei/v1/search?key=&prettyPrint=false',
    ytGetPlaylist: 'https://yt.artemislena.eu/api/v1/playlists/',
    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/',
    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 SAPISIDHASH = await GM_getValue('SAPISIDHASH');
if (address.includes('youtube.com')) {
    try {
        SAPISIDHASH = await getSApiSidHash('https://www.youtube.com');
        GM_setValue('SAPISIDHASH', SAPISIDHASH);
    } catch (error) {}
}
console.log(SAPISIDHASH);

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



// MENU SETUP:
let YtS_ID, StY_ID;
const callback = () => {
    // If playlist is YouTube
    if (/list=.{34}/.test(address)) {
        YtS_ID = GM_registerMenuCommand('🔄 YouTube to Spotify 🔄', YtS);
    } else {
        GM_unregisterMenuCommand(YtS_ID);
    }

    // If playlist is Spotify
    if (/playlist\/.{22}/.test(address)) {
        StY_ID = GM_registerMenuCommand('🔄 Spotify to YouTube 🔄', 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
});



// YouTube to Spotify
let ytUserId;
async function YtS() {
    try {
        // Get the title of the YouTube playlist
        let yt_playlistTitle = document.querySelector('.metadata-wrapper yt-formatted-string') || document.querySelector('#header .title');
        if (address.includes('watch?v=')) yt_playlistTitle = document.querySelector('#header-description a[href*="playlist?list="]') || document.querySelector('#tab-renderer .subtitle');
        yt_playlistTitle = yt_playlistTitle.innerText;
        console.log(yt_playlistTitle);

        // Yser confirmation
        if (confirm(`Convert "${yt_playlistTitle}" to Spotify?`)) {
            // Unregister the menu command
            GM_unregisterMenuCommand(YtS_ID);

            // Create the UI
            let UI = createUI();

            // Define the operations and generate spans
            let operations = [`Getting Spotify tokens`, `Getting YouTube playlist info`, `Converting songs using Songlink's API`, `Adding playlist to Spotify`];
            let spans = generateSpans(operations);

            // Append the UI elements to floating div
            UI.centerDiv.append(...spans);
            UI.floatingDiv.appendChild(UI.centerDiv);
            UI.floatingDiv.appendChild(UI.cancelButton);
            UI.floatingDiv.appendChild(UI.closeButton);

            // Display the floating div
            UI.floatingDiv.style.display = 'flex';

            // Start the first operation: Getting Spotify tokens
            UI.centerDiv.querySelector('.op-1').style.opacity = 1;
            const spotifyTokens = await getSpotifyTokens();

            // Set the Spotify user ID and auth token
            SPOTIFY_USER_ID = spotifyTokens.usernameId;
            SPOTIFY_AUTH_TOKEN = spotifyTokens.accessToken;

            // Update the Spotify playlist changes API
            // APIs.spotifyPlaylistChanges = APIs.spotifyPlaylistChanges.replace('usernameId', SPOTIFY_USER_ID);

            // Mark the first operation as complete
            UI.centerDiv.querySelector('.op-1').textContent += ' ✅';

            // Start the second operation: Getting YouTube playlist info
            UI.centerDiv.querySelector('.op-2').style.opacity = 1;
            const yt_playlistId = address.match(/list=(.{34})/)[1];
            console.log(yt_playlistId);

            // Get the YouTube playlist content
            const yt_playlistTrackTitles = await getYtPlaylistContent(yt_playlistId);
            console.log(yt_playlistTrackTitles);

            // Get YouTube User ID (for multiple accounts)
            ytUserId = await getYtUserId();
            console.log(ytUserId);

            // Initialize an array to store the YouTube music IDs
            const yt_musicIds = [];

            // Loop through the YouTube playlist track titles
            for (const [index, entry] of yt_playlistTrackTitles.entries()) {
                // Update the UI with the progress
                UI.centerDiv.querySelector('.op-2').textContent = `${operations[1]} (${index + 1}/${yt_playlistTrackTitles.length})`;

                // Get the YouTube music ID and add it to the array
                yt_musicIds.push(await getYtMusicId(entry));
            }

            // Mark the second operation as complete
            UI.centerDiv.querySelector('.op-2').textContent += ' ✅';

            // Start the third operation: Converting songs using Songlink's API
            UI.centerDiv.querySelector('.op-3').style.opacity = 1;

            // Initialize arrays to store the Spotify track IDs and tracks not found
            const spotify_trackIds = [];
            const tracksNotFound = [];

            // Loop through the YouTube music IDs
            for (const [index, entry] of yt_musicIds.entries()) {

                // Update the UI with the progress
                UI.centerDiv.querySelector('.op-3').textContent = `${operations[2]} (${index + 1}/${yt_playlistTrackTitles.length})`;

                // Convert the platform and get the Spotify track ID
                const spotify_trackId = await convertPlatform(entry);

                // If the Spotify track ID is found, add it to the array
                // Otherwise, add the track title to the tracks not found array
                if (spotify_trackId) {
                    spotify_trackIds.push(spotify_trackId);
                } else {
                    tracksNotFound.push(yt_playlistTrackTitles[index]);
                    console.log('NOT FOUND ON SPOTIFY:', yt_playlistTrackTitles[index]);
                }
            }

            // Mark the third operation as complete
            UI.centerDiv.querySelector('.op-3').textContent += ' ✅';
            console.log(spotify_trackIds);

            // Start the fourth operation: Adding playlist to Spotify
            UI.centerDiv.querySelector('.op-4').style.opacity = 1;

            // Create the Spotify playlist
            const spotify_playlistId = await createSpotifyPlaylist(yt_playlistTitle);
            console.log(spotify_playlistId);

            // Add the tracks to the Spotify playlist
            await addToSpotifyPlaylist(spotify_playlistId, spotify_trackIds);

            // Mark the fourth operation as complete
            UI.centerDiv.querySelector('.op-4').textContent += ' ✅';

            // Set the cancel button's onclick event to open the Spotify playlist
            UI.cancelButton.onclick = () => {
                window.open(`https://open.spotify.com/playlist/${spotify_playlistId.replace('spotify:playlist:','')}`);
            };

            // Set the cancel button's onclick event to remove floatingDiv
            UI.closeButton.onclick = () => {
                UI.floatingDiv.remove();
            };

            // Style the cancel button
            UI.cancelButton.style.backgroundColor = '#1ed55f';
            UI.cancelButton.textContent = 'Open in Spotify!';

            // Re-register the menu command
            YtS_ID = GM_registerMenuCommand('🔄 YouTube to Spotify 🔄', YtS);
        }
    } catch (error) {
        console.log('🔄🔄🔄', error);
        // Error handling:
        if (error.message == APIs.spotifyToken) alert('⛔ Could not get Spotify token: Make sure you are signed in to Spotify and try again..');
        if (error.message == APIs.spotifyUserId) alert('⛔ Could not get Spotify User ID: Make sure you are signed in to Spotify and try again..');
        if (error.message == APIs.ytGetPlaylist) alert('⛔ Could not get playlist info: Make sure the playlist is not set to "Private" and try again..');
        if (error.message == APIs.spotifyCreatePlaylist) alert('⛔ Could not create Spotify playlist..');
        if (error.message == APIs.spotifyAddPlaylist) alert('⛔ Could not add songs to Spotify playlist..');
    }
}

// Spotify to YouTube
async function StY() {
    try {
        // SAPISIDHASH check
        if (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 Error('SAPISIDHASH not found');
        }

        // Get the title of the Spotify playlist
        let spotify_playlistTitle = document.querySelector('[data-testid="entityTitle"]').innerText;
        console.log(spotify_playlistTitle);

        // Confirm with the user if they want to convert the playlist to YouTube
        if (confirm(`Convert playlist "${spotify_playlistTitle}" to YouTube?`)) {
            // Unregister the menu command
            GM_unregisterMenuCommand(StY_ID);

            // Create the UI
            let UI = createUI();

            // Define the operations and generate spans
            let operations = [`Getting Spotify tokens`, `Getting Spotify playlist info`, `Converting songs using Songlink's API`, `Adding playlist to YouTube`];
            let spans = generateSpans(operations);

            // Append the UI elements to floating div
            UI.centerDiv.append(...spans);
            UI.floatingDiv.appendChild(UI.centerDiv);
            UI.floatingDiv.appendChild(UI.cancelButton);
            UI.floatingDiv.appendChild(UI.closeButton);

            // Display the floating div
            UI.floatingDiv.style.display = 'flex';

            // Start the first operation: Getting Spotify tokens
            UI.centerDiv.querySelector('.op-1').style.opacity = 1;
            const spotifyTokens = await getSpotifyTokens();
            console.log(spotifyTokens);

            // Set the Spotify user ID and auth token
            SPOTIFY_USER_ID = spotifyTokens.usernameId;
            SPOTIFY_AUTH_TOKEN = spotifyTokens.accessToken;

            // Mark the first operation as complete
            UI.centerDiv.querySelector('.op-1').textContent += ' ✅';

            // Start the second operation: Getting Spotify playlist info
            UI.centerDiv.querySelector('.op-2').style.opacity = 1;
            const spotify_playlistId = address.match(/playlist\/(.{22})/)[1];
            console.log(spotify_playlistId);

            // Get the Spotify playlist content
            const spotify_trackIds = await getSpotifyPlaylistContent(spotify_playlistId);
            if (spotify_trackIds.length == 0) throw new Error('Empty playlist');

            // Update the UI with the progress and mark the operation as complete
            UI.centerDiv.querySelector('.op-2').textContent += ` (${spotify_trackIds.length}/${spotify_trackIds.length}) ✅`;

            // Start the third operation: Converting songs using Songlink's API
            UI.centerDiv.querySelector('.op-3').style.opacity = 1;

            // Initialize an array to store the YouTube track IDs and tracks not found
            const yt_trackIds = [];
            const tracksNotFound = [];

            // Loop through the Spotify track IDs
            for (const [index, entry] of spotify_trackIds.entries()) {
                // Update the UI with the progress
                UI.centerDiv.querySelector('.op-3').textContent = `${operations[2]} (${index + 1}/${spotify_trackIds.length})`;

                // Convert the platform and get the YouTube track ID
                let yt_trackId = await convertPlatform(entry.replace('spotify:track:', ''));


                if (yt_trackId) {
                    yt_trackId = yt_trackId.replace('YOUTUBE_VIDEO::', '');
                     // Add the YouTube track ID to the array
                    yt_trackIds.push(yt_trackId);
                } else {
                    // TODO
                }
            }

            // Mark the third operation as complete
            UI.centerDiv.querySelector('.op-3').textContent += ' ✅';
            console.log(yt_trackIds);

            // Start the fourth operation: Adding playlist to YouTube
            UI.centerDiv.querySelector('.op-4').style.opacity = 1;

            // Create the YouTube playlist
            ytUserId = await getYtUserId();
            const yt_playlistId = await createYtPlaylist(spotify_playlistTitle, yt_trackIds);
            console.log(ytUserId);
            console.log(yt_playlistId);

            // Mark the fourth operation as complete
            UI.centerDiv.querySelector('.op-4').textContent += ' ✅';

            // Set the cancel button's onclick event to open the YouTube playlist
            UI.cancelButton.onclick = () => {
                window.open(`https://www.youtube.com/playlist?list=${yt_playlistId}`);
            };

            // Set the cancel button's onclick event to remove floatingDiv
            UI.closeButton.onclick = () => {
                UI.floatingDiv.remove();
            };

            // Style the cancel button
            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('🔄 Spotify to YouTube 🔄', StY);
        }
    } catch (error) {
        console.log('🔄🔄🔄', error);
        // Error handling:
        if (error.message == 'SAPISIDHASH not found') window.location.href = 'https://www.youtube.com/#go_back_fragment';
        if (error.message == 'Empty playlist') alert('⛔ Could not get playlist info: The playlist is empty!');
        if (error.message == APIs.spotifyToken) alert('⛔ Could not get Spotify token: Make sure you are signed in to Spotify and try again..');
        if (error.message == APIs.spotifyUserId) alert('⛔ Could not get Spotify User ID: Make sure you are signed in to Spotify and try again..');
        if (error.message == APIs.spotifyPlaylistContent) alert('⛔ Could not get playlist info..');
        //if (!error.message.includes(APIs.ytUserId)) alert('⛔ Could not get YouTube User ID: Make sure you are signed in to YouTube and try again..');
        if (error.message == APIs.ytCreatePlaylist) alert('⛔ Could not create YouTube playlist..');
    }
}



// YOUTUBE FUNCTIONS:
async function getYtPlaylistContent(playlistId) {
    const response = await
    GM.xmlHttpRequest({
        method: "GET",
        url: `${APIs.ytGetPlaylist}${playlistId}`,
    });
    if (response.status == 200) {
        const responseJson = JSON.parse(response.responseText);
        const videoTitles = responseJson.videos.map(video => video.title);
        return videoTitles;
    } else {
        console.log(response);
        throw new Error(response.finalUrl.slice(0, -34));
    }
}

async function getYtUserId() { // 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 Error(response.finalUrl);
    }
}

async function getYtMusicId(title) {
    const response = await
    GM.xmlHttpRequest({
        method: "POST",
        url: APIs.ytMusicId,
        headers: {
            "content-type": "application/json",
            "x-goog-authuser": ytUserId
        },
        data: JSON.stringify({
            "context": {
                "client": {
                    "userAgent": userAgent,
                    "clientName": "WEB_REMIX",
                    "clientVersion": "1.20240117.01.01"
                }
            },
            "query": title,
            "params": "EgWKAQIIAWoKEAMQBBAKEBEQEA%3D%3D" // Songs only
        })
    });
    if (response.status == 200) {
        const searchResults = response.responseText;
        const id = searchResults.match(/(?:videoId":")(.+?)(?:")/);
        if (id) return id[1];
        console.log(`${title} NOT FOUND AS SONG ONLY`);
        return null;
    } else {
        console.log(response);
        throw new Error(response.finalUrl);
    }
}

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 ${await GM_getValue('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": {
                    "userAgent": userAgent,
                    "clientName": "WEB",
                    "clientVersion": "2.20240123.06.00",
                }
            },
            "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 Error(response.finalUrl);
    }
}

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}`,
            }
        });
        if (response.status == 200) {
            const responseText = response.responseText;
            const responseJson = JSON.parse(responseText);
            next = responseJson.next; // Next page of tracks
            trackIds = trackIds.concat((responseJson.items.map(item => item.track.uri)));
        } else {
            console.log(response);
            throw new Error(APIs.spotifyPlaylistContent);
        }
    }
    console.log(trackIds);
    return trackIds;
}

async function createSpotifyPlaylist(playlistTitle) {
    let requestUrl = APIs.spotifyCreatePlaylist.replace('userId', SPOTIFY_USER_ID);
    console.log(requestUrl);
    console.log(playlistTitle);
    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 responseText = response.responseText;
        const responseJson = JSON.parse(responseText);
        const playlistId = responseJson.uri.replace('spotify:playlist:', '');
        console.log(responseJson.uri);
        return playlistId;
    } else {
        console.log(response);
        throw new Error(APIs.spotifyCreatePlaylist);
    }
}

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 responseText = response.responseText;
            const responseJson = JSON.parse(responseText);
            console.log(responseJson);
        } else {
            console.log(response);
            throw new Error(APIs.spotifyAddPlaylist);
        }
    }
}

async function getSpotifyTokens() {
    const tokenResponse = await
    GM.xmlHttpRequest({
        method: "GET",
        url: APIs.spotifyToken
    });
    let accessToken;
    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 Error(tokenResponse.finalUrl);
    }
    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 Error(usernameResponse.finalUrl);
    }
}




// PLATFORM CONVERTER FUNCTION:
async function convertPlatform(id) {
    const idIsYoutube = id.length == 11;
    const idIsSpotify = id.length == 22;
    let url;
    if (idIsYoutube) {
        url = `https://www.youtube.com/watch?v=${id}`;
    }
    if (idIsSpotify) {
        url = `https://open.spotify.com/track/${id}`;
    }
    const response = await GM.xmlHttpRequest({
        method: "GET",
        url: `${APIs.platformConverter}${url}`
    });
    if (response.status == 200) {
        const platformsResponse = JSON.parse(response.responseText);
        const spotifyAvailable = platformsResponse.linksByPlatform.hasOwnProperty('spotify');
        const ytAvailble = platformsResponse.linksByPlatform.hasOwnProperty('youtube');
        if (idIsYoutube && spotifyAvailable) {
            const spotifyTrackId = platformsResponse.linksByPlatform.spotify.nativeAppUriDesktop;
            return spotifyTrackId;
        } else if (idIsSpotify && ytAvailble) {
            const ytVideoId = platformsResponse.linksByPlatform.youtube.entityUniqueId;
            return ytVideoId;
        } else {
            return null;
        }
    } else {
        console.log(response);
        throw new Error(response.finalUrl);
    }
}
})();

QingJ © 2025

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