SoundCloud Cover Downloader

Use Alt+C to download cover image from soundcloud

// ==UserScript==
// @name         SoundCloud Cover Downloader
// @name:zh-CN   SoundCloud封面下载器
// @namespace    http://tampermonkey.net/
// @version      2024-10-09
// @description  Use Alt+C to download cover image from soundcloud
// @description:zh-CN  使用Alt+C下载SoundCloud的封面图片
// @author       Nolca
// @license      MIT
// @match        https://soundcloud.com/*
// @icon         https://soundcloud.com/favicon.ico
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @noframes
// ==/UserScript==
const DEBUG = true;
const cli = { x: 0, y: 0 };
const l = {
    leaveBlankToCopy: {
        "zh-CN": "留空以复制封面URL",
        "en-US": "Leave Filename blank to copy cover URL"
    },
    fileName: {
        "zh-CN": "文件名",
        "en-US": "Filename"
    },
    useAltC: {
        "zh-CN": "Alt+C下载当前封面;\n打开播放列表后,下载鼠标指向的封面",
        "en-US": "Alt+C to download current cover;\nOpen playlist and download the cover under mouse cursor"
    },
    alwaysRename: {
        "zh-CN": "总是重命名",
        "en-US": "Always Rename"
    },
    alwaysRenameTip: {
        "zh-CN": "每次按下Alt+C时,都会弹窗询问文件名",
        "en-US": "Every time you press Alt+C, a prompt will ask for filename"
    }
};
let LANG = GM_getValue('userLang') || navigator.language || navigator.userLanguage;
 
 
let menu_tip = GM_registerMenuCommand(
    l.useAltC[LANG],
    function () {
        GM_unregisterMenuCommand(menu_tip);
    },
    {
        id: 'menu_tip',
        autoClose: false,
        title: l.useAltC[LANG]
    }
);
 
function menu_directDownload_click() {
    GM_setValue('alwaysRename', !GM_getValue('alwaysRename'));
    // GM_unregisterMenuCommand(menu_directDownload);
    menu_directDownload = menu_directDownload_regist();
};
function menu_directDownload_regist() {
    return GM_registerMenuCommand(
        `${l.alwaysRename[LANG]}: ${GM_getValue('alwaysRename') ? '✅' : '❌'}`,
        menu_directDownload_click,
        {
            id: 'menu_directDownload',
            accessKey: 'r',
            autoClose: false,
            title: l.alwaysRenameTip[LANG]
        }
    );
};
if (GM_getValue('alwaysRename') === undefined) GM_setValue('alwaysRename', true);
var menu_directDownload = menu_directDownload_regist();
 
 
document.removeEventListener('keydown', event_keydown);
document.removeEventListener('mousemove', event_mousemove);
 
async function download_xhr2_blob(url, filenameDefault = "cover_soundcloud") {
    let filename = filenameDefault;
    if (GM_getValue('alwaysRename') === true) {
        filename = prompt(`${l.leaveBlankToCopy[LANG]}:\n${url}\n\n${l.fileName[LANG]}:`, filenameDefault);
        if (filename === "") {
            // Copy URL to clipboard
            navigator.clipboard.writeText(url.replace(/-t500x500/, '-original'));
            return;
        } else if (filename === null) return;
    }
 
    const res = await fetch(url);
    const blob = await res.blob();
 
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
 
    URL.revokeObjectURL(a.href);
    a.remove();
}
 
function get_coverUrl_from(span, regex = /-t50x50/) {
    if (span) {
        const coverUrl = span.style.backgroundImage.match(/url\("(.+)"\)/)[1];
        // 将-t50x50替换为-t500x500
        return coverUrl.replace(regex, '-t500x500');
    } else {
        console.error("span not found");
    }
}
 
function get_wrapper_coverSpan() {
    const elem = document.elementFromPoint(cli.x, cli.y);
    const parentWrapper = elem.closest('div.queue__itemWrapper');
    const coverPic = parentWrapper.querySelector("div > div.queueItemView__artwork > div.image.queueItemView__artworkImage > span");
    return {
        wrapper: parentWrapper,
        span: coverPic
    };
}
 
function event_downloader_cover() {
    let author, title, coverUrl;
    const elem_playlist = document.querySelector("#app > div.playControls.g-z-index-control-bar.m-visible.m-queueVisible > section > div > div.playControls__queue > div > div.queue__scrollable.g-scrollable.g-scrollable-v > div.queue__scrollableInner.g-scrollable-inner > div > div > div");
    if (elem_playlist) {
        // alert("playList opened");
        const { wrapper, span } = get_wrapper_coverSpan();
        coverUrl = get_coverUrl_from(span);
 
        const details = wrapper.querySelector("div > div.queueItemView__details");
        author = details.querySelector("div.queueItemView__meta > a.queueItemView__username").innerHTML;
        title = details.querySelector("div.queueItemView__title > a").innerHTML;
 
 
    } else {
        // alert("playList closed");
        const span = document.querySelector("#app > div.playControls.g-z-index-control-bar.m-visible > section > div > div.playControls__elements > div.playControls__soundBadge > div > a > div > span");
        coverUrl = get_coverUrl_from(span);
 
        const select = document.querySelector("#app > div.playControls.g-z-index-control-bar.m-visible > section > div > div.playControls__elements > div.playControls__soundBadge > div > div.playbackSoundBadge__titleContextContainer.sc-mr-3x");
        author = select.querySelector("a").innerHTML;
        title = select.querySelector("div > a > span:nth-child(2)").innerHTML;
    }
    const filename = `@${author} - ${title}☁️`;
    console.log("event_downloader_cover: coverUrl=", coverUrl);
    download_xhr2_blob(coverUrl, filename).then().catch(console.error);
}
 
function is_alt(lowerCase, event) {
    return event.altKey && event.key === lowerCase || event.altKey && event.key === lowerCase.toUpperCase();
}
function is_alt_shift(lowerCase, event) {
    return event.altKey && event.shiftKey && event.key === lowerCase || event.altKey && event.shiftKey && event.key === lowerCase.toUpperCase();
}
function event_keydown(event) {
    if (is_alt('c', event)) {
        event_downloader_cover();
    } else if (is_alt('s', event)) {
        event_downloader_song();
    }
}
function event_mousemove(event) {
    cli.x = event.clientX;
    cli.y = event.clientY;
    // console.log(cli);
}
 
document.addEventListener('keydown', event_keydown);
document.addEventListener('mousemove', event_mousemove);