SceneNZBs Thumbnail Hover Zoom

Cover-Hover: rahmenlos, performant, fix 200px nach rechts verschoben, passt sich automatisch an Bildschirmgröße an

// ==UserScript==
// @name		SceneNZBs Thumbnail Hover Zoom
// @description		Cover-Hover: rahmenlos, performant, fix 200px nach rechts verschoben, passt sich automatisch an Bildschirmgröße an
// @version		6.1.10
// @match		https://*.scenenzbs.com/*
// @icon		https://cdn.scenenzbs.com/assets/static/favicon.ico
// @namespace		https://scenenzbs.com/
// @author		Baumeister
// @grant		none
// @license		MIT
// ==/UserScript==
(function() {
    'use strict';

    // Fixe Einstellungen
    const ZOOMFACTOR = 4.2;     // feste Vergrößerung
    const BASE_WIDTH = 340;
    const BASE_HEIGHT = 480;
    const H_OFFSET_PX = 200;    // feste Verschiebung nach rechts

    // CSS nur einmal einfügen
    if (!window.__overlayCSS) {
        const style = document.createElement('style');
        style.textContent = `
            img.thumb-zoom:hover,
            .thumb-zoom-wrap:hover img.thumb-zoom,
            img.thumb-zoom:active,
            img.thumb-zoom:focus {
                transform: none !important;
                transition: none !important;
                z-index: auto !important;
                position: static !important;
                outline: none !important;
                filter: none !important;
                box-shadow: none !important;
            }

            .ultra-overlay-img {
                position: fixed;
                top: 50%;
                left: 50%;
                z-index: 99999;
                opacity: 0;
                pointer-events: none;
                display: block;
                max-width: 90vw;
                max-height: 90vh;
                object-fit: contain;
                transition: opacity 0.18s cubic-bezier(.19,1,.22,1);
            }

            .ultra-hint-text {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(calc(-50% + 200px), -50%);
                z-index: 99999;
                opacity: 0;
                pointer-events: none;
                background: rgba(0, 0, 0, 0.85);
                color: white;
                padding: 20px 30px;
                border-radius: 8px;
                font-size: 16px;
                font-weight: bold;
                text-align: center;
                transition: opacity 0.18s cubic-bezier(.19,1,.22,1);
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
            }
        `;
        document.head.appendChild(style);
        window.__overlayCSS = true;
    }

    // Overlay-Bild erzeugen
    if (!window.__overlayImg) {
        const overlayImg = document.createElement('img');
        overlayImg.className = 'ultra-overlay-img';
        overlayImg.style.opacity = '0';
        document.body.appendChild(overlayImg);
        window.__overlayImg = overlayImg;
    }

    // Hinweistext für Platzhalter erzeugen
    if (!window.__hintText) {
        const hintText = document.createElement('div');
        hintText.className = 'ultra-hint-text';
        hintText.textContent = 'Bildersuche nach Coverbild';
        hintText.style.opacity = '0';
        document.body.appendChild(hintText);
        window.__hintText = hintText;
    }

    // Platzhalter erkennen
    function isPlaceholder(img) {
        const src = (img.getAttribute('src') || '').toLowerCase();
        return src.includes('category_') || src.includes('placeholder.webp') || src.includes('no-cover.webp');
    }

    // Release-Namen aus der Seite extrahieren
    function getReleaseName(img) {
        // Suche nach dem nächsten Release-Link
        let element = img.closest('.row, .col, .card, .item');
        if (!element) element = img.parentElement;
        
        // Suche nach dem Release-Namen in verschiedenen möglichen Strukturen
        const releaseLink = element?.querySelector('a.fw-bold.text-decoration-none[href*="/details/"]');
        if (releaseLink) {
            return releaseLink.textContent.trim();
        }
        
        // Fallback: Suche im gesamten Container
        const allLinks = element?.querySelectorAll('a[href*="/details/"]');
        if (allLinks && allLinks.length > 0) {
            return allLinks[0].textContent.trim();
        }
        
        return '';
    }

    // Kategorie aus der Seite extrahieren
    function getCategory(img) {
        let element = img.closest('.row, .col, .card, .item');
        if (!element) element = img.parentElement;
        
        // Suche nach Kategorie-Link
        const categoryLink = element?.querySelector('.text-muted a[href*="/browse?t="]');
        if (categoryLink) {
            const fullCategory = categoryLink.textContent.trim();
            // Nur den Teil vor ">" zurückgeben
            const mainCategory = fullCategory.split('>')[0].trim();
            return mainCategory;
        }
        
        return '';
    }

    // NFO-URL aus der Seite extrahieren
    function getNfoUrl(img) {
        let element = img.closest('.row, .col, .card, .item');
        if (!element) element = img.parentElement;
        
        // Suche nach NFO-Button
        const nfoButton = element?.querySelector('a[data-url*="/nfo?id="]');
        if (nfoButton) {
            return nfoButton.getAttribute('data-url');
        }
        
        return '';
    }

    // URL aus NFO-Text extrahieren
    async function extractUrlFromNfo(nfoUrl) {
        try {
            const response = await fetch(nfoUrl);
            const html = await response.text();
            
            // Parse HTML und extrahiere den NFO-Text
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const nfoText = doc.body.textContent || '';
            
            // Suche nach URLs in verschiedenen Formaten
            const urlPatterns = [
                /URL[.\s:]+([^\s\n]+)/i,
                /https?:\/\/(play\.napster\.com|tidal\.com|open\.spotify\.com|music\.apple\.com|www\.deezer\.com|music\.youtube\.com|open\.qobuz\.com)[^\s\n]*/gi
            ];
            
            for (const pattern of urlPatterns) {
                const match = nfoText.match(pattern);
                if (match) {
                    // Wenn es ein URL:-Feld ist, nimm die zweite Gruppe
                    if (match[1] && !match[0].startsWith('http')) {
                        return match[1].trim();
                    }
                    // Sonst nimm die ganze URL
                    return match[0].trim();
                }
            }
        } catch (error) {
            console.error('Fehler beim Abrufen der NFO:', error);
        }
        
        return '';
    }

    // Streaming-Button hinzufügen
    async function addStreamingButton(img) {
        const nfoUrl = getNfoUrl(img);
        if (!nfoUrl) return;
        
        // Finde den Button-Container
        let element = img.closest('.row, .col, .card, .item');
        if (!element) element = img.parentElement;
        
        const buttonContainer = element?.querySelector('.mt-2.d-flex.flex-wrap.gap-1');
        if (!buttonContainer) return;
        
        // Prüfe, ob Button bereits existiert
        if (buttonContainer.parentElement?.querySelector('.ultra-streaming-btn')) return;
        
        // Erstelle den Button-Container (eine Zeile unter den anderen Buttons)
        const linkContainer = document.createElement('div');
        linkContainer.className = 'mt-1';
        
        // Erstelle den Button
        const streamButton = document.createElement('a');
        streamButton.href = '#';
        streamButton.className = 'btn btn-sm btn-tag btn-outline-primary ultra-streaming-btn';
        streamButton.textContent = 'URL aus NFO';
        streamButton.title = 'Medienlink aus NFO öffnen';
        
        // Klick-Handler: Erst beim Klick NFO abrufen
        streamButton.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();
            
            // Zeige Lade-Indikator
            streamButton.textContent = 'Lädt...';
            streamButton.disabled = true;
            
            try {
                const extractedUrl = await extractUrlFromNfo(nfoUrl);
                
                if (extractedUrl) {
                    // Öffne die URL
                    window.open(extractedUrl, '_blank');
                    
                    // Aktualisiere Button-Text mit Service-Namen
                    let serviceName = 'Link';
                    if (extractedUrl.includes('napster.com')) serviceName = 'Napster';
                    else if (extractedUrl.includes('tidal.com')) serviceName = 'Tidal';
                    else if (extractedUrl.includes('spotify.com')) serviceName = 'Spotify';
                    else if (extractedUrl.includes('apple.com')) serviceName = 'Apple Music';
                    else if (extractedUrl.includes('deezer.com')) serviceName = 'Deezer';
                    else if (extractedUrl.includes('youtube.com')) serviceName = 'YouTube Music';
                    else if (extractedUrl.includes('qobuz.com')) serviceName = 'Qobuz';
                    
                    streamButton.textContent = serviceName;
                    streamButton.href = extractedUrl;
                } else {
                    // Keine URL gefunden
                    streamButton.textContent = 'Kein Link';
                    streamButton.classList.remove('btn-outline-primary');
                    streamButton.classList.add('btn-outline-secondary');
                }
            } catch (error) {
                console.error('Fehler beim Abrufen der NFO:', error);
                streamButton.textContent = 'Fehler';
                streamButton.classList.remove('btn-outline-primary');
                streamButton.classList.add('btn-outline-danger');
            } finally {
                streamButton.disabled = false;
            }
        });
        
        linkContainer.appendChild(streamButton);
        
        // Füge Container direkt nach dem Button-Container ein
        buttonContainer.parentElement.insertBefore(linkContainer, buttonContainer.nextSibling);
    }

    // Events für Thumbs binden
    function bindOverlay() {
        // Original: Bilder mit thumb-zoom Klasse
        const thumbZoomImages = document.querySelectorAll('img.thumb-zoom');
        
        // Neu: Bilder in wiederkehrenden Row-Containern (movies, books, console, music)
        const rowImages = document.querySelectorAll('.row.border-bottom img.img-fluid.rounded.shadow-sm');
        
        // Kombiniere beide Listen
        const allImages = [...thumbZoomImages, ...rowImages];
        
        allImages.forEach(img => {
            if (img.dataset.ultraBound) return;
            img.dataset.ultraBound = '1';

            const isPlaceholderImg = isPlaceholder(img);

            img.addEventListener('mouseenter', () => {
                if (isPlaceholderImg) {
                    // Zeige Hinweistext statt Zoom
                    window.__overlayImg.style.opacity = '0';
                    
                    // Aktualisiere Hinweistext mit Release-Namen
                    const releaseName = getReleaseName(img);
                    if (releaseName) {
                        window.__hintText.innerHTML = `Bildersuche nach<br><strong>${releaseName}</strong>`;
                    } else {
                        window.__hintText.textContent = 'Bildersuche nach Coverbild';
                    }
                    
                    window.__hintText.style.opacity = '1';
                    return;
                }
                if (window.__overlayImg.src !== img.src) {
                    window.__overlayImg.src = img.src;
                }
                // feste Verschiebung nach rechts
                window.__overlayImg.style.transform =
                    `translate(calc(-50% + ${H_OFFSET_PX}px), -50%)`;
                window.__overlayImg.style.opacity = '1';
            });

            img.addEventListener('mouseleave', () => {
                window.__overlayImg.style.opacity = '0';
                window.__hintText.style.opacity = '0';
            });

            // Klick-Event für Platzhalter: Google Bildersuche
            if (isPlaceholderImg) {
                img.style.cursor = 'pointer';
                img.addEventListener('click', async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    
                    const releaseName = getReleaseName(img);
                    const category = getCategory(img);
                    
                    // Fallback: Google Bildersuche
                    if (releaseName) {
                        // Kombiniere Kategorie und Release-Name für bessere Suchergebnisse
                        let searchTerm = releaseName;
                        if (category) {
                            searchTerm = `${category}: ${releaseName}`;
                        }
                        
                        const searchQuery = encodeURIComponent(searchTerm + ' cover');
                        const googleImageSearchUrl = `https://www.google.com/search?tbm=isch&q=${searchQuery}`;
                        window.open(googleImageSearchUrl, '_blank');
                    } else {
                        console.log('Kein Release-Name gefunden');
                    }
                });
            }
        });
        
        // Füge Streaming-Button für ALLE Einträge hinzu (nicht nur Platzhalter)
        allImages.forEach(img => {
            addStreamingButton(img);
        });
    }

    // Scroll-Guard
    window.addEventListener('scroll', () => {
        if (window.__overlayImg && window.__overlayImg.style.opacity === '1') {
            window.__overlayImg.style.opacity = '0';
        }
    });

    // Start
    bindOverlay();

    const observer = new MutationObserver(() => {
        bindOverlay();
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();