// ==UserScript==
// @name Lecteur Media
// @namespace http://tampermonkey.net/
// @version 1.1.6
// @description Intègre TOUS les lecteurs pour Instagram, Facebook, Twitter, TikTok, Webmshare, (et 50 autres) sur JVC.
// @author FaceDePet
// @match https://www.jeuxvideo.com/forums/*
// @match https://jvarchive.com/*
// @match https://jvarchive.st/*
// @icon https://cdn-icons-png.flaticon.com/512/4187/4187272.png
// @grant GM_addStyle
// @grant GM.xmlHttpRequest
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// =========================================================================
// == HELPERS
// =========================================================================
// Gère le redimensionnement dynamique des iframes en écoutant les événements `postMessage`
const IframeResizeManager = {
isInitialized: false,
handlers: {},
register: function(origin, handler) {
this.handlers[origin] = handler;
},
init: function() {
if (this.isInitialized) {
return;
}
window.addEventListener('message', this._handleMessage.bind(this));
this.isInitialized = true;
},
_handleMessage: function(event) {
console.log('Message reçu de:', event.origin);
const handler = this.handlers[event.origin];
if (handler) {
handler.process(event);
}
}
};
// --- Définition des Handlers ---
const twitterResizeHandler = {
maxHeight: 520,
process: function(event) {
let data;
if (typeof event.data === 'string') {
try { data = JSON.parse(event.data); } catch (e) { return; }
} else if (typeof event.data === 'object' && event.data !== null) {
data = event.data;
} else {
return;
}
const embedData = data['twttr.embed'];
if (embedData?.method === 'twttr.private.resize' && embedData.params?.[0]?.height) {
this._resizeIframe(embedData.params[0], event.source);
}
},
_resizeIframe: function(params, sourceWindow) {
const heightFromTwitter = params.height;
if (heightFromTwitter <= 0 || !sourceWindow) return;
const finalHeight = Math.min(heightFromTwitter, this.maxHeight);
const iframes = document.querySelectorAll('iframe.iframe-twitter');
for (const iframe of iframes) {
if (iframe.contentWindow === sourceWindow) {
iframe.style.height = `${finalHeight}px`;
iframe.scrolling = (heightFromTwitter > this.maxHeight) ? 'yes' : 'no';
break;
}
}
}
};
const redditResizeHandler = {
process: function(event) {
let data;
if (typeof event.data === 'string') {
try { data = JSON.parse(event.data); } catch (e) { return; }
} else if (typeof event.data === 'object' && event.data !== null) {
data = event.data;
} else {
return;
}
if (data && data.type === 'resize.embed' && typeof data.data === 'number') {
const height = data.data;
this._resizeIframe(height, event.source);
}
},
_resizeIframe: function(height, sourceWindow) {
if (height <= 0 || !sourceWindow) return;
const iframes = document.querySelectorAll('iframe.iframe-reddit');
for (const iframe of iframes) {
if (iframe.contentWindow === sourceWindow) {
iframe.style.height = `${height}px`;
break;
}
}
}
};
IframeResizeManager.register('https://platform.twitter.com', twitterResizeHandler);
IframeResizeManager.register('https://embed.reddit.com', redditResizeHandler);
// =========================================================================
// == STYLES GLOBAUX
// =========================================================================
GM_addStyle(`
.bloc-embed {
margin: 1em 0;
margin-top: 0;
display: flex;
justify-content: left;
}
.iframe-embed, .video-embed, .image-embed, .thumbnail-embed, .facebook-embed-placeholder {
max-width: 550px;
width: 100%;
border-radius: 9px;
border: none;
display: block;
background-color: #1c1c1c;
}
html:not(.theme-light) .facebook-embed-placeholder {
border: 1px solid #444;
}
html.theme-light .facebook-embed-placeholder {
background-color: #f0f2f5;
border: 1px solid #ddd;
}
.iframe-twitter {
height: 500px;
background-color: transparent;
transition: height 0.4s ease-in-out;
}
.iframe-giphy {
aspect-ratio: 16 / 9;
height: auto;
}
.iframe-streamable {
height: auto;
}
.iframe-youtube, .iframe-streamable, .iframe-tiktok, .iframe-twitch, .iframe-vocaroo, .iframe-reddit, .iframe-giphy {
max-height: 80vh;
}
.iframe-vertical-content {
max-width: 320px;
max-height: 65vh;
}
.iframe-youtube-short {
aspect-ratio: 9 / 16;
height: auto;
}
.iframe-youtube {
aspect-ratio: 16 / 9;
height: auto;
}
.youtube-facade-container {
position: relative;
display: block;
aspect-ratio: 16 / 9;
max-width: 550px;
width: 100%;
}
.youtube-facade-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
cursor: pointer;
background: transparent;
z-index: 1;
}
.youtube-facade-container .iframe-youtube {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
@media (max-width: 768px) {
.iframe-youtube,
.youtube-facade-container {
aspect-ratio: 5 / 4;
}
}
.iframe-tiktok {
height: auto;
aspect-ratio: 9 / 16.5;
max-height: 75vh;
background-color: #000;
}
.iframe-twitch {
aspect-ratio: 16 / 9;
height: auto;
}
.iframe-vocaroo {
max-width: 300px;
width: 100%;
height: 60px;
}
.iframe-reddit {
height: 500px;
background-color: transparent;
transition: height 0.3s ease-in-out;
}
.video-embed, .image-embed, .thumbnail-embed {
height: auto;
max-height: 80vh;
object-fit: contain;
}
.placeholder-embed {
padding: 20px;
text-align: center;
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
}
.placeholder-embed a {
text-decoration: none;
font-weight: 500;
}
html:not(.theme-light) .placeholder-embed {
background-color: #2a2a2e;
border: 1px solid #444;
}
html:not(.theme-light) .placeholder-embed a {
color: #b9bbbe;
}
html.theme-light .placeholder-embed {
background-color: #f0f2f5;
border: 1px solid #ddd;
}
html.theme-light .placeholder-embed a {
color: #555;
}
.twitter-loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
min-height: 120px;
}
html:not(.theme-light) .twitter-loading-placeholder {
background-color: #1c1c1c;
color: #b9bbbe;
}
html.theme-light .twitter-loading-placeholder {
background-color: #f0f2f5;
color: #555;
}
.tweet-unavailable-placeholder {
display: flex;
align-items: center;
gap: 15px;
text-align: left;
padding: 15px 20px;
}
.tweet-unavailable-placeholder .icon-container {
flex-shrink: 0;
}
.tweet-unavailable-placeholder .icon-container svg {
width: 40px;
height: 40px;
opacity: 0.6;
}
.tweet-unavailable-placeholder .text-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.tweet-unavailable-placeholder strong {
font-weight: 600;
font-size: 15px;
}
html:not(.theme-light) .tweet-unavailable-placeholder strong {
color: #e4e6eb;
}
html.theme-light .tweet-unavailable-placeholder strong {
color: #050505;
}
.tweet-unavailable-placeholder .description {
font-size: 13px;
}
html:not(.theme-light) .tweet-unavailable-placeholder .description {
color: #b9bbbe;
}
html.theme-light .tweet-unavailable-placeholder .description {
color: #65676b;
}
.tweet-unavailable-placeholder a {
font-size: 13px;
text-decoration: underline;
opacity: 0.9;
}
.snapchat-embed-placeholder {
max-width: 416px;
min-height: 650px;
background-color: #333;
border-radius: 12px;
}
html.theme-light .snapchat-embed-placeholder {
background-color: #e0e0e0;
}
.iframe-google-drive {
height: 600px;
max-height: 80vh;
aspect-ratio: 4 / 3;
}
.iframe-google-slides {
height: auto;
aspect-ratio: 16 / 9;
}
.iframe-google-maps {
aspect-ratio: 16 / 9;
height: 450px;
max-height: 75vh;
}
.dead-link-sticker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-family: Arial, sans-serif;
color: #8c8c8c;
background-color: #f0f2f5;
border: 1px solid transparent;
padding: 5px 10px;
border-radius: 8px;
}
html:not(.theme-light) .dead-link-sticker {
color: #b9bbbe;
background-color: transparent;
border: transparent;
}
.dead-link-sticker img {
width: 50px;
height: auto;
}
.iframe-flourish {
height: 450px;
max-height: 85vh;
background-color: #ffffff;
}
.thumbnail-embed img { width: 100%; height: 100%; object-fit: cover; }
.thumbnail-embed { position: relative; cursor: pointer; overflow: hidden; }
.jvchat-content .bloc-embed { justify-content: flex-start; }
.instagram-placeholder { min-height: 450px; max-width: 500px; width: calc(100% - 20px); margin: 1em auto; border-radius: 8px; }
.article-preview-card {
max-width: 550px;
width: 100%;
border-radius: 12px;
border: 1px solid #444;
display: flex;
flex-direction: column;
text-decoration: none;
overflow: hidden;
transition: background-color 0.2s ease;
}
html:not(.theme-light) .article-preview-card {
background-color: #2a2a2e;
}
html:not(.theme-light) .article-preview-card:hover {
background-color: #333338;
}
html.theme-light .article-preview-card {
border: 1px solid #ddd;
background-color: #f0f2f5;
}
html.theme-light .article-preview-card:hover {
background-color: #e8eaf0;
}
.article-preview-image {
width: 100%;
aspect-ratio: 1.91 / 1;
background-size: cover;
background-position: center;
border-bottom: 1px solid #444;
}
html.theme-light .article-preview-image {
border-bottom: 1px solid #ddd;
}
.article-preview-content {
padding: 12px 15px;
display: flex;
flex-direction: column;
gap: 4px;
}
.article-preview-sitename {
font-size: 12px;
font-family: Arial, sans-serif;
text-transform: uppercase;
}
.article-preview-title {
font-size: 16px;
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.article-preview-description {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-height: 5.4em; /* Limite à environ 3 lignes */
overflow: hidden;
text-overflow: ellipsis;
}
html:not(.theme-light) .article-preview-sitename { color: #8c8c8c; }
html:not(.theme-light) .article-preview-title { color: #e4e6eb; }
html:not(.theme-light) .article-preview-description { color: #b9bbbe; }
html.theme-light .article-preview-sitename { color: #65676b; }
html.theme-light .article-preview-title { color: #050505; }
html.theme-light .article-preview-description { color: #65676b; }
.distrokid-embed-card {
display: flex;
align-items: center;
max-width: 550px;
width: 100%;
border-radius: 12px;
overflow: hidden;
text-decoration: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
transition: background-color 0.2s ease;
}
html:not(.theme-light) .distrokid-embed-card {
background-color: #2a2a2e;
border: 1px solid #444;
}
html.theme-light .distrokid-embed-card {
background-color: #f0f2f5;
border: 1px solid #ddd;
}
html:not(.theme-light) .distrokid-embed-card:hover {
background-color: #333338;
}
html.theme-light .distrokid-embed-card:hover {
background-color: #e8eaf0;
}
.distrokid-album-art {
width: 90px;
height: 90px;
flex-shrink: 0;
background-size: cover;
background-position: center;
}
.distrokid-content {
flex-grow: 1;
padding: 10px 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 90px;
box-sizing: border-box;
}
.distrokid-title {
font-size: 16px;
font-weight: 600;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
}
html:not(.theme-light) .distrokid-title { color: #e4e6eb; }
html.theme-light .distrokid-title { color: #050505; }
.distrokid-artist {
font-size: 14px;
}
html:not(.theme-light) .distrokid-artist { color: #b9bbbe; }
html.theme-light .distrokid-artist { color: #65676b; }
.distrokid-content audio {
width: 100%;
height: 30px;
}
.tweet-loading-overlay {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.7);
color: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 15px;
font-weight: 500;
z-index: 10;
border-radius: 9px;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
html.theme-light .tweet-loading-overlay {
background: rgba(255, 255, 255, 0.7);
color: #0f1419;
}
.tweet-embed-wrapper {
position: relative;
display: block;
line-height: 0;
}
.overlay-replies-button {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(20, 23, 26, 0.85);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
text-align: center;
padding: 12px 0;
cursor: pointer;
border-bottom-left-radius: 9px;
border-bottom-right-radius: 9px;
transition: background-color 0.2s ease;
display: block; /* Visible par défaut */
}
.tweet-embed-wrapper.showing-replies .overlay-replies-button {
display: none;
}
.overlay-replies-button a {
color: white;
text-decoration: none;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
}
html.theme-light .overlay-replies-button {
background: rgba(255, 255, 255, 0.8);
}
html.theme-light .overlay-replies-button a {
color: #0f1419;
}
.iframe-pdf {
height: 600px;
max-height: 80vh;
aspect-ratio: 4 / 3;
}
`);
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const foundElement = document.querySelector(selector);
if (foundElement) {
obs.disconnect();
callback(foundElement);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
class EmbedManager {
constructor(providers) {
this.providers = providers;
this.isDarkTheme = !document.documentElement.classList.contains('theme-light');
this.embedCreatorObserver = new IntersectionObserver(async (entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const link = entry.target;
observer.unobserve(link);
const providerName = link.dataset.providerName;
if (!providerName) continue;
const provider = this.providers.find(p => p.name === providerName);
if (!provider) continue;
try {
const embedElement = await provider.createEmbedElement(link, this.isDarkTheme);
if (embedElement) {
link.after(embedElement);
if (provider.postProcess) {
setTimeout(() => provider.postProcess(embedElement), 50);
}
}
} catch (e) {
console.error(`[${provider.name}] Erreur lors de la création différée:`, e);
}
}
}
}, { rootMargin: '400px 0px' });
}
init() {
const hostname = window.location.hostname;
if (hostname === 'www.jeuxvideo.com') {
waitForElement('.conteneur-messages-pagi', () => this.startObserving('.conteneur-messages-pagi', '[MiniaTweetPRO+] Observateur du forum JVC activé.'));
waitForElement('#jvchat-main', () => this.startObserving('#jvchat-main', '[MiniaTweetPRO+] Observateur de JVChat activé.'));
} else if (hostname === 'jvarchive.com' || hostname === 'jvarchive.st') {
waitForElement('body', () => this.startObserving('body', '[MiniaTweetPRO+] Observateur de JVArchive activé.'));
}
}
startObserving(selector, startMessage) {
const targetNode = document.querySelector(selector);
if (!targetNode) return;
console.log(startMessage);
const messageSelector = '.txt-msg, .jvchat-bloc-message, .message-content';
const scanAndDelegate = (scope) => {
const messageSelector = '.txt-msg, .jvchat-bloc-message, .message-content';
const messageElements = [];
if (scope.nodeType === Node.ELEMENT_NODE) {
if (scope.matches(messageSelector)) {
messageElements.push(scope);
}
messageElements.push(...scope.querySelectorAll(messageSelector));
}
messageElements.forEach(messageElement => {
this.providers.forEach(provider => {
messageElement.querySelectorAll(provider.selector).forEach(link => {
if (link.dataset.miniatweetProcessed) return;
link.dataset.miniatweetProcessed = 'true';
link.dataset.providerName = provider.name;
this.embedCreatorObserver.observe(link);
});
});
});
};
scanAndDelegate(targetNode);
const mutationObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
scanAndDelegate(node);
}
}
if (mutation.type === 'childList' && mutation.target.nodeType === Node.ELEMENT_NODE) {
scanAndDelegate(mutation.target);
}
}
});
mutationObserver.observe(targetNode, {
childList: true,
subtree: true,
});
}
}
// =========================================================================
// == FOURNISSEURS DE SERVICES
// =========================================================================
const providers = [
{
name: 'Instagram',
selector: 'a[href*="instagram.com/"]:not([data-miniatweet-processed])',
scriptPromise: null,
loadScript() {
if (!this.scriptPromise) {
this.scriptPromise = new Promise((resolve) => {
if (unsafeWindow.instgrm) return resolve();
const script = document.createElement('script');
script.async = true;
script.charset = 'utf-8';
script.src = 'https://www.instagram.com/embed.js';
script.onload = resolve;
document.head.appendChild(script);
});
}
return this.scriptPromise;
},
createEmbedElement(link, isDarkTheme) {
this.loadScript();
const cleanedUrl = link.href.replace(/(\.com\/)[^/]+\/((p|reel)\/)/, '$1$2');
const blockquote = document.createElement('blockquote');
blockquote.className = 'instagram-media instagram-placeholder';
blockquote.setAttribute('data-instgrm-permalink', cleanedUrl);
blockquote.setAttribute('data-instgrm-version', '14');
blockquote.style.background = isDarkTheme ? '#2f3136' : '#f9f9f9';
blockquote.style.border = isDarkTheme ? '1px solid #444' : '1px solid #dbdbdb';
return blockquote;
},
postProcess() {
this.loadScript().then(() => {
if (unsafeWindow.instgrm) unsafeWindow.instgrm.Embeds.process();
});
}
},
{
name: 'Twitter',
selector: 'a[href*="twitter.com/"], a[href*="x.com/"]',
createEmbedElement(link, isDarkTheme) {
IframeResizeManager.init();
const parts = link.href.split('/');
const statusIndex = parts.findIndex(p => p === 'status' || p === 'statuses');
if (statusIndex === -1 || !parts[statusIndex + 1]) return null;
const tweetId = parts[statusIndex + 1].split(/[?#]/)[0];
const uniqueId = `${tweetId}-${Math.random().toString(36).substring(2, 9)}`;
const containerId = `tweet-container-${uniqueId}`;
const placeholderId = `tweet-placeholder-${uniqueId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<div id="${containerId}" class="tweet-embed-wrapper iframe-embed" style="padding: 0; background-color: transparent;">
<div id="${placeholderId}" class="twitter-loading-placeholder">Chargement du Tweet...</div>
</div>`;
let isHandled = false;
const handleResponse = (status, finalUrl) => {
if (isHandled) return;
isHandled = true;
const mainContainer = document.getElementById(containerId);
const placeholderElement = document.getElementById(placeholderId);
if (!mainContainer || !placeholderElement) return;
if (status === 200) {
const theme = isDarkTheme ? 'dark' : 'light';
const iframeUrl = `https://platform.twitter.com/embed/Tweet.html?id=${tweetId}&theme=${theme}&dnt=true&lang=fr`;
const iframeId = `twitter-iframe-${uniqueId}`;
placeholderElement.outerHTML = `
<iframe id="${iframeId}" src="${iframeUrl}" class="iframe-embed iframe-twitter" title="Twitter Tweet" sandbox="allow-scripts allow-same-origin" allowtransparency="true" allowfullscreen allow="fullscreen" scrolling="no"></iframe>
<div class="overlay-replies-button">
<a href="#" class="show-replies-btn">Afficher les réponses</a>
</div>
`;
const targetIframe = document.getElementById(iframeId);
const repliesContainerButton = mainContainer.querySelector('.overlay-replies-button');
targetIframe.addEventListener('load', () => {
const loadingOverlay = mainContainer.querySelector('.tweet-loading-overlay');
if (loadingOverlay) loadingOverlay.remove();
try {
if (targetIframe.contentWindow.location.href.includes('platform.twitter.com')) {
mainContainer.classList.remove('showing-replies');
} else {
mainContainer.classList.add('showing-replies');
}
} catch (e) {
}
});
repliesContainerButton.addEventListener('click', (e) => {
e.preventDefault();
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'tweet-loading-overlay';
loadingOverlay.textContent = 'Chargement des réponses...';
mainContainer.appendChild(loadingOverlay);
const nitterUrl = link.href.replace(/x\.com|twitter\.com/, 'nitter.net');
const finalNitterUrl = `${nitterUrl.split('?')[0]}`;
targetIframe.src = finalNitterUrl;
targetIframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
targetIframe.style.height = '80vh';
targetIframe.setAttribute('scrolling', 'yes');
});
} else {
mainContainer.className = 'placeholder-embed tweet-unavailable-placeholder';
mainContainer.innerHTML = `
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"></path>
</svg>
</div>
<div class="text-container">
<strong>Ce Tweet n'est plus disponible</strong>
<span class="description">Le lien est peut-être rompu ou le Tweet a été supprimé par son auteur.</span>
<a href="${link.href || finalUrl}" target="_blank" rel="noopener noreferrer">Consulter le lien d'origine</a>
</div>
`;
}
};
GM.xmlHttpRequest({
method: "GET",
url: `https://publish.twitter.com/oembed?url=${encodeURIComponent(link.href)}`,
onload: function(response) { handleResponse(response.status, response.finalUrl); },
onerror: function(response) { handleResponse(response.status || 0, response.finalUrl); }
});
return container;
}
},
{
name: 'TikTok',
selector: 'a[href*="tiktok.com/"], a[href*="vm.tiktok.com/"]',
async createEmbedElement(link) {
console.log("Computing from Tiktok provider");
let finalUrl = link.href;
if (finalUrl.includes('vm.tiktok.com')) {
try {
finalUrl = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: finalUrl,
onload: response => resolve(response.finalUrl),
onerror: error => reject(error),
ontimeout: error => reject(error)
});
});
} catch (error) {
console.error('[TikTok] Impossible de résoudre le lien court :', error);
return null;
}
}
const match = finalUrl.match(/\/(?:video|photo)\/(\d+)/);
if (!match || !match[1]) {
console.log('[TikTok] Le lien ne semble pas être une vidéo ou une photo valide :', finalUrl);
return null;
}
const postId = match[1];
const iframeUrl = `https://www.tiktok.com/embed/v2/${postId}?lang=fr-FR&referrer=${encodeURIComponent(window.location.href)}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-tiktok iframe-vertical-content"
title="TikTok Post"
sandbox="allow-scripts allow-same-origin allow-popups"
allow="autoplay; encrypted-media"
aspect-ratio: auto;"></iframe>`;
return container;
}
},
{
name: 'Streamable',
selector: 'a[href*="streamable.com/"]',
createEmbedElement(link) {
const cleanUrl = link.href.replace('/e/', '/');
const httpsUrl = cleanUrl.replace('http://', 'https://');
const iframeUrl = httpsUrl.replace('.com/', '.com/e/');
const container = document.createElement('div');
container.className = 'bloc-embed';
const iframe = document.createElement('iframe');
iframe.src = iframeUrl;
iframe.className = 'iframe-embed iframe-streamable';
iframe.title = "Streamable Video";
iframe.setAttribute('allowfullscreen', '');
iframe.style.aspectRatio = '16 / 9';
container.appendChild(iframe);
GM.xmlHttpRequest({
method: 'GET',
url: `https://api.streamable.com/oembed.json?url=${encodeURIComponent(cleanUrl)}`,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
if (data.width && data.height) {
if (data.height > data.width) {
iframe.classList.add('iframe-vertical-content');
}
iframe.style.aspectRatio = `${data.width} / ${data.height}`;
}
} catch (e) {
console.error('[Streamable API] Erreur de parsing JSON.', e);
}
}
},
onerror: function(error) {
console.error('[Streamable API] Erreur GM.xmlHttpRequest:', error);
}
});
return container;
}
},
{
name: 'Webmshare',
selector: 'a[href*="webmshare.com/"]',
createEmbedElement(link) {
if (!/webmshare\.com\/(play\/)?[\w]+$/.test(link.href)) return null;
const videoId = link.pathname.split('/').pop();
if (!videoId) return null;
const videoUrl = `https://s1.webmshare.com/${videoId}.webm`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<video src="${videoUrl}" class="video-embed" controls autoplay muted loop playsinline></video>`;
return container;
}
},
{
name: 'YouTube',
selector: 'a[href*="youtube.com/watch"], a[href*="youtu.be/"], a[href*="youtube.com/shorts/"]',
createEmbedElement(link) {
const youtubeRegex = /(?:[?&]v=|\/shorts\/|youtu\.be\/)([^?&/\s]{11})/;
const match = link.href.match(youtubeRegex);
if (!match || !match[1]) return null;
const videoId = match[1];
const isShort = link.href.includes('/shorts/');
if (isShort) {
const iframeUrl = `https://www.youtube.com/embed/${videoId}`;
const iframeClasses = 'iframe-embed iframe-youtube-short iframe-vertical-content';
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe src="${iframeUrl}" class="${iframeClasses}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`;
return container;
}
let startTime = null;
try {
const url = new URL(link.href);
const timeParam = url.searchParams.get('t');
if (timeParam) {
let totalSeconds = 0;
const hoursMatch = timeParam.match(/(\d+)h/);
const minutesMatch = timeParam.match(/(\d+)m/);
const secondsMatch = timeParam.match(/(\d+)s/);
if (hoursMatch) totalSeconds += parseInt(hoursMatch[1], 10) * 3600;
if (minutesMatch) totalSeconds += parseInt(minutesMatch[1], 10) * 60;
if (secondsMatch) totalSeconds += parseInt(secondsMatch[1], 10);
if (totalSeconds === 0 && /^\d+$/.test(timeParam)) totalSeconds = parseInt(timeParam, 10);
if (totalSeconds > 0) startTime = totalSeconds;
}
} catch (e) {}
const params = new URLSearchParams();
if (startTime) params.append('start', startTime);
params.append('enablejsapi', '1');
params.append('rel', '0');
const container = document.createElement('div');
container.className = 'youtube-facade-container';
const iframe = document.createElement('iframe');
iframe.className = 'iframe-embed iframe-youtube';
iframe.title = "YouTube video player";
iframe.src = `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
console.log("iframe src: ", iframe.src);
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
iframe.setAttribute('allowfullscreen', '');
const facade = document.createElement('div');
facade.className = 'youtube-facade-overlay';
container.appendChild(iframe);
container.appendChild(facade);
facade.addEventListener('click', () => {
try {
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', 'https://www.youtube.com');
} catch (e) {
console.error("Erreur lors de l'envoi du message à l'iframe YouTube:", e);
}
facade.remove();
}, { once: true });
const finalContainer = document.createElement('div');
finalContainer.className = 'bloc-embed';
finalContainer.appendChild(container);
return finalContainer;
}
},
{
name: 'Xcancel',
selector: 'a[href*="xcancel.com/"]',
createEmbedElement(link, isDarkTheme) {
IframeResizeManager.init();
const parts = link.href.split('/');
const statusIndex = parts.findIndex(p => p === 'status' || p === 'statuses');
if (statusIndex === -1 || !parts[statusIndex + 1]) return null;
const tweetId = parts[statusIndex + 1].split(/[?#]/)[0];
const uniqueId = `${tweetId}-${Math.random().toString(36).substring(2, 9)}`;
const containerId = `tweet-container-${uniqueId}`;
const placeholderId = `tweet-placeholder-${uniqueId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<div id="${containerId}" class="tweet-embed-wrapper iframe-embed" style="padding: 0; background-color: transparent;">
<div id="${placeholderId}" class="twitter-loading-placeholder">Chargement du Tweet...</div>
</div>`;
let isHandled = false;
const handleResponse = (status, finalUrl) => {
if (isHandled) return;
isHandled = true;
const mainContainer = document.getElementById(containerId);
const placeholderElement = document.getElementById(placeholderId);
if (!mainContainer || !placeholderElement) return;
if (status === 200) {
const theme = isDarkTheme ? 'dark' : 'light';
const iframeUrl = `https://platform.twitter.com/embed/Tweet.html?id=${tweetId}&theme=${theme}&dnt=true&lang=fr`;
const iframeId = `twitter-iframe-${uniqueId}`;
placeholderElement.outerHTML = `
<iframe id="${iframeId}" src="${iframeUrl}" class="iframe-embed iframe-twitter" title="Twitter Tweet" sandbox="allow-scripts allow-same-origin" allowtransparency="true" allowfullscreen allow="fullscreen" scrolling="no"></iframe>
<div class="overlay-replies-button">
<a href="#" class="show-replies-btn">Afficher les réponses</a>
</div>
`;
const targetIframe = document.getElementById(iframeId);
const repliesContainerButton = mainContainer.querySelector('.overlay-replies-button');
targetIframe.addEventListener('load', () => {
const loadingOverlay = mainContainer.querySelector('.tweet-loading-overlay');
if (loadingOverlay) loadingOverlay.remove();
try {
if (targetIframe.contentWindow.location.href.includes('platform.twitter.com')) {
mainContainer.classList.remove('showing-replies');
} else {
mainContainer.classList.add('showing-replies');
}
} catch (e) {
}
});
repliesContainerButton.addEventListener('click', (e) => {
e.preventDefault();
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'tweet-loading-overlay';
loadingOverlay.textContent = 'Chargement des réponses...';
mainContainer.appendChild(loadingOverlay);
const nitterUrl = link.href.replace('xcancel.com', 'nitter.net');
const finalNitterUrl = `${nitterUrl.split('?')[0]}?theme=${isDarkTheme ? 'dark' : 'light'}`;
targetIframe.src = finalNitterUrl;
targetIframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
targetIframe.style.height = '80vh';
targetIframe.setAttribute('scrolling', 'yes');
});
} else {
mainContainer.className = 'placeholder-embed tweet-unavailable-placeholder';
mainContainer.innerHTML = `
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"></path>
</svg>
</div>
<div class="text-container">
<strong>Ce Tweet n'est plus disponible</strong>
<span class="description">Le lien est peut-être rompu ou le Tweet a été supprimé par son auteur.</span>
<a href="${link.href || finalUrl}" target="_blank" rel="noopener noreferrer">Consulter le lien d'origine</a>
</div>
`;
}
};
const twitterApiUrl = link.href.replace('xcancel.com', 'twitter.com');
GM.xmlHttpRequest({
method: "GET",
url: `https://publish.twitter.com/oembed?url=${encodeURIComponent(twitterApiUrl)}`,
onload: function(response) { handleResponse(response.status, response.finalUrl); },
onerror: function(response) { handleResponse(response.status || 0, response.finalUrl); }
});
return container;
}
},
{
name: 'Facebook',
selector: `
a[href*="facebook.com/posts/"],
a[href*="facebook.com/videos/"],
a[href*="facebook.com/photos/"],
a[href*="facebook.com/photo/"],
a[href*="facebook.com/reel/"],
a[href*="facebook.com/share/"],
a[href*="facebook.com/photo.php"]
`,
scriptPromise: null,
loadScript() {
if (!this.scriptPromise) {
this.scriptPromise = new Promise((resolve) => {
if (unsafeWindow.FB) return resolve();
unsafeWindow.fbAsyncInit = function() {
unsafeWindow.FB.init({ xfbml: true, version: 'v18.0' });
resolve();
};
const script = document.createElement('script');
script.async = true;
script.defer = true;
script.crossOrigin = 'anonymous';
script.src = 'https://connect.facebook.net/fr_FR/sdk.js';
document.head.appendChild(script);
});
}
return this.scriptPromise;
},
async createEmbedElement(link) {
let embedUrl = link.href;
if (link.href.includes('/share/')) {
try {
embedUrl = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: link.href,
onload: response => {
resolve(response.finalUrl || link.href);
},
onerror: error => {
console.error('[Facebook] Erreur de résolution du lien de partage:', error);
resolve(link.href);
}
});
});
} catch (e) {
console.error('[Facebook] Exception lors de la résolution du lien:', e);
}
}
this.loadScript();
const container = document.createElement('div');
container.className = 'bloc-embed facebook-embed-placeholder';
container.style.minHeight = '350px';
container.innerHTML = `<div class="fb-post" data-href="${embedUrl}" data-width="550" data-show-text="true"></div>`;
return container;
},
postProcess(element) {
this.loadScript().then(() => {
if (unsafeWindow.FB) {
unsafeWindow.FB.XFBML.parse(element);
}
});
}
},
{
name: 'Twitch',
selector: 'a[href*="twitch.tv/"]',
createEmbedElement(link) {
const parentHostname = window.location.hostname;
try {
const url = new URL(link.href);
const pathParts = url.pathname.split('/').filter(p => p);
let iframeSrc = null;
if (url.hostname === 'clips.twitch.tv' || pathParts.includes('clip')) {
const clipId = pathParts[pathParts.length - 1];
if (clipId) {
iframeSrc = `https://clips.twitch.tv/embed?clip=${clipId}&parent=${parentHostname}`;
}
}
else if (pathParts[0] === 'videos' && pathParts[1]) {
const videoId = pathParts[1];
iframeSrc = `https://player.twitch.tv/?video=${videoId}&parent=${parentHostname}`;
}
// Gère les directs : twitch.tv/CHANNEL_NAME
else if (pathParts.length === 1 && pathParts[0]) {
const channelName = pathParts[0];
iframeSrc = `https://player.twitch.tv/?channel=${channelName}&parent=${parentHostname}`;
}
if (!iframeSrc) return null;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeSrc}"
class="iframe-embed iframe-twitch"
title="Lecteur vidéo Twitch"
allowfullscreen="true"
frameborder="0">
</iframe>`;
return container;
} catch (e) {
console.error('[Twitch] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Vocaroo',
selector: 'a[href*="vocaroo.com/"], a[href*="voca.ro/"]',
createEmbedElement(link) {
try {
const audioId = link.pathname.split('/').pop();
if (!audioId) return null;
if (link.pathname === '/') return null;
const iframeSrc = `https://vocaroo.com/embed/${audioId}?autoplay=0`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeSrc}"
class="iframe-embed iframe-vocaroo"
title="Lecteur audio Vocaroo"
frameborder="0"
allow="autoplay">
</iframe>`;
return container;
} catch (e) {
console.error('[Vocaroo] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Reddit',
selector: 'a[href*="reddit.com/"]',
async createEmbedElement(link, isDarkTheme) {
IframeResizeManager.init();
try {
let finalUrl;
if (link.pathname.includes('/s/')) {
finalUrl = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: link.href,
onload: (response) => {
resolve(response.finalUrl);
},
onerror: (error) => {
console.error('[Reddit] Erreur GM.xmlHttpRequest :', error);
reject(error);
}
});
});
} else {
finalUrl = link.href;
}
const urlObject = new URL(finalUrl);
if (!urlObject.pathname.includes('/comments/')) {
return null;
}
urlObject.hostname = 'embed.reddit.com';
urlObject.searchParams.set('embed', 'true');
urlObject.searchParams.set('theme', isDarkTheme ? 'dark' : 'light');
urlObject.searchParams.set('showmedia', 'true');
urlObject.searchParams.set('showmore', 'false');
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${urlObject.toString()}"
class="iframe-embed iframe-reddit"
title="Contenu Reddit intégré"
sandbox="allow-scripts allow-same-origin allow-popups"
height="600"
allowfullscreen>
</iframe>`;
return container;
} catch (e) {
console.error('[Reddit] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Vimeo',
selector: 'a[href*="vimeo.com/"]',
createEmbedElement(link) {
const match = link.href.match(/vimeo\.com\/(?:video\/)?(\d+)/);
if (!match || !match[1]) return null;
const videoId = match[1];
const iframeUrl = `https://player.vimeo.com/video/${videoId}?autoplay=1&muted=1&loop=1`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Vimeo video player"
allow="autoplay; fullscreen; picture-in-picture"
frameborder="0"
allowfullscreen>
</iframe>`;
return container;
}
},
{
name: 'Dailymotion',
selector: 'a[href*="dailymotion.com/video/"], a[href*="dai.ly/"]',
createEmbedElement(link) {
const pathParts = new URL(link.href).pathname.split('/');
let videoId = null;
if (link.hostname.includes('dai.ly')) {
videoId = pathParts[1];
} else {
const videoIndex = pathParts.findIndex(p => p === 'video');
if (videoIndex !== -1 && pathParts[videoIndex + 1]) {
videoId = pathParts[videoIndex + 1];
}
}
if (!videoId) return null;
const iframeUrl = `https://www.dailymotion.com/embed/video/${videoId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Dailymotion video player"
allow="autoplay; fullscreen; picture-in-picture"
frameborder="0"
allowfullscreen>
</iframe>`;
return container;
}
},
{
name: 'SoundCloud',
selector: 'a[href*="soundcloud.com/"]',
async createEmbedElement(link) {
const pathParts = new URL(link.href).pathname.split('/').filter(p => p);
if (pathParts.length < 2) return null;
try {
const data = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: `https://soundcloud.com/oembed?format=json&url=${encodeURIComponent(link.href)}&maxheight=166&color=%23ff5500&auto_play=false&show_comments=false`,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(JSON.parse(response.responseText));
} else {
reject(new Error(`L'API SoundCloud a retourné le statut ${response.status}`));
}
},
onerror: (error) => reject(new Error('Erreur réseau lors de la requête vers SoundCloud')),
ontimeout: () => reject(new Error('La requête vers l\'API SoundCloud a expiré'))
});
});
if (!data || !data.html) {
console.warn('[SoundCloud] Pas de HTML d\'intégration dans la réponse API pour', link.href);
return null;
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = data.html;
return container;
} catch (error) {
console.error('[SoundCloud] Erreur lors de la récupération de l\'embed :', error);
return null;
}
}
},
{
name: 'StrawPoll',
selector: 'a[href*="strawpoll.com/"]',
createEmbedElement(link) {
const url = new URL(link.href);
const pathParts = url.pathname.split('/').filter(p => p);
let pollId = null;
if (pathParts[0] === 'polls' && pathParts[1]) {
pollId = pathParts[1];
} else if (pathParts.length === 1 && pathParts[0]) {
pollId = pathParts[0];
}
if (!pollId) return null;
const iframeUrl = `https://strawpoll.com/embed/${pollId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed"
style="width: 100%; height: 450px; border: 0;"
title="Sondage StrawPoll">
</iframe>`;
return container;
}
},
{
name: 'Imgur',
selector: 'a[href*="imgur.com/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
const container = document.createElement('div');
container.className = 'bloc-embed';
const isDirectMedia = url.hostname === 'i.imgur.com' || /\.(mp4|gifv|webm|jpg|jpeg|png|gif)$/i.test(url.pathname);
if (isDirectMedia) {
if (/\.(mp4|gifv|webm)$/i.test(url.pathname)) {
let videoUrl = url.href.replace('.gifv', '.mp4');
container.innerHTML = `<video src="${videoUrl}" class="video-embed" controls autoplay muted loop playsinline></video>`;
}
else {
container.innerHTML = `<img src="${url.href}" class="image-embed" alt="Image depuis Imgur" loading="lazy">`;
}
return container;
}
const pathParts = url.pathname.split('/').filter(p => p);
if (pathParts.length === 0 || ['upload', 'search'].includes(pathParts[0])) {
return null;
}
const embedId = pathParts.join('/').replace(/\.[^/.]+$/, "");
container.classList.add('imgur-embed');
const blockquote = document.createElement('blockquote');
blockquote.className = 'imgur-embed-pub';
blockquote.lang = 'en';
blockquote.setAttribute('data-id', embedId);
blockquote.innerHTML = `<a href="//imgur.com/${embedId}">${link.textContent || 'Voir sur Imgur'}</a>`;
const script = document.createElement('script');
script.async = true;
script.src = 'https://s.imgur.com/min/embed.js';
script.charset = 'utf-8';
container.appendChild(blockquote);
container.appendChild(script);
return container;
} catch (e) {
console.error(`[Imgur] Échec final pour trouver une image valide pour ${link.href}:`, e);
const stickerUrl = 'https://risibank.fr/cache/medias/0/5/512/51206/thumb.png';
const deadLinkContainer = document.createElement('div');
deadLinkContainer.className = 'bloc-embed';
deadLinkContainer.innerHTML = `<div class="dead-link-sticker"><img src="${stickerUrl}" alt="[Média supprimé]"><span>[Média supprimé]</span></div>`;
return deadLinkContainer;
}
return null;
}
},
{
name: 'Flickr',
selector: 'a[href*="flickr.com/photos/"], a[href*="flic.kr/p/"]',
async createEmbedElement(link) {
try {
const apiUrl = `https://www.flickr.com/services/oembed/?url=${encodeURIComponent(link.href)}&format=json&maxwidth=550`;
const data = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: apiUrl,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(JSON.parse(response.responseText));
} else {
reject(new Error(`Erreur API Flickr: ${response.status}`));
}
},
onerror: (error) => reject(error)
});
});
if (!data || !data.html) {
console.warn('[Flickr] Pas de HTML d\'intégration dans la réponse API pour', link.href);
return null;
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = data.html;
return container;
} catch (error) {
console.error('[Flickr] Erreur lors de la récupération de l\'embed :', error);
return null;
}
}
},
{
name: 'Spotify',
selector: 'a[href*="open.spotify.com/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
if (!/\/(track|album|playlist|artist|episode|show)\//.test(url.pathname)) {
return null;
}
const embedUrl = url.href.replace(
'open.spotify.com/',
'open.spotify.com/embed/'
);
const height = url.pathname.includes('/track/') ? '152' : '352';
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
class="iframe-embed"
src="${embedUrl}"
style="height: ${height}px;"
frameborder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
title="Lecteur Spotify intégré">
</iframe>`;
return container;
} catch (e) {
console.error('[Spotify] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Zupimages',
selector: 'a[href*="zupimages.net/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
let imageUrl = null;
if (url.pathname.includes('viewer.php')) {
const imageId = url.searchParams.get('id');
if (imageId) {
imageUrl = `https://zupimages.net/up/${imageId}`;
}
}
else if (url.pathname.startsWith('/up/') && /\.(jpe?g|png|gif|webp)$/i.test(url.pathname)) {
imageUrl = link.href;
}
if (!imageUrl) {
return null;
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${imageUrl}" class="image-embed" alt="Image depuis Zupimages" loading="lazy">`;
return container;
} catch (e) {
console.error('[Zupimages] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Giphy',
selector: 'a[href*="giphy.com/"], a[href*="gph.is/"]',
async createEmbedElement(link) {
try {
let finalUrl = link.href;
if (link.hostname === 'gph.is') {
finalUrl = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: link.href,
onload: response => resolve(response.finalUrl || link.href),
onerror: error => reject(error),
ontimeout: error => reject(error)
});
});
}
const url = new URL(finalUrl);
let gifId = null;
if (url.hostname === 'media.giphy.com' && url.pathname.includes('/media/')) {
const pathParts = url.pathname.split('/');
if (pathParts.length > 2 && pathParts[1] === 'media') {
gifId = pathParts[2];
}
} else if (url.hostname === 'giphy.com' && url.pathname.includes('/gifs/')) {
const lastPart = url.pathname.split('/').filter(p => p).pop();
if (lastPart) {
gifId = lastPart.split('-').pop();
}
}
if (!gifId) {
return null;
}
const iframeUrl = `https://giphy.com/embed/${gifId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Giphy embed"
frameborder="0"
allowfullscreen>
</iframe>`;
return container;
} catch (e) {
console.error('[Giphy] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Telegram',
selector: 'a[href*="t.me/"]',
createEmbedElement(link, isDarkTheme) {
try {
const url = new URL(link.href);
const pathParts = url.pathname.split('/').filter(p => p);
if (pathParts.length < 2 || !/^\d+$/.test(pathParts[1])) {
return null;
}
const postData = `${pathParts[0]}/${pathParts[1]}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
const script = document.createElement('script');
script.async = true;
script.src = 'https://telegram.org/js/telegram-widget.js?22';
script.setAttribute('data-telegram-post', postData);
script.setAttribute('data-width', '100%');
script.setAttribute('data-userpic', 'true');
if (isDarkTheme) {
script.setAttribute('data-dark', '1');
}
container.appendChild(script);
return container;
} catch (e) {
console.error('[Telegram] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'GoogleDrive',
selector: 'a[href*="drive.google.com/"], a[href*="docs.google.com/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
let embedUrl = null;
let iframeClass = 'iframe-embed iframe-google-drive';
const docMatch = url.pathname.match(/\/(document|spreadsheets|presentation|drawings)\/d\/([^/]+)/);
if (docMatch) {
const docType = docMatch[1];
const docId = docMatch[2];
const embedType = (docType === 'presentation' || docType === 'drawings') ? 'embed' : 'preview';
embedUrl = `https://docs.google.com/${docType}/d/${docId}/${embedType}`;
if (docType === 'presentation') {
iframeClass = 'iframe-embed iframe-google-slides';
}
}
const formMatch = url.pathname.match(/\/forms\/d\/e\/([^/]+)/);
if (formMatch) {
const formId = formMatch[1];
embedUrl = `https://docs.google.com/forms/d/e/${formId}/viewform?embedded=true`;
}
const fileMatch = url.pathname.match(/\/file\/d\/([^/]+)/);
if (fileMatch) {
const fileId = fileMatch[1];
embedUrl = `https://drive.google.com/file/d/${fileId}/preview`;
}
const folderMatch = url.pathname.match(/\/drive\/folders\/([^/]+)/);
if (folderMatch) {
const folderId = folderMatch[1];
embedUrl = `https://drive.google.com/embeddedfolderview?id=${folderId}#list`;
}
if (!embedUrl) {
return null;
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe src="${embedUrl}" class="${iframeClass}" frameborder="0" allowfullscreen></iframe>`;
return container;
} catch (e) {
console.error('[GoogleDrive] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'IssouTV',
selector: 'a[href*="issoutv.com/videos/"]',
createEmbedElement(link) {
try {
const videoId = new URL(link.href).pathname.split('/').pop();
if (!videoId) return null;
const videoUrl = `https://issoutv.com/storage/videos/${videoId}.webm`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<video
src="${videoUrl}"
class="video-embed"
controls
autoplay
muted
loop
playsinline>
</video>`;
return container;
} catch (e) {
console.error('[IssouTV] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Bilibili',
selector: 'a[href*="bilibili.com/video/"]',
createEmbedElement(link) {
try {
const match = link.href.match(/\/video\/(BV[a-zA-Z0-9]+)/);
if (!match || !match[1]) {
return null;
}
const videoId = match[1];
const iframeUrl = `https://player.bilibili.com/player.html?bvid=${videoId}&page=1&high_quality=1`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Lecteur vidéo Bilibili"
scrolling="no"
border="0"
frameborder="0"
framespacing="0"
allowfullscreen="true">
</iframe>`;
return container;
} catch (e) {
console.error('[Bilibili] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Koreus',
selector: 'a[href*="koreus.com/video/"]',
createEmbedElement(link) {
try {
const match = link.href.match(/\/video\/(.+?)\.html/);
if (!match || !match[1]) {
return null;
}
const videoId = match[1];
const iframeUrl = `https://www.koreus.com/embed/${videoId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Lecteur vidéo Koreus"
frameborder="0"
allowfullscreen>
</iframe>`;
return container;
} catch (e) {
console.error('[Koreus] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'GoogleMaps',
selector: 'a[href*="google.com/maps/place/"], a[href*="google.com/maps/@"], a[href*="maps.app.goo.gl/"]',
async createEmbedElement(link) {
try {
let finalUrl = link.href;
if (link.hostname === 'maps.app.goo.gl') {
finalUrl = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: link.href,
onload: response => resolve(response.finalUrl || link.href),
onerror: error => reject(error)
});
});
}
const url = new URL(finalUrl);
let query = null;
const placeMatch = url.pathname.match(/\/place\/([^/]+)/);
if (placeMatch && placeMatch[1]) {
query = placeMatch[1];
}
else {
const coordsMatch = url.pathname.match(/@(-?\d+\.\d+,-?\d+\.\d+)/);
if (coordsMatch && coordsMatch[1]) {
query = coordsMatch[1];
}
}
if (!query) {
console.warn('[GoogleMaps] Impossible d\'extraire la localisation depuis:', finalUrl);
return null;
}
const embedUrl = `https://maps.google.com/maps?q=${encodeURIComponent(query)}&output=embed&z=15`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
class="iframe-embed iframe-google-maps"
src="${embedUrl}"
frameborder="0"
allowfullscreen
loading="lazy"
title="Carte Google Maps intégrée">
</iframe>`;
return container;
} catch (e) {
console.error('[GoogleMaps] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'AppleMusic',
selector: 'a[href*="music.apple.com/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
if (!/\/(album|playlist|station|artist)\//.test(url.pathname)) {
return null;
}
const embedUrl = url.href.replace(
'music.apple.com',
'embed.music.apple.com'
);
const isSong = url.searchParams.has('i');
const height = isSong ? '175' : '450';
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
class="iframe-embed"
src="${embedUrl}"
style="height: ${height}px;"
frameborder="0"
allowfullscreen
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation"
allow="autoplay *; encrypted-media *; fullscreen *"
loading="lazy"
title="Lecteur Apple Music intégré">
</iframe>`;
return container;
} catch (e) {
console.error('[AppleMusic] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'DeviantArt',
selector: 'a[href*="deviantart.com/"][href*="/art/"]',
async createEmbedElement(link) {
try {
let normalizedUrl = link.href;
const originalUrl = new URL(link.href);
if (originalUrl.hostname !== 'www.deviantart.com') {
try {
normalizedUrl = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: link.href,
onload: response => {
resolve(response.finalUrl || link.href);
},
onerror: error => reject(error),
ontimeout: error => reject(error)
});
});
} catch (e) {
console.warn('[DeviantArt] La résolution du lien a échoué. On continue avec l\'URL originale.', e);
normalizedUrl = link.href;
}
}
const apiUrl = `https://backend.deviantart.com/oembed?url=${encodeURIComponent(normalizedUrl)}`;
const data = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: apiUrl,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(JSON.parse(response.responseText));
} else {
reject(new Error(`L'API DeviantArt a retourné le statut ${response.status}`));
}
},
onerror: (error) => reject(error),
ontimeout: (error) => reject(new Error('La requête vers l\'API DeviantArt a expiré.'))
});
});
const imageUrl = data.url || data.thumbnail_url;
if (imageUrl) {
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${imageUrl}" class="image-embed" alt="${data.title || 'Art depuis DeviantArt'}" loading="lazy">`;
return container;
} else {
console.warn('[DeviantArt] Aucune URL d\'image trouvée dans la réponse de l\'API pour :', normalizedUrl);
return null;
}
} catch (error) {
console.error('[DeviantArt] Erreur lors de la création de l\'embed :', error);
return null;
}
}
},
{
name: 'Pinterest',
selector: 'a[href*="pinterest."][href*="/pin/"]',
scriptPromise: null,
loadScript() {
if (!this.scriptPromise) {
this.scriptPromise = new Promise((resolve, reject) => {
if (window.PinUtils) return resolve();
const script = document.createElement('script');
script.async = true;
script.defer = true;
script.src = 'https://assets.pinterest.com/js/pinit.js';
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load Pinterest script'));
document.head.appendChild(script);
});
}
return this.scriptPromise;
},
createEmbedElement(link) {
const match = link.href.match(/\/pin\/(\d+)\/?/);
if (!match || !match[1]) {
console.warn('[Pinterest] Impossible d\'extraire l\'ID du Pin depuis :', link.href);
return null;
}
const pinId = match[1];
const canonicalUrl = `https://www.pinterest.com/pin/${pinId}/`;
const container = document.createElement('div');
container.className = 'bloc-embed';
const pinEmbed = document.createElement('a');
pinEmbed.href = canonicalUrl;
pinEmbed.setAttribute('data-pin-do', 'embedPin');
pinEmbed.setAttribute('data-pin-width', 'large');
pinEmbed.setAttribute('data-pin-terse', 'true');
container.appendChild(pinEmbed);
return container;
},
postProcess() {
this.loadScript().then(() => {
if (window.PinUtils && typeof window.PinUtils.build === 'function') {
window.PinUtils.build();
}
}).catch(error => {
console.error('[Pinterest] Erreur lors du chargement ou de l\'exécution du script :', error);
});
}
},
{
name: 'ImageShack',
selector: 'a[href*="imageshack.com/i/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
const pathParts = url.pathname.split('/').filter(p => p);
if (pathParts.length !== 2 || pathParts[0] !== 'i' || !pathParts[1]) {
return null;
}
const imageId = pathParts[1];
if (imageId.length < 3) {
throw new Error("ID d'image ImageShack invalide.");
}
const base36Part = imageId.substring(0, 2);
let filePart = imageId.substring(2);
// Convertir la première partie de base 36 en base 10
const serverFolder = parseInt(base36Part, 36);
if (isNaN(serverFolder)) {
throw new Error(`Échec de la conversion de '${base36Part}' depuis la base 36.`);
}
if (/[a-zA-Z]$/.test(filePart)) {
filePart = filePart.slice(0, -1);
}
// Le format est : https://imagizer.imageshack.com/v2/{transfo}/{dossier}/{fichier}.jpg
const transformationParam = 'xq70';
const imageUrl = `https://imagizer.imageshack.com/v2/${transformationParam}/${serverFolder}/${filePart}.jpg`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${imageUrl}" class="image-embed" alt="Image depuis ImageShack" loading="lazy">`;
return container;
} catch (error) {
console.error(`[ImageShack] Erreur lors de la transformation de l'URL ${link.href}:`, error.message);
return null;
}
}
},
{
name: 'Gofundme',
selector: 'a[href*="gofundme.com/f/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
if (!url.pathname.startsWith('/f/')) {
return null;
}
const cleanUrlPath = url.pathname;
const embedUrl = `https://www.gofundme.com${cleanUrlPath}/widget/large`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${embedUrl}"
class="iframe-embed"
style="height: 620px; border-radius: 8px;"
title="Campagne Gofundme intégrée"
frameborder="0"
scrolling="no">
</iframe>`;
return container;
} catch (e) {
console.error('[Gofundme] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Coub',
selector: 'a[href*="coub.com/view/"]',
createEmbedElement(link) {
try {
const match = link.href.match(/view\/([a-zA-Z0-9]+)/);
if (!match || !match[1]) return null;
const videoId = match[1];
const iframeUrl = `https://coub.com/embed/${videoId}?muted=true&autostart=true&originalSize=false&startWithHD=true`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Coub Video"
allow="autoplay"
frameborder="0"
width="550"
height="310">
</iframe>`;
return container;
} catch (e) {
console.error('[Coub] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Gyazo',
selector: 'a[href^="https://gyazo.com/"]',
async createEmbedElement(link) {
if (link.hostname === 'i.gyazo.com' && /\.(jpe?g|png|gif|webp)$/i.test(link.pathname)) {
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${link.href}" class="image-embed" alt="Image depuis Gyazo" loading="lazy">`;
return container;
}
if (link.hostname === 'gyazo.com' && link.pathname.length > 1) {
try {
const data = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: `https://api.gyazo.com/api/oembed?url=${encodeURIComponent(link.href)}`,
onload: (response) => {
if (response.status === 200) resolve(JSON.parse(response.responseText));
else reject(new Error(`API Gyazo a retourné le statut ${response.status}`));
},
onerror: (err) => reject(err)
});
});
if (data && data.url) {
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${data.url}" class="image-embed" alt="Image depuis Gyazo" loading="lazy">`;
return container;
}
} catch (error) {
console.error('[Gyazo] Erreur lors de la récupération de l\'embed :', error);
return null;
}
}
return null;
}
},
{
name: 'Codepen',
selector: 'a[href*="codepen.io/"]',
createEmbedElement(link) {
if (!link.pathname.includes('/pen/')) return null;
try {
const url = new URL(link.href);
url.pathname = url.pathname.replace('/pen/', '/embed/');
url.searchParams.set('default-tab', 'result');
url.searchParams.set('theme-id', 'dark');
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${url.toString()}"
class="iframe-embed"
style="height: 450px; border: 1px solid #444;"
title="Codepen Embed"
scrolling="no"
frameborder="0"
loading="lazy"
allowtransparency="true"
allowfullscreen="true">
</iframe>`;
return container;
} catch (e) {
console.error('[Codepen] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Pastebin',
selector: 'a[href*="pastebin.com/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
const pathParts = url.pathname.split('/').filter(p => p);
if (pathParts.length !== 1 || !/^[a-zA-Z0-9]{8}$/.test(pathParts[0])) {
return null;
}
const pasteId = pathParts[0];
const iframeUrl = `https://pastebin.com/embed_iframe/${pasteId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed"
style="height: 400px;"
title="Pastebin Embed"
sandbox="allow-scripts allow-same-origin"
frameborder="0">
</iframe>`;
return container;
} catch (e) {
console.error('[Pastebin] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Tenor',
selector: 'a[href*="tenor.com/"][href*="/view/"]',
createEmbedElement(link) {
try {
const gifId = link.pathname.split('-').pop();
if (!gifId || !/^\d+$/.test(gifId)) {
console.warn('[Tenor] ID du GIF non trouvé ou invalide pour le lien :', link.href);
return null;
}
const iframeUrl = `https://tenor.com/embed/${gifId}`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-youtube"
title="Tenor GIF"
frameborder="0"
allowfullscreen
style="width: 100%; min-height: 200px;">
</iframe>`;
GM.xmlHttpRequest({
method: 'GET',
url: `https://tenor.com/oembed?url=${encodeURIComponent(link.href)}`,
onload: (response) => {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const iframe = container.querySelector('iframe');
if (iframe && data && data.width && data.height) {
const aspectRatio = data.width / data.height;
iframe.style.aspectRatio = `${aspectRatio}`;
iframe.style.minHeight = 'auto';
}
} catch (e) {
console.error('[Tenor] Erreur de parsing JSON depuis l\'API oEmbed.', e);
}
}
},
onerror: (error) => {
console.error('[Tenor] Erreur réseau lors de l\'appel à l\'API oEmbed.', error);
}
});
return container;
} catch (e) {
console.error('[Tenor] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Postimages',
selector: 'a[href*="postimg.cc/"]',
async createEmbedElement(link) {
try {
const url = new URL(link.href);
if (url.hostname === 'postimg.cc' && url.pathname.startsWith('/image/')) {
const pageHtml = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: link.href,
onload: response => resolve(response.responseText),
onerror: error => reject(error)
});
});
const doc = new DOMParser().parseFromString(pageHtml, 'text/html');
const imageUrl = doc.querySelector('meta[property="og:image"]')?.getAttribute('content');
if (imageUrl) {
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${imageUrl}" class="image-embed" alt="Image depuis Postimages" loading="lazy">`;
return container;
}
}
return null;
} catch (e) {
console.error('[Postimages] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'ImgBB',
selector: 'a[href*="ibb.co/"]',
async createEmbedElement(link) {
if (link.pathname.split('/').filter(p => p).length !== 1) {
return null;
}
try {
const pageHtml = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: link.href,
onload: response => resolve(response.responseText),
onerror: error => reject(error),
ontimeout: () => reject(new Error('Timeout'))
});
});
const match = pageHtml.match(/<meta\s+property="og:image"\s+content="([^"]+)"/);
const imageUrl = match ? match[1] : null;
if (imageUrl) {
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<img src="${imageUrl}" class="image-embed" alt="Image depuis ImgBB" loading="lazy">`;
return container;
} else {
console.warn('[ImgBB] Impossible de trouver l\'URL de l\'image pour :', link.href);
return null;
}
} catch (e) {
console.error('[ImgBB] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Sketchfab',
selector: 'a[href*="sketchfab.com/3d-models/"], a[href*="sketchfab.com/models/"]',
createEmbedElement(link) {
try {
const url = new URL(link.href);
const modelMatch = url.href.match(/([a-f0-9]{32})/i);
if (modelMatch && modelMatch[1]) {
const modelId = modelMatch[1];
const iframeUrl = `https://sketchfab.com/models/${modelId}/embed`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
title="Modèle 3D Sketchfab"
class="iframe-embed iframe-youtube"
src="${iframeUrl}"
frameborder="0"
allow="autoplay; fullscreen; xr-spatial-tracking"
xr-spatial-tracking
execution-while-out-of-viewport
execution-while-not-rendered
web-share
allowfullscreen>
</iframe>`;
return container;
}
return null;
} catch (e) {
console.error('[Sketchfab] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Steam',
selector: 'a[href*="store.steampowered.com/app/"]',
createEmbedElement(link) {
try {
const match = link.href.match(/\/app\/(\d+)/);
if (!match || !match[1]) return null;
const appId = match[1];
const iframeUrl = `https://store.steampowered.com/widget/${appId}/`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed"
style="height: 190px; border-radius: 8px;"
title="Widget Steam"
frameborder="0">
</iframe>`;
return container;
} catch (e) {
console.error('[Steam] Erreur lors de la création de l\'embed :', e);
return null;
}
}
},
{
name: 'Bandcamp',
selector: 'a[href*=".bandcamp.com/"]',
async createEmbedElement(link) {
if (!link.pathname.includes('/track/') && !link.pathname.includes('/album/')) {
return null;
}
try {
const pageHtml = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: link.href,
onload: response => resolve(response.responseText),
onerror: error => reject(error),
ontimeout: () => reject(new Error('Timeout'))
});
});
const doc = new DOMParser().parseFromString(pageHtml, "text/html");
const embedUrlMeta = doc.querySelector('meta[property="og:video"]');
if (!embedUrlMeta) {
console.warn('[Bandcamp] Meta tag "og:video" introuvable pour :', link.href);
return null;
}
let iframeUrl = embedUrlMeta.getAttribute('content');
if (!iframeUrl) return null;
if (!iframeUrl.endsWith('/')) iframeUrl += '/';
iframeUrl += 'transparent=true/';
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
style="border: 0; width: 100%; max-width: 550px; height: 120px;"
src="${iframeUrl}"
title="Lecteur Bandcamp"
seamless>
</iframe>`;
return container;
} catch (error) {
console.error('[Bandcamp] Erreur lors de la création de l\'embed :', error);
return null;
}
}
},
{
name: 'Flourish',
selector: 'a[href*="public.flourish.studio/visualisation/"], a[href*="public.flourish.studio/story/"]',
async createEmbedElement(link) {
const match = link.href.match(/(visualisation|story)\/\d+/);
if (!match || !match[0]) {
console.warn('[Flourish] Impossible d\'extraire l\'ID de la visualisation:', link.href);
return null;
}
const embedPath = match[0];
const iframeUrl = `https://flo.uri.sh/${embedPath}/embed`;
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${iframeUrl}"
class="iframe-embed iframe-flourish"
title="Flourish Visualisation"
sandbox="allow-scripts allow-same-origin"
scrolling="no"
></iframe>`;
return container;
}
},
{
name: 'DistroKid',
selector: 'a[href*="distrokid.com/hyperfollow/"]',
async createEmbedElement(link) {
const defaultAlbumArt = 'https://risibank.fr/cache/medias/0/5/532/53280/full.png';
const hyperfollowUrl = link.href.split('?')[0];
try {
const responseText = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: hyperfollowUrl,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
},
onload: (response) => resolve(response.responseText),
onerror: (error) => reject(error),
ontimeout: () => reject(new Error('Timeout'))
});
});
let audioUrl = null;
const audioTagMatch = responseText.match(/<audio[^>]+src="([^"]+)"/);
if (audioTagMatch && audioTagMatch[1]) {
audioUrl = audioTagMatch[1];
} else {
const jsonDataMatch = responseText.match(/previewData\.tracks\s*=\s*JSON\.parse\(\s*"(.+?)"\s*\);/);
if (jsonDataMatch && jsonDataMatch[1]) {
const jsonString = new Function(`return "${jsonDataMatch[1]}"`)();
const tracks = JSON.parse(jsonString);
if (tracks && tracks.length > 0) audioUrl = tracks[0].preview;
}
}
if (!audioUrl) {
console.warn('[DistroKid] Impossible de trouver un lien audio pour :', hyperfollowUrl);
return null;
}
const titleMatch = responseText.match(/<title[^>]*>([^<]+) by ([^<]+) - DistroKid<\/title>/);
const trackTitle = titleMatch ? titleMatch[1] : 'Titre inconnu';
const artistName = titleMatch ? titleMatch[2] : 'Artiste inconnu';
let albumArtUrl = defaultAlbumArt;
const bodyArtMatch = responseText.match(/<img[^>]+class="artCover[^"]+"[^>]+src="([^"]+)"/);
if (bodyArtMatch && bodyArtMatch[1]) {
albumArtUrl = bodyArtMatch[1];
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `
<a href="${hyperfollowUrl}" target="_blank" rel="noopener noreferrer" class="distrokid-embed-card">
<div class="distrokid-album-art" style="background-image: url('${albumArtUrl}');"></div>
<div class="distrokid-content">
<div>
<div class="distrokid-title">${trackTitle}</div>
<div class="distrokid-artist">${artistName}</div>
</div>
<audio src="${audioUrl}" controls preload="metadata"></audio>
</div>
</a>
`;
return container;
} catch (error) {
console.error('[DistroKid] Erreur lors de la création de l\'embed :', error);
return null;
}
}
},
{
name: 'PDF',
selector: 'a[href$=".pdf" i]',
async createEmbedElement(link) {
const pdfUrl = link.href;
try {
const headers = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: pdfUrl,
onload: (response) => resolve(response.responseHeaders),
onerror: (error) => reject(error),
ontimeout: (error) => reject(error)
});
});
const lowerCaseHeaders = headers.toLowerCase();
const xFrameOptions = lowerCaseHeaders.match(/x-frame-options:\s*(deny|sameorigin)/);
const csp = lowerCaseHeaders.match(/content-security-policy:.*frame-ancestors\s+('none'|'self')/);
if (xFrameOptions || csp) {
console.log(`[PDF Embed] Intégration bloquée pour ${pdfUrl} par les en-têtes du serveur.`);
return null;
}
} catch (error) {
console.error(`[PDF Embed] Erreur réseau en vérifiant les en-têtes pour ${pdfUrl}:`, error);
return null;
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `<iframe
src="${pdfUrl}"
class="iframe-embed iframe-pdf"
title="Lecteur PDF"
frameborder="0"
sandbox="allow-scripts allow-same-origin">
</iframe>`;
return container;
}
},
{
name: 'GenericMedia',
selector: `
a[href*=".jpg" i]:not([href*="noelshack.com"]), a[href*=".jpeg" i]:not([href*="noelshack.com"]),
a[href*=".png" i]:not([href*="noelshack.com"]), a[href*=".gif" i]:not([href*="noelshack.com"]),
a[href*=".webp" i]:not([href*="noelshack.com"]), a[href*=".bmp" i]:not([href*="noelshack.com"]),
a[href*=".mp4" i]:not([href*="noelshack.com"]), a[href*=".webm" i]:not([href*="noelshack.com"]),
a[href*=".mov" i]:not([href*="noelshack.com"]), a[href*=".ogg" i]:not([href*="noelshack.com"])
`,
async createEmbedElement(link) {
console.log("GenericMedia creating");
const stickerUrl = 'https://risibank.fr/cache/medias/0/5/512/51206/thumb.png';
const handleMediaResize = (mediaElement) => {
const isVideo = mediaElement.tagName === 'VIDEO';
const width = isVideo ? mediaElement.videoWidth : mediaElement.naturalWidth;
const height = isVideo ? mediaElement.videoHeight : mediaElement.naturalHeight;
if (width > 0 && height > width) {
mediaElement.classList.add('iframe-vertical-content');
}
};
const createDeadLinkSticker = () => {
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `
<div class="dead-link-sticker">
<img src="${stickerUrl}" alt="[Média supprimé]">
<span>[Média supprimé]</span>
</div>
`;
return container;
};
try {
await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'HEAD',
url: link.href,
timeout: 4000,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve();
} else {
reject(new Error(`Statut HTTP: ${response.status}`));
}
},
onerror: (error) => reject(new Error('Erreur réseau')),
ontimeout: () => reject(new Error('Timeout dépassé'))
});
});
const container = document.createElement('div');
container.className = 'bloc-embed';
const handleError = (element) => {
const parentBloc = element.closest('.bloc-embed');
if (parentBloc) {
parentBloc.replaceWith(createDeadLinkSticker());
}
};
const pathname = new URL(link.href).pathname;
if (/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(pathname)) {
const img = document.createElement('img');
img.src = link.href;
img.className = 'image-embed';
img.alt = 'Image intégrée';
img.loading = 'lazy';
img.addEventListener('load', () => handleMediaResize(img), { once: true });
img.onerror = () => handleError(img);
container.appendChild(img);
} else if (/\.(mp4|webm|mov|ogg)$/i.test(pathname)) {
const video = document.createElement('video');
video.src = link.href;
video.className = 'video-embed';
video.controls = true;
video.autoplay = true;
video.muted = true;
video.loop = true;
video.playsinline = true;
video.addEventListener('loadedmetadata', () => handleMediaResize(video), { once: true });
video.onerror = () => handleError(video);
container.appendChild(video);
}
if (container.hasChildNodes()) {
return container;
}
return null;
} catch (error) {
console.warn(`[GenericMedia] Lien mort détecté pour ${link.href}. Raison: ${error.message}`);
return createDeadLinkSticker();
}
}
},
{
name: 'ArticlePreview',
selector: 'a[href^="http"]:not([data-miniatweet-processed])',
async createEmbedElement(link) {
const href = link.href;
const excludedDomains = [
'youtube.com', 'youtu.be', 'twitter.com', 'x.com', 'instagram.com',
'tiktok.com', 'vm.tiktok.com', 'streamable.com', 'webmshare.com',
'facebook.com', 'twitch.tv', 'vocaroo.com', 'voca.ro', 'reddit.com',
'flourish.studio', 'jeuxvideo.com', 'jvarchive.com', 'jvarchive.st',
'noelshack.com', 'spotify.com'
];
if (excludedDomains.some(domain => new URL(href).hostname.includes(domain))) {
return null;
}
if (/\.(jpg|jpeg|png|gif|webp|bmp|mp4|webm|mov|ogg)$/i.test(href)) {
return null;
}
let htmlContent;
try {
htmlContent = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: href,
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' },
onload: response => {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText);
} else {
reject(new Error(`Statut HTTP: ${response.status}`));
}
},
onerror: err => reject(err),
ontimeout: () => reject(new Error('Timeout'))
});
});
} catch (error) {
console.warn(`[ArticlePreview] Impossible de récupérer ${href}: ${error.message}`);
return null;
}
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const getMeta = (prop) => doc.querySelector(`meta[property="${prop}"], meta[name="${prop}"]`)?.getAttribute('content')?.trim();
const title = getMeta('og:title') || doc.querySelector('title')?.textContent.trim();
const description = getMeta('og:description');
const imageUrl = getMeta('og:image');
const siteName = getMeta('og:site_name');
if (!title || !imageUrl) {
return null;
}
const container = document.createElement('div');
container.className = 'bloc-embed';
container.innerHTML = `
<a href="${href}" class="article-preview-card" target="_blank" rel="noopener noreferrer" data-miniatweet-processed="true">
<div class="article-preview-image" style="background-image: url('${imageUrl}');"></div>
<div class="article-preview-content">
${siteName ? `<div class="article-preview-sitename">${siteName}</div>` : ''}
<div class="article-preview-title">${title}</div>
${description ? `<div class="article-preview-description">${description}</div>` : ''}
</div>
</a>
`;
return container;
}
},
];
const embedManager = new EmbedManager(providers);
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', () => embedManager.init());
} else {
embedManager.init();
}
})();