Embeds YouTube, Spotify, SoundCloud, and direct media links in Mucklet with advanced hash directives.
// ==UserScript==
// @name Mucklet Media Embedder Ultra
// @name:de Mucklet Medien-Einbetter Ultra
// @namespace https://forum.wolfery.com/u/felinex/
// @version 1.5
// @description Embeds YouTube, Spotify, SoundCloud, and direct media links in Mucklet with advanced hash directives.
// @description:de Bettet YouTube, Spotify, SoundCloud und direkte Medien-Links in Mucklet mit erweiterten Hash-Direktiven ein.
// @icon https://static.f-list.net/images/eicon/wolfery.png
// @license All Rights Reserved
// @author Felinex Gloomfort
// @match *://*.mucklet.com/*
// @match *://wolfery.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const STYLE_ID = 'mme-ultra-style';
const MODAL_ID = 'mme-ultra-modal';
// Chat scope selectors / Selektoren für Chat-Bereiche
const CHAT_SCOPE_SELECTORS = [
'[role="log"]',
'.charlog',
'.chatlog',
'.chatLog',
'.messages',
'.message',
'.msg',
'.logEntry',
'.log-entry',
'.logentry',
'.chat',
'[class*="charlog"]',
'[class*="chatlog"]',
'[class*="message"]',
'[class*="msg"]',
'[class*="speech"]',
'[class*="ooc"]',
'[class*="ic"]'
];
// Excluded UI areas / Ausgeschlossene UI-Bereiche
const EXCLUDED_SCOPE_SELECTORS = [
'nav',
'header',
'footer',
'aside',
'form',
'button',
'[role="dialog"]',
'[role="navigation"]',
'.menu',
'.dropdown',
'.tooltip',
'.modal',
'.popup'
].join(',');
injectStyle();
const modalApi = createModal();
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
scanForLinks(node);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
scanForLinks(document.body);
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
.mme-embed-holder {
display: block;
margin-top: 6px;
margin-bottom: 6px;
max-width: 100%;
overflow: hidden;
}
.mme-embed-box {
display: inline-block;
max-width: 100%;
border-radius: 8px;
overflow: hidden;
vertical-align: top;
background: rgba(0, 0, 0, 0.14);
}
.mme-embed-box iframe,
.mme-embed-box img,
.mme-embed-box video,
.mme-embed-box audio {
display: block;
max-width: 100%;
border: none;
}
.mme-inline-emot {
display: inline-block;
height: 1em;
width: auto;
max-height: 1em;
vertical-align: baseline;
object-fit: contain;
}
.mme-popup-overlay {
position: fixed;
inset: 0;
z-index: 2147483647;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.82);
}
.mme-popup-overlay.mme-open {
display: flex;
}
.mme-popup-dialog {
position: relative;
width: auto;
max-width: min(92vw, 1100px);
max-height: 90vh;
overflow: auto;
border-radius: 10px;
padding: 12px;
box-sizing: border-box;
background: rgba(20, 20, 20, 0.97);
}
.mme-popup-close {
display: block;
margin-left: auto;
margin-bottom: 10px;
cursor: pointer;
border: none;
border-radius: 6px;
padding: 6px 10px;
color: #fff;
background: rgba(255,255,255,0.16);
}
.mme-popup-content iframe,
.mme-popup-content img,
.mme-popup-content video,
.mme-popup-content audio {
display: block;
max-width: 100%;
}
.mme-popup-content video,
.mme-popup-content img {
max-height: 80vh;
}
`;
document.head.appendChild(style);
}
function createModal() {
let overlay = document.getElementById(MODAL_ID);
if (overlay) {
return {
open(node) {
const content = overlay.querySelector('.mme-popup-content');
content.innerHTML = '';
content.appendChild(node);
overlay.classList.add('mme-open');
},
close() {
const content = overlay.querySelector('.mme-popup-content');
content.innerHTML = '';
overlay.classList.remove('mme-open');
}
};
}
overlay = document.createElement('div');
overlay.id = MODAL_ID;
overlay.className = 'mme-popup-overlay';
const dialog = document.createElement('div');
dialog.className = 'mme-popup-dialog';
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'mme-popup-close';
closeBtn.textContent = 'Close / Schließen';
const content = document.createElement('div');
content.className = 'mme-popup-content';
closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', (event) => {
if (event.target === overlay) closeModal();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && overlay.classList.contains('mme-open')) {
closeModal();
}
});
dialog.appendChild(closeBtn);
dialog.appendChild(content);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
function closeModal() {
content.innerHTML = '';
overlay.classList.remove('mme-open');
}
return {
open(node) {
content.innerHTML = '';
content.appendChild(node);
overlay.classList.add('mme-open');
},
close: closeModal
};
}
function scanForLinks(root) {
if (!(root instanceof Element || root instanceof Document || root instanceof DocumentFragment)) return;
const anchors = [];
if (root instanceof HTMLAnchorElement && root.href) {
anchors.push(root);
}
const found = typeof root.querySelectorAll === 'function'
? root.querySelectorAll('a[href]')
: [];
anchors.push(...found);
for (const link of anchors) {
if (!(link instanceof HTMLAnchorElement)) continue;
processLink(link);
}
}
function findChatScope(link) {
if (!(link instanceof Element)) return null;
if (link.closest(EXCLUDED_SCOPE_SELECTORS)) return null;
for (const selector of CHAT_SCOPE_SELECTORS) {
const match = link.closest(selector);
if (match) return match;
}
let current = link.parentElement;
while (current && current !== document.body) {
if (current.matches?.(EXCLUDED_SCOPE_SELECTORS)) return null;
if (looksLikeChatNode(current)) return current;
current = current.parentElement;
}
return null;
}
function isInfoScope(link) {
if (!(link instanceof Element)) return false;
if (link.closest(EXCLUDED_SCOPE_SELECTORS)) return false;
return true;
}
function looksLikeChatNode(node) {
if (!(node instanceof Element)) return false;
const role = (node.getAttribute('role') || '').toLowerCase();
if (role === 'log' || role === 'article' || role === 'listitem') return true;
const classBlob = [
typeof node.className === 'string' ? node.className : '',
node.id || '',
node.getAttribute('data-id') || '',
node.getAttribute('data-cid') || ''
].join(' ').toLowerCase();
return /(chat|charlog|chatlog|message|msg|speech|ooc|ic|logentry|log-entry|logline|line)/i.test(classBlob);
}
function processLink(link) {
if (link.dataset.mmeProcessed === '1') return;
const info = parseLinkInfo(link.href);
if (!info) return;
const inChatScope = !!findChatScope(link);
const inInfoScope = info.directives.info && isInfoScope(link);
if (!inChatScope && !inInfoScope) return;
link.dataset.mmeProcessed = '1';
const key = buildEmbedKey(info);
if (info.directives.popup) {
preparePopupLink(link, info);
return;
}
if (info.directives.mediaUrl && info.type === 'image') {
applyMediaDirective(link, info.directives);
return;
}
if (info.directives.emot && info.type === 'image') {
const img = createEmotImage(info.directives.mediaUrl || info.mediaSrc);
replaceLinkWithNode(link, img, false);
return;
}
if (info.directives.icon && info.type === 'image') {
const img = createIconImage(info.directives.mediaUrl || info.mediaSrc);
replaceLinkWithNode(link, img, false);
return;
}
const embedNode = createEmbedNode(info);
if (!embedNode) return;
if (info.directives.replace) {
const holder = wrapEmbed(embedNode, key);
replaceLinkWithNode(link, holder, true);
return;
}
appendEmbedToParentEnd(link, embedNode, key);
}
function parseLinkInfo(rawHref) {
let url;
try {
url = new URL(rawHref, location.href);
} catch {
return null;
}
const directives = parseDirectives(url.hash, url);
const baseWithoutHash = `${url.origin}${url.pathname}${url.search}`;
const mediaSrc = baseWithoutHash + directives.nativeHash;
const pathname = url.pathname.toLowerCase();
const youtubeMatch = baseWithoutHash.match(/(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?|shorts)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i);
if (youtubeMatch) {
return {
type: 'youtube',
originalUrl: mediaSrc,
mediaSrc,
directives,
videoId: youtubeMatch[1],
nativeHashParts: directives.nativeHashParts
};
}
const spotifyMatch = baseWithoutHash.match(/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/i);
if (spotifyMatch) {
return {
type: 'spotify',
originalUrl: mediaSrc,
mediaSrc,
directives,
spotifyKind: spotifyMatch[1],
spotifyId: spotifyMatch[2]
};
}
if (isSoundCloudUrl(url)) {
return {
type: 'soundcloud',
originalUrl: mediaSrc,
mediaSrc,
directives
};
}
const detectedType = detectDirectMediaType(url, pathname);
if (detectedType) {
return {
type: detectedType,
originalUrl: mediaSrc,
mediaSrc,
directives,
isDiscordCdn: isDiscordCdn(url)
};
}
return null;
}
function parseDirectives(hash, baseUrl) {
const result = {
play: false,
replace: false,
emot: false,
popup: false,
icon: false,
info: false,
width: null,
height: null,
mediaUrl: null,
nativeHash: '',
nativeHashParts: []
};
if (!hash || hash.length < 2) return result;
const tokens = decodeURIComponent(hash.slice(1))
.split(';')
.map(token => token.trim())
.filter(Boolean);
for (const token of tokens) {
if (token === 'play') {
result.play = true;
continue;
}
if (token === 'replace') {
result.replace = true;
continue;
}
if (token === 'emot') {
result.emot = true;
continue;
}
if (token === 'popup') {
result.popup = true;
continue;
}
if (token === 'icon') {
result.icon = true;
continue;
}
if (token === 'info') {
result.info = true;
continue;
}
if (/^h:\d+$/i.test(token)) {
const value = parseInt(token.slice(2), 10);
if (value > 0) result.height = value;
continue;
}
if (/^w:\d+$/i.test(token)) {
const value = parseInt(token.slice(2), 10);
if (value > 0) result.width = value;
continue;
}
if (/^media:/i.test(token)) {
const value = token.slice(6).trim();
if (value) result.mediaUrl = normalizeMediaUrl(value, baseUrl);
continue;
}
result.nativeHashParts.push(token);
}
if (result.nativeHashParts.length > 0) {
result.nativeHash = '#' + result.nativeHashParts.join(';');
}
return result;
}
function normalizeMediaUrl(value, baseUrl) {
if (/^https?:\/\//i.test(value)) return value;
if (value.startsWith('//')) return `${location.protocol}${value}`;
try {
if (value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) {
return new URL(value, `${baseUrl.origin}${baseUrl.pathname}${baseUrl.search}`).href;
}
} catch {}
return `https://${value}`;
}
function detectDirectMediaType(url, pathname) {
if (/\.(jpeg|jpg|gif|png|webp|bmp|svg|avif)$/i.test(pathname)) return 'image';
if (/\.(mp4|webm|ogv|mov|m4v)$/i.test(pathname)) return 'video';
if (/\.(mp3|wav|ogg|flac|m4a|opus|aac)$/i.test(pathname)) return 'audio';
if (isDiscordCdn(url)) {
if (/\.(jpeg|jpg|gif|png|webp|bmp|svg|avif)$/i.test(pathname)) return 'image';
if (/\.(mp4|webm|ogv|mov|m4v)$/i.test(pathname)) return 'video';
if (/\.(mp3|wav|ogg|flac|m4a|opus|aac)$/i.test(pathname)) return 'audio';
}
return null;
}
function isDiscordCdn(url) {
return /(^|\.)cdn\.discordapp\.com$|(^|\.)media\.discordapp\.net$|(^|\.)images-ext-\d+\.discordapp\.net$/i.test(url.hostname);
}
function isSoundCloudUrl(url) {
return /(^|\.)soundcloud\.com$|(^|\.)snd\.sc$/i.test(url.hostname);
}
function buildEmbedKey(info) {
return [
info.type,
info.mediaSrc,
info.directives.play ? 'play' : '',
info.directives.popup ? 'popup' : '',
info.directives.replace ? 'replace' : '',
info.directives.info ? 'info' : '',
info.directives.width || '',
info.directives.height || '',
info.directives.mediaUrl || ''
].join('|');
}
function createEmbedNode(info) {
const d = info.directives;
if (info.type === 'youtube') {
const iframe = document.createElement('iframe');
const params = new URLSearchParams();
params.set('rel', '0');
params.set('playsinline', '1');
if (d.play) params.set('autoplay', '1');
const startSeconds = extractYouTubeStart(info.nativeHashParts || []);
if (startSeconds > 0) params.set('start', String(startSeconds));
iframe.src = `https://www.youtube-nocookie.com/embed/${info.videoId}?${params.toString()}`;
iframe.width = '560';
iframe.height = '315';
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.allowFullscreen = true;
return iframe;
}
if (info.type === 'spotify') {
const iframe = document.createElement('iframe');
const params = new URLSearchParams();
if (d.play) params.set('autoplay', '1');
const suffix = params.toString() ? `?${params.toString()}` : '';
iframe.src = `https://open.spotify.com/embed/${info.spotifyKind}/${info.spotifyId}${suffix}`;
iframe.width = '300';
iframe.height = String(info.spotifyKind === 'track' ? 152 : 352);
iframe.allow = 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture';
return iframe;
}
if (info.type === 'soundcloud') {
const iframe = document.createElement('iframe');
const scParams = new URLSearchParams({
url: info.originalUrl,
auto_play: d.play ? 'true' : 'false',
visual: 'true'
});
iframe.src = `https://w.soundcloud.com/player/?${scParams.toString()}`;
iframe.width = '100%';
iframe.height = '300';
iframe.allow = 'autoplay';
return iframe;
}
if (info.type === 'image') {
const img = document.createElement('img');
img.src = d.mediaUrl || info.mediaSrc;
img.loading = 'lazy';
applyMediaDimensions(img, d);
return img;
}
if (info.type === 'video') {
const video = document.createElement('video');
video.src = info.mediaSrc;
video.controls = true;
video.playsInline = true;
if (d.play) {
video.autoplay = true;
video.muted = true;
}
applyMediaDimensions(video, d);
return video;
}
if (info.type === 'audio') {
const audio = document.createElement('audio');
audio.src = info.mediaSrc;
audio.controls = true;
if (d.play) audio.autoplay = true;
return audio;
}
return null;
}
function extractYouTubeStart(hashParts) {
for (const part of hashParts) {
const lowered = String(part).toLowerCase();
if (/^t=\d+$/.test(lowered)) return parseInt(lowered.slice(2), 10);
if (/^start=\d+$/.test(lowered)) return parseInt(lowered.slice(6), 10);
const timeMatch = lowered.match(/^t=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
if (timeMatch) {
const h = parseInt(timeMatch[1] || '0', 10);
const m = parseInt(timeMatch[2] || '0', 10);
const s = parseInt(timeMatch[3] || '0', 10);
return h * 3600 + m * 60 + s;
}
}
return 0;
}
function applyMediaDimensions(node, directives) {
if (!(node instanceof HTMLElement)) return;
if (directives.width) {
node.style.width = `${directives.width}px`;
} else {
node.style.width = 'auto';
}
if (directives.height) {
node.style.height = `${directives.height}px`;
} else {
node.style.height = 'auto';
}
if (directives.width && !directives.height) {
node.style.height = 'auto';
}
if (directives.height && !directives.width) {
node.style.width = 'auto';
}
}
function wrapEmbed(embedNode, key) {
const holder = document.createElement('span');
holder.className = 'mme-embed-holder';
holder.dataset.mmeEmbedKey = key;
const box = document.createElement('span');
box.className = 'mme-embed-box';
box.appendChild(embedNode);
holder.appendChild(box);
return holder;
}
function appendEmbedToParentEnd(link, embedNode, key) {
const parent = link.parentNode;
if (!(parent instanceof Element)) return;
const duplicate = Array.from(parent.children).find(child =>
child instanceof HTMLElement &&
child.classList.contains('mme-embed-holder') &&
child.dataset.mmeEmbedKey === key
);
if (duplicate) return;
const holder = wrapEmbed(embedNode, key);
parent.appendChild(holder);
}
function replaceLinkWithNode(link, node, withBreaks) {
const parent = link.parentNode;
if (!parent) return;
if (!withBreaks) {
link.replaceWith(node);
return;
}
const before = document.createElement('br');
const after = document.createElement('br');
link.replaceWith(before, node, after);
}
function createEmotImage(src) {
const img = document.createElement('img');
img.src = src;
img.loading = 'lazy';
img.className = 'mme-inline-emot';
return img;
}
function createIconImage(src) {
const img = document.createElement('img');
img.src = src;
img.loading = 'lazy';
img.className = 'badge--icon';
return img;
}
function applyMediaDirective(link, directives) {
const img = document.createElement('img');
img.src = directives.mediaUrl;
img.loading = 'lazy';
if (directives.height) {
img.style.height = `${directives.height}px`;
img.style.width = directives.width ? `${directives.width}px` : 'auto';
}
if (directives.width) {
img.style.width = `${directives.width}px`;
if (!directives.height) img.style.height = 'auto';
}
if (!directives.width && !directives.height) {
img.style.height = 'auto';
img.style.maxWidth = '100%';
}
link.textContent = '';
link.appendChild(img);
}
function preparePopupLink(link, info) {
if (info.directives.mediaUrl && info.type === 'image') {
applyMediaDirective(link, info.directives);
} else if (info.directives.emot && info.type === 'image') {
const img = createEmotImage(info.directives.mediaUrl || info.mediaSrc);
link.textContent = '';
link.appendChild(img);
} else if (info.directives.icon && info.type === 'image') {
const img = createIconImage(info.directives.mediaUrl || info.mediaSrc);
link.textContent = '';
link.appendChild(img);
}
link.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const node = createEmbedNode(info);
if (!node) return;
if (node instanceof HTMLIFrameElement && info.type === 'youtube') {
node.width = '900';
node.height = '506';
}
if (node instanceof HTMLImageElement || node instanceof HTMLVideoElement) {
node.style.maxWidth = '88vw';
node.style.maxHeight = '80vh';
}
modalApi.open(node);
});
}
})();