// ==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 });
})();