@music

музыка с саундклауда

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         @music
// @namespace    нету
// @version      0
// @description  музыка с саундклауда
// @author       жди
// @match        *://lolz.live/*
// @match        *://zelenka.guru/*
// @match        *://lolz.guru/*
// @match        *://lolz.market/*
// @match        *://zelenka.market/*
// @match        *://lzt.market/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=soundcloud.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        unsafeWindow
// @connect      api-v2.soundcloud.com
// ==/UserScript==

(function () {
    'use strict';

    const AUTOSEND_SETTING_KEY = "music_autosend_enabled";
    const ANON_ID_KEY = "soundcloud_anon_id";

    const SOUNDCLOUD_CLIENT_ID = 'ROEsbH8bpjaGsqvLX2uk7VjkqxNVlinH';

    const SOUNDCLOUD_APP_VERSION = '1779782131';
    const SOUNDCLOUD_APP_LOCALE = 'en';

    let autoSendEnabled = GM_getValue(AUTOSEND_SETTING_KEY, false);

    let lastQuery = '';
    let debounceTimer = null;
    let suppressObserver = false;
    let cmdId = null;

    init();

    function initObserver() {
        const observer = new MutationObserver(() => {
            if (suppressObserver) return;

            const editor = document.querySelector('.tiptap.ProseMirror');
            if (!editor) return;

            const text = editor.innerText.trim();

            if (!text.startsWith("@music" + ' ')) {
                removeResults();
                lastQuery = '';
                return;
            }

            const query = text.slice(("@music" + ' ').length).trim();

            if (!query) {
                removeResults();
                lastQuery = '';
                return;
            }

            if (query === lastQuery && document.querySelector('.music-results-renderer')) {
                return;
            }

            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(async () => {
                const currentEditor = document.querySelector('.tiptap.ProseMirror');
                if (!currentEditor) return;

                const currentText = currentEditor.innerText.trim();

                if (!currentText.startsWith("@music" + ' ')) {
                    removeResults();
                    lastQuery = '';
                    return;
                }

                const currentQuery = currentText.slice(("@music" + ' ').length).trim();

                if (!currentQuery) {
                    removeResults();
                    lastQuery = '';
                    return;
                }

                if (currentQuery === lastQuery && document.querySelector('.music-results-renderer')) {
                    return;
                }

                try {
                    const tracks = await getTracks(currentQuery);

                    lastQuery = currentQuery;
                    renderTracks(tracks);
                } catch (err) {
                    console.error('[music] req failed:', err);
                    removeResults();
                }
            }, 400);
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }

    function addStyles() {
        GM_addStyle(`
    .music-results-renderer {
        position: absolute;
        left: 60px;
        bottom: calc(100% + 8px);
        z-index: 9999;
        pointer-events: none;
    }

    .music-popup {
        pointer-events: auto;
        display: flex;
        flex-direction: column;
        gap: 6px;
        width: min(440px, calc(100vw - 80px));
        max-height: 700px;
        overflow-y: auto;
        padding: 8px;
        border-radius: 8px;
        background: #2b2b2b;
        border: 1px solid rgba(255,255,255,0.08);
        box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    }

    .music-result {
        width: 100%;
        min-height: 64px;
        border-radius: 8px;
        overflow: hidden;
        cursor: pointer;
        background: rgba(255,255,255,0.04);
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 6px;
        box-sizing: border-box;
    }

    .music-result img {
        width: 54px;
        height: 54px;
        min-width: 54px;
        object-fit: cover;
        display: block;
        border-radius: 6px;
        background: rgba(255,255,255,0.06);
    }

    .music-info {
        min-width: 0;
        flex: 1;
    }

    .music-title {
        color: #fff;
        font-size: 13px;
        font-weight: 700;
        line-height: 1.25;
        overflow: hidden;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
    }

    .music-plays {
        color: rgba(255,255,255,0.65);
        font-size: 12px;
        line-height: 1.25;
        margin-top: 4px;
    }

    .music-result:hover {
        outline: 2px solid rgba(255,255,255,0.35);
    }
`);
    }

    function registerSetting() {
        if (cmdId !== null) {
            GM_unregisterMenuCommand(cmdId);
        }

        const commandTitle = (autoSendEnabled ? "Выключить" : "Включить") + " автоотправку";

        cmdId = GM_registerMenuCommand(commandTitle, () => {
            autoSendEnabled = !autoSendEnabled;
            GM_setValue(AUTOSEND_SETTING_KEY, autoSendEnabled);

            registerSetting();
        });
    }

    function removeResults() {
        suppressObserver = true;
        document.querySelectorAll('.music-results-renderer').forEach(el => el.remove());
        queueMicrotask(() => {
            suppressObserver = false;
        });
    }

    function renderTracks(tracks) {
        removeResults();

        if (!tracks || !tracks.length) return;

        const editor = document.querySelector('.tiptap.ProseMirror');
        if (!editor) return;

        const wrapper = document.querySelector('.editor-box-wrapper') || editor.parentElement;
        if (!wrapper) return;

        if (getComputedStyle(wrapper).position === 'static') {
            wrapper.style.position = 'relative';
        }

        const renderer = document.createElement('div');
        renderer.className = 'music-results-renderer';

        const popup = document.createElement('div');
        popup.className = 'music-popup';

        tracks.forEach(track => {
            const item = document.createElement('div');
            item.className = 'music-result';

            const img = document.createElement('img');
            img.src = track.artwork;
            img.alt = `${track.artist} - ${track.title}`;
            img.title = `${track.artist} - ${track.title}`;

            const info = document.createElement('div');
            info.className = 'music-info';

            const title = document.createElement('div');
            title.className = 'music-title';
            title.textContent = `${track.artist} - ${track.title}`;

            const plays = document.createElement('div');
            plays.className = 'music-plays';
            plays.textContent = track.plays;

            info.appendChild(title);
            info.appendChild(plays);

            item.appendChild(img);
            item.appendChild(info);

            item.addEventListener('click', () => {
                insertTrack(track.url);
                removeResults();
            });

            popup.appendChild(item);
        });

        renderer.appendChild(popup);

        suppressObserver = true;
        wrapper.appendChild(renderer);
        queueMicrotask(() => {
            suppressObserver = false;
        });
    }

    function insertTrack(url) {
        const editor = document.querySelector('.tiptap.ProseMirror');
        if (!editor) return;

        const text = editor.innerText;
        const newText = text.replace(/^@music(?:\s+.*)?$/m, `[MEDIA=soundcloud]${url.replace("https://", "")}[/MEDIA]`);

        editor.focus();
        editor.innerText = newText;
        editor.dispatchEvent(new Event('input', { bubbles: true }));

        if (autoSendEnabled) {
            setTimeout(() => unsafeWindow.$('[aria-label="send-message"]').trigger('click'), 200);
        }
    }

    function requestJson(url) {
        return new Promise((resolve, reject) => {
            toggleProgress('PseudoAjaxStart');

            GM_xmlhttpRequest({
                method: 'GET',
                url,

                headers: {
                    'Accept': 'application/json, text/plain, */*'
                },

                onload: res => {
                    toggleProgress('PseudoAjaxStop');

                    if (res.status < 200 || res.status >= 300) {
                        reject(new Error(`HTTP ${res.status}: ${res.responseText?.slice?.(0, 200) || ''}`));
                        return;
                    }

                    try {
                        resolve(JSON.parse(res.responseText));
                    } catch (err) {
                        reject(err);
                    }
                },

                onerror: err => {
                    toggleProgress('PseudoAjaxStop');
                    reject(err);
                },

                ontimeout: err => {
                    toggleProgress('PseudoAjaxStop');
                    reject(err);
                },

                onabort: err => {
                    toggleProgress('PseudoAjaxStop');
                    reject(err);
                }
            });
        });
    }

    async function getTracks(query) {
        const q = encodeURIComponent(query);
        const userId = getAnonId();

        const url =
            `https://api-v2.soundcloud.com/search` +
            `?q=${q}` +
            `&facet=model` +
            `&user_id=${encodeURIComponent(userId)}` +
            `&client_id=${encodeURIComponent(SOUNDCLOUD_CLIENT_ID)}` +
            `&limit=20` +
            `&offset=0` +
            `&linked_partitioning=1` +
            `&app_version=${encodeURIComponent(SOUNDCLOUD_APP_VERSION)}` +
            `&app_locale=${encodeURIComponent(SOUNDCLOUD_APP_LOCALE)}`;

        const data = await requestJson(url);

        return parseTracks(data).slice(0, 20);
    }

    function parseTracks(data) {
        const tracks = [];
        const seen = new Set();

        const collection = Array.isArray(data?.collection) ? data.collection : [];

        for (const item of collection) {
            const track = normalizeTrack(item);

            if (!track) continue;
            if (seen.has(track.id || track.url)) continue;

            seen.add(track.id || track.url);
            tracks.push(track);
        }

        return tracks;
    }

    function normalizeTrack(item) {
        const t = item?.kind === 'track'
            ? item
            : item?.track?.kind === 'track'
                ? item.track
                : item?.model?.kind === 'track'
                    ? item.model
                    : null;

        if (!t) return null;

        const title = cleanText(t.title);
        const artist = cleanText(t.user?.username || t.publisher_metadata?.artist || 'Unknown artist');
        const url = t.permalink_url;

        if (!title || !url) return null;

        return {
            id: t.id,
            title,
            artist,
            url,
            artwork: getArtwork(t),
            plays: formatPlays(t.playback_count)
        };
    }

    function getArtwork(track) {
        let url =
            track.artwork_url ||
            track.user?.avatar_url ||
            '';

        if (!url) {
            return 'https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico';
        }

        url = url
            .replace('-large.', '-t300x300.')
            .replace('-t120x120.', '-t300x300.')
            .replace('-small.', '-t300x300.');

        if (url.startsWith('//')) url = 'https:' + url;

        return url;
    }

    function formatPlays(count) {
        const num = Number(count);

        if (!Number.isFinite(num) || num <= 0) {
            return '0 прослушиваний';
        }

        if (num >= 1000000000) {
            return `${trimNumber(num / 1000000000)}B прослушиваний`;
        }

        if (num >= 1000000) {
            return `${trimNumber(num / 1000000)}M прослушиваний`;
        }

        if (num >= 1000) {
            return `${trimNumber(num / 1000)}K прослушиваний`;
        }

        return `${num} прослушиваний`;
    }

    function trimNumber(value) {
        return value.toFixed(value >= 10 ? 0 : 1).replace('.0', '');
    }

    function cleanText(value) {
        return String(value || '').replace(/\s+/g, ' ').trim();
    }

    function getAnonId() {
        let id = GM_getValue(ANON_ID_KEY, '');

        if (!id) {
            id = genAID();
            GM_setValue(ANON_ID_KEY, id);
        }

        return id;
    }

    function genAID() {
        return [
            randomDigits(6),
            randomDigits(6),
            randomDigits(6),
            randomDigits(6)
        ].join('-');
    }

    function randomDigits(length) {
        let out = '';

        for (let i = 0; i < length; i++) {
            out += Math.floor(Math.random() * 10);
        }

        return out;
    }

    function toggleProgress(eventName) {
        const $ = unsafeWindow.jQuery || unsafeWindow.$;

        if ($) {
            $(unsafeWindow.document).trigger(eventName);
        }
    }

    function init() {
        initObserver();
        addStyles();
        registerSetting();
    }
})();