@vid

ютьюб видео

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         @vid
// @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=youtube.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @connect      www.youtube.com
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    const AUTOSEND_SETTING_KEY = "autosend_enabled";

    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('@vid ')) {
                removeResults();
                lastQuery = '';
                return;
            }

            const query = text.slice('@vid '.length).trim();

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

            if (query === lastQuery && document.querySelector('.vid-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('@vid ')) {
                    removeResults();
                    lastQuery = '';
                    return;
                }

                const currentQuery = currentText.slice('@vid '.length).trim();

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

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

                try {
                    const videos = await getVideos(currentQuery);

                    lastQuery = currentQuery;
                    renderVideos(videos);
                } catch (err) {
                    console.error('[vid] req failed:', err);
                    removeResults();
                }
            }, 400);
        });

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

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

    .vid-popup {
        pointer-events: auto;
        display: flex;
        flex-direction: column;
        gap: 6px;
        width: min(420px, 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);
    }

    .vid-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;
    }

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

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

    .vid-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;
    }

    .vid-views {
        color: rgba(255,255,255,0.65);
        font-size: 12px;
        line-height: 1.25;
        margin-top: 4px;
    }
    
    .vid-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('.vid-results-renderer').forEach(el => el.remove());
        queueMicrotask(() => {
            suppressObserver = false;
        });
    }

    function renderVideos(videos) {
        removeResults();

        if (!videos || !videos.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 = 'vid-results-renderer';

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

        videos.forEach(src => {
            const item = document.createElement('div');
            item.className = 'vid-result';

            const img = document.createElement('img');
            img.src = src.thumbnail;
            img.alt = src.title;
            img.title = src.title;

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

            const title = document.createElement('div');
            title.className = 'vid-title';
            title.textContent = src.title;

            const views = document.createElement('div');
            views.className = 'vid-views';
            views.textContent = src.views;

            info.appendChild(title);
            info.appendChild(views);

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

            item.addEventListener('click', () => {
                insertVid(src.videoId);
                removeResults();
            });

            popup.appendChild(item);
        });

        renderer.appendChild(popup);

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

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

        const text = editor.innerText;
        const newText = text.replace(/^@vid(?:\s+.*)?$/m, `[MEDIA=youtube]${videoId}[/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 request(url) {
        return new Promise((resolve, reject) => {
            toggleProgress('PseudoAjaxStart');

            GM_xmlhttpRequest({
                method: 'GET', url,

                headers: {
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
                },

                onload: res => {
                    toggleProgress('PseudoAjaxStop');
                    resolve(res.responseText);
                },

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

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

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

    async function getVideos(query) {
        const q = encodeURIComponent(query);

        const searchUrl = `https://www.youtube.com/results?search_query=${q}`;

        const html = await request(searchUrl);

        const data = extractData(html);

        return parse(data).slice(0, 50);
    }

    function extractData(html) {
        const marker = 'var ytInitialData = ';
        let start = html.indexOf(marker);

        if (start === -1) {
            const marker2 = 'ytInitialData = ';
            start = html.indexOf(marker2);

            if (start === -1) {
                throw new Error("ytInitialData not found");
            }

            start += marker2.length;
        } else {
            start += marker.length;
        }

        let i = start;
        let depth = 0;
        let inString = false;
        let stringQuote = '';
        let escaped = false;

        for (; i < html.length; i++) {
            const ch = html[i];

            if (inString) {
                if (escaped) {
                    escaped = false;
                } else if (ch === '\\') {
                    escaped = true;
                } else if (ch === stringQuote) {
                    inString = false;
                }

                continue;
            }

            if (ch === '"' || ch === "'") {
                inString = true;
                stringQuote = ch;
                continue;
            }

            if (ch === '{') depth++;
            if (ch === '}') depth--;

            if (depth === 0 && ch === '}') {
                i++;
                break;
            }
        }

        return JSON.parse(html.slice(start, i));
    }

    function parse(data) {
        const videos = [];
        const seen = new Set();

        walk(data, node => {
            if (!node.videoRenderer) return;

            const v = node.videoRenderer;
            const videoId = v.videoId;

            if (!videoId || seen.has(videoId)) return;
            seen.add(videoId);

            videos.push({
                videoId,
                title: getText(v.title),
                views: formatViews(getText(v.viewCountText)),
                thumbnail: getBestThumbnail(v.thumbnail?.thumbnails) || getThumbnail(videoId),
            });
        });

        return videos;
    }

    function walk(obj, cb) {
        if (!obj || typeof obj !== 'object') return;

        cb(obj);

        if (Array.isArray(obj)) {
            obj.forEach(item => walk(item, cb));
        } else {
            Object.values(obj).forEach(value => walk(value, cb));
        }
    }

    function getText(obj) {
        if (!obj) return "";
        if (obj.simpleText) return obj.simpleText;
        if (Array.isArray(obj.runs)) return obj.runs.map(item => item.text).join("");

        return "";
    }

    function getBestThumbnail(thumbnails) {
        if (!Array.isArray(thumbnails) || !thumbnails.length) return "";

        const best = thumbnails
            .slice()
            .sort((a, b) => (b.width || 0) - (a.width || 0))[0];

        if (!best || !best.url) return "";

        let url = best.url;

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

        return url;
    }

    function getThumbnail(videoId) {
        return `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
    }

    function formatViews(text) {
        const num = Number(String(text || '').replace(/[^\d]/g, ''));

        if (!num) return text || '';

        if (num >= 900000) {
            return `${String(Math.floor(num / 100000) / 10).replace('.0', '')}M views`;
        }

        if (num >= 1000) {
            return `${Math.floor(num / 1000)}K views`;
        }

        return `${num} views`;
    }

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

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

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