Music Manager - Universal Sync

Press '/' for menu. Songs stay saved across ALL different websites! Now with search + hover URL scanner that auto-copies.

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/579897/1835463/Music%20Manager%20-%20Universal%20Sync.js을(를) 사용하여 포함하는 라이브러리입니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Music Manager - Universal Sync
// @namespace    http://tampermonkey.net/
// @version      12.0
// @description  Press '/' for menu. Songs stay saved across ALL different websites! Now with search + hover URL scanner that auto-copies.
// @author       Gemini
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ---- CONFIG ----
    const defaultSongs = [
        { name: "Arctic Monkeys - 505", url: "https://youtu.be/CKI8iQTgZKU?si=O3Z_qmJTGLECGhCa", fav: true, vid: false },
        { name: "The Neighbourhood - Softcore", url: "https://youtu.be/Ssj5ZBuhwIo?si=h4fTut2Vq7IM1QFC", fav: false, vid: false }
    ];

    let savedSongs = GM_getValue('tm_universal_playlist', defaultSongs);
    let currentTab = 'library';
    let searchQuery = '';
    let scannerEnabled = true;

    // ---- UNIVERSAL URL SCANNER (hovers over websites & auto-copies) ----
    const scannerBox = document.createElement('div');
    Object.assign(scannerBox.style, {
        position: 'fixed',
        display: 'none',
        background: '#0d0d0d',
        border: '1px solid #ff3333',
        borderRadius: '8px',
        padding: '8px 14px',
        color: '#00ff88',
        fontFamily: 'monospace',
        fontSize: '12px',
        zIndex: '999999',
        pointerEvents: 'none',
        maxWidth: '600px',
        overflow: 'hidden',
        textOverflow: 'ellipsis',
        whiteSpace: 'nowrap',
        boxShadow: '0 4px 20px rgba(255,0,0,0.3)',
        transition: 'opacity 0.1s'
    });
    document.body.appendChild(scannerBox);

    // Scanner status indicator in corner
    const scannerStatus = document.createElement('div');
    Object.assign(scannerStatus.style, {
        position: 'fixed',
        bottom: '10px',
        right: '10px',
        background: '#0d0d0d',
        border: '1px solid #00ff88',
        borderRadius: '6px',
        padding: '4px 10px',
        color: '#00ff88',
        fontFamily: 'monospace',
        fontSize: '10px',
        zIndex: '999999',
        cursor: 'pointer',
        userSelect: 'none',
        opacity: '0.6'
    });
    scannerStatus.textContent = '🔍 SCANNER ON';
    scannerStatus.title = 'Click to toggle URL scanner on/off';
    scannerStatus.onclick = () => {
        scannerEnabled = !scannerEnabled;
        scannerStatus.textContent = scannerEnabled ? '🔍 SCANNER ON' : '🔍 SCANNER OFF';
        scannerStatus.style.borderColor = scannerEnabled ? '#00ff88' : '#ff3333';
        scannerStatus.style.color = scannerEnabled ? '#00ff88' : '#ff3333';
        if (!scannerEnabled) scannerBox.style.display = 'none';
    };
    document.body.appendChild(scannerStatus);

    // Track last copied URL to avoid spam
    let lastCopiedUrl = '';

    function positionScannerBox(e) {
        let x = e.clientX + 16;
        let y = e.clientY + 16;
        const rect = scannerBox.getBoundingClientRect();
        if (x + rect.width > window.innerWidth - 10) x = e.clientX - rect.width - 16;
        if (y + rect.height > window.innerHeight - 10) y = e.clientY - rect.height - 16;
        if (x < 10) x = 10;
        if (y < 10) y = 10;
        scannerBox.style.left = x + 'px';
        scannerBox.style.top = y + 'px';
    }

    // Hover listener on the whole document
    document.addEventListener('mousemove', (e) => {
        if (!scannerEnabled) return;

        const target = e.target;
        let url = null;

        // Check if hovering over a link
        if (target.tagName === 'A' && target.href) {
            url = target.href;
        }
        // Check if hovering over an image
        else if (target.tagName === 'IMG' && target.src) {
            url = target.src;
        }
        // Check for parent anchors (in case inner elements like spans inside <a>)
        else {
            let parent = target.closest('a');
            if (parent && parent.href) {
                url = parent.href;
            }
            // Check for elements with data-url or similar attributes
            if (!url && target.dataset && target.dataset.url) {
                url = target.dataset.url;
            }
        }

        if (url) {
            // Show the URL
            scannerBox.textContent = '📍 ' + url;
            scannerBox.style.display = 'block';
            positionScannerBox(e);

            // Auto-copy to clipboard (only if it's a new URL)
            if (url !== lastCopiedUrl) {
                lastCopiedUrl = url;
                navigator.clipboard.writeText(url).catch(() => {
                    // Fallback for older browsers
                    const ta = document.createElement('textarea');
                    ta.value = url;
                    ta.style.position = 'fixed';
                    ta.style.opacity = '0';
                    document.body.appendChild(ta);
                    ta.select();
                    document.execCommand('copy');
                    document.body.removeChild(ta);
                });
                // Flash effect
                scannerBox.style.borderColor = '#00ff88';
                setTimeout(() => { scannerBox.style.borderColor = '#ff3333'; }, 200);
            }
        } else {
            scannerBox.style.display = 'none';
        }
    });

    // Clean up when leaving the page
    document.addEventListener('mouseleave', () => {
        if (scannerEnabled) scannerBox.style.display = 'none';
    });

    // ---- INFO MODAL (Draggable) ----
    const infoBox = document.createElement('div');
    Object.assign(infoBox.style, {
        position: 'fixed', top: '50%', left: '50%',
        transform: 'translate(-50%, -50%)',
        background: '#1e1e1e', border: '1px solid #444', borderRadius: '10px',
        zIndex: '10001', display: 'none', flexDirection: 'column',
        width: '300px', padding: '0 0 20px 0',
        boxShadow: '0 0 50px rgba(0,0,0,0.9)', fontFamily: 'Segoe UI, sans-serif',
        color: 'white', textAlign: 'center', overflow: 'hidden'
    });
    document.body.appendChild(infoBox);

    function showInfo(song) {
        infoBox.innerHTML = `
            <div id="infoDragHandle" style="background: #333; color: #ff0000; padding: 8px; cursor: grab; font-weight: bold; font-size: 12px; margin-bottom: 15px; user-select: none;">
                :: DETAILS (Drag) ::
            </div>
            <div style="padding: 0 20px;">
                <div style="font-size: 11px; color: #aaa;">NAME</div>
                <div style="font-size: 13px; margin-bottom: 15px; word-break: break-all;">${song.name}</div>
                <div style="font-size: 11px; color: #aaa;">URL</div>
                <div style="font-size: 12px; margin-bottom: 20px; word-break: break-all; color: #00abff;">${song.url}</div>
                <button id="closeInfo" style="padding: 8px 20px; background: #ff0000; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">CLOSE</button>
            </div>
        `;
        infoBox.style.display = 'flex';
        document.getElementById('closeInfo').onclick = () => infoBox.style.display = 'none';
        setupDragging(document.getElementById('infoDragHandle'), infoBox, true);
    }

    // ---- MAIN MENU ----
    const menu = document.createElement('div');
    Object.assign(menu.style, {
        position: 'fixed', top: '15%', left: '35%',
        background: '#121212', border: '2px solid #ff0000', borderRadius: '12px',
        zIndex: '10000', display: 'none', flexDirection: 'column',
        width: '450px', maxHeight: '88vh',
        boxShadow: '0 10px 40px rgba(0,0,0,0.8)', fontFamily: 'Segoe UI, sans-serif',
        color: 'white', overflow: 'hidden'
    });

    // -- Drag Handle --
    const dragHandle = document.createElement('div');
    dragHandle.innerText = ":: Music Menu (Drag) ::";
    Object.assign(dragHandle.style, {
        background: '#ff0000', color: 'white', padding: '8px', cursor: 'grab',
        textAlign: 'center', fontWeight: 'bold', fontSize: '11px', userSelect: 'none'
    });
    menu.appendChild(dragHandle);

    // -- Nav Tabs --
    const nav = document.createElement('div');
    nav.style.display = 'flex';
    nav.style.background = '#1a1a1a';

    const libBtn = document.createElement('button');
    const favBtn = document.createElement('button');
    const vidBtn = document.createElement('button');

    [libBtn, favBtn, vidBtn].forEach(b => {
        Object.assign(b.style, {
            flex: '1', padding: '10px', cursor: 'pointer', border: 'none',
            background: 'none', color: 'white', fontWeight: 'bold', fontSize: '12px'
        });
    });

    libBtn.innerText = "📁 Movies/songs"; favBtn.innerText = "⭐ Music"; vidBtn.innerText = "⚪ Movies";

    libBtn.onclick = () => { currentTab = 'library'; searchQuery = ''; updateTabStyles(); renderPlaylist(); };
    favBtn.onclick = () => { currentTab = 'favorites'; searchQuery = ''; updateTabStyles(); renderPlaylist(); };
    vidBtn.onclick = () => { currentTab = 'videos'; searchQuery = ''; updateTabStyles(); renderPlaylist(); };

    function updateTabStyles() {
        libBtn.style.borderBottom = currentTab === 'library' ? '3px solid #ff0000' : 'none';
        favBtn.style.borderBottom = currentTab === 'favorites' ? '3px solid #ffcc00' : 'none';
        vidBtn.style.borderBottom = currentTab === 'videos' ? '3px solid #00abff' : 'none';
    }

    nav.append(libBtn, favBtn, vidBtn);
    menu.appendChild(nav);

    // -- SEARCH BAR --
    const searchBar = document.createElement('input');
    searchBar.placeholder = "🔍 Search by name or keyword...";
    Object.assign(searchBar.style, {
        margin: '10px 15px 0 15px',
        padding: '9px 12px',
        background: '#1a1a1a',
        color: 'white',
        border: '1px solid #444',
        borderRadius: '6px',
        fontFamily: 'Segoe UI, sans-serif',
        fontSize: '13px',
        outline: 'none'
    });
    searchBar.addEventListener('input', () => {
        searchQuery = searchBar.value.toLowerCase().trim();
        renderPlaylist();
    });
    searchBar.addEventListener('keydown', (e) => e.stopPropagation());
    menu.appendChild(searchBar);

    // -- SCROLLABLE LIST --
    const listContainer = document.createElement('div');
    Object.assign(listContainer.style, {
        padding: '15px',
        overflowY: 'auto',
        flexGrow: '1',
        maxHeight: '350px',
        minHeight: '120px'
    });
    menu.appendChild(listContainer);

    function renderPlaylist() {
        listContainer.innerHTML = '';

        let filtered = savedSongs.filter(s => {
            if (currentTab === 'favorites') return s.fav;
            if (currentTab === 'videos') return s.vid;
            return !s.fav && !s.vid;
        });

        if (searchQuery) {
            const q = searchQuery.toLowerCase();
            filtered = filtered.filter(s =>
                s.name.toLowerCase().includes(q) ||
                s.url.toLowerCase().includes(q)
            );
        }

        const count = document.createElement('div');
        count.style = 'font-size: 11px; color: #666; margin-bottom: 8px;';
        count.textContent = `${filtered.length} item${filtered.length !== 1 ? 's' : ''}`;
        listContainer.appendChild(count);

        if (filtered.length === 0) {
            const empty = document.createElement('div');
            empty.style = 'color: #555; text-align: center; padding: 30px 0; font-size: 13px;';
            empty.textContent = searchQuery ? 'No matches found.' : 'This folder is empty.';
            listContainer.appendChild(empty);
            return;
        }

        filtered.forEach(song => {
            const row = document.createElement('div');
            row.style = 'display: flex; gap: 8px; margin-bottom: 8px; align-items: center;';

            const star = document.createElement('button');
            star.innerText = song.fav ? "★" : "☆";
            star.style = `background:none; border:none; color:${song.fav ? '#ffcc00':'#444'}; cursor:pointer; font-size:18px;`;
            star.onclick = () => { song.fav = !song.fav; if(song.fav) song.vid = false; save(); };

            const circ = document.createElement('button');
            circ.innerText = song.vid ? "●" : "○";
            circ.style = `background:none; border:none; color:${song.vid ? '#00abff':'#444'}; cursor:pointer; font-size:18px;`;
            circ.onclick = () => { song.vid = !song.vid; if(song.vid) song.fav = false; save(); };

            const mainBtn = document.createElement('button');
            mainBtn.innerText = song.name;
            mainBtn.style = 'flex-grow:1; padding:8px; background:#222; color:white; border:1px solid #333; border-radius:4px; text-align:left; font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:pointer;';
            mainBtn.onclick = () => window.open(song.url, '_blank');

            const infoBtn = document.createElement('button');
            infoBtn.innerText = "⋮";
            infoBtn.style = 'background:none; border:none; color:#888; cursor:pointer; font-size:18px; padding:0 5px;';
            infoBtn.onclick = (e) => { e.stopPropagation(); showInfo(song); };

            const del = document.createElement('button');
            del.innerText = "✕";
            del.style = 'background:none; border:none; color:#444; cursor:pointer; font-size:14px;';
            del.onclick = () => { savedSongs = savedSongs.filter(s => s !== song); save(); };

            row.append(star, circ, mainBtn, infoBtn, del);
            listContainer.appendChild(row);
        });
    }

    // -- FOOTER --
    const footer = document.createElement('div');
    footer.style = 'padding: 15px; border-top: 1px solid #333; display: flex; flex-direction: column; gap: 8px;';

    const iName = document.createElement('input');
    iName.placeholder = "Name...";
    iName.style = 'padding:8px; background:#1a1a1a; color:white; border:1px solid #444; border-radius:4px;';

    const iUrl = document.createElement('input');
    iUrl.placeholder = "URL...";
    iUrl.style = 'padding:8px; background:#1a1a1a; color:white; border:1px solid #444; border-radius:4px;';

    const addBtn = document.createElement('button');
    addBtn.innerText = "Add to Library";
    addBtn.style = 'padding:10px; background:#ff0000; color:white; border:none; border-radius:5px; cursor:pointer; font-weight:bold;';

    addBtn.onclick = () => {
        if(iName.value && iUrl.value) {
            savedSongs.push({ name: iName.value, url: iUrl.value, fav: false, vid: false });
            iName.value = '';
            iUrl.value = '';
            save();
        }
    };

    footer.append(iName, iUrl, addBtn);
    menu.appendChild(footer);

    // ---- UNIVERSAL SAVE ----
    function save() {
        GM_setValue('tm_universal_playlist', savedSongs);
        renderPlaylist();
    }

    // ---- DRAG LOGIC ----
    function setupDragging(handle, target, isCentered = false) {
        let dragging = false, ox, oy;
        handle.onmousedown = (e) => {
            dragging = true;
            if (isCentered) {
                const rect = target.getBoundingClientRect();
                target.style.transform = 'none';
                target.style.left = rect.left + 'px';
                target.style.top = rect.top + 'px';
            }
            ox = e.clientX - target.offsetLeft;
            oy = e.clientY - target.offsetTop;
        };
        document.addEventListener('mousemove', (e) => {
            if (dragging) {
                target.style.left = (e.clientX - ox) + 'px';
                target.style.top = (e.clientY - oy) + 'px';
            }
        });
        document.addEventListener('mouseup', () => dragging = false);
    }

    setupDragging(dragHandle, menu);

    // ---- KEYBOARD TOGGLE ----
    window.addEventListener('keydown', (e) => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
        if (e.key === '/') {
            e.preventDefault();
            const isHidden = menu.style.display === 'none';
            menu.style.display = isHidden ? 'flex' : 'none';
            if (!isHidden) infoBox.style.display = 'none';
            else searchBar.focus();
        }
    });

    // ---- FINAL APPEND & INIT ----
    document.body.appendChild(menu);
    updateTabStyles();
    renderPlaylist();

})();