// ==UserScript==
// @name improved poster
// @version 9.0
// @date 19.07.25
// @description Galerie d'affiches, badges TMDB/IMDB, options menu stables
// @author Aerya + adapation UNFR
// @match *://*/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
/* ==================================================================
* 1. AUTO UPDATE DISCRET MULTI-URL
* ==================================================================*/
function getLocalVersion() {
try {
let scriptText = document.currentScript?.text || '';
if (!scriptText) {
const scripts = document.querySelectorAll('script');
for (let s of scripts) {
if (s.textContent.includes('@name improved poster')) {
scriptText = s.textContent;
break;
}
}
}
const match = scriptText.match(/@version\s+([0-9]+(?:\.[0-9]+)*)/);
return match ? match[1] : null;
} catch {
return null;
}
}
const UPDATE_INTERVAL_MS = 12 * 60 * 60 * 1000;
const UPDATE_LS_KEY = 'afficheLastUpdateCheck';
const LOCAL_VERSION = getLocalVersion() || '0.0.0';
const UPDATE_URLS = [
`${location.protocol}//tig.${location.hostname}/UNFR/improved-poster/raw/branch/main/improved_poster.js`
];
function parseVersion(txt) {
const m = txt.match(/@version\s+([0-9]+(?:\.[0-9]+)*)/);
return m ? m[1] : null;
}
function isNewer(remote, local) {
const R = remote.split('.').map(Number);
const L = local.split('.').map(Number);
for (let i = 0; i < Math.max(R.length, L.length); i++) {
const a = R[i] || 0, b = L[i] || 0;
if (a > b) return true;
if (a < b) return false;
}
return false;
}
(function checkUpdateMulti() {
const last = parseInt(localStorage.getItem(UPDATE_LS_KEY) || '0', 10);
const now = Date.now();
if (now - last < UPDATE_INTERVAL_MS) return;
localStorage.setItem(UPDATE_LS_KEY, now.toString());
Promise.all(
UPDATE_URLS.map(url =>
fetch(`${url}?_=${now}`)
.then(r => r.text())
.then(txt => ({ url, version: parseVersion(txt) }))
.catch(() => null)
)
).then(results => {
const valid = results.filter(x => x && x.version && isNewer(x.version, LOCAL_VERSION));
if (!valid.length) return;
valid.sort((a, b) => {
const aV = a.version.split('.').map(Number);
const bV = b.version.split('.').map(Number);
for (let i = 0; i < Math.max(aV.length, bV.length); i++) {
if ((aV[i] || 0) > (bV[i] || 0)) return -1;
if ((aV[i] || 0) < (bV[i] || 0)) return 1;
}
return 0;
});
// Juste prévenir discrètement sans ouvrir d’onglet
//toast(`Nouvelle version dispo : ${valid[0].version} ! Actualise pour mettre à jour.`, 'success', 8000);
});
})();
/* ==================================================================
* 0. CONDITIONS D’ACTIVATION
* ==================================================================*/
if (!document.getElementById('m0de_afficheS_EnAbled')) return;
// === Toast notif
function toast(msg, type = 'info', ms = 3500) {
let color = type === 'error' ? '#c00' : type === 'success' ? '#15a92e' : '#368efc';
let bg = type === 'error' ? '#ffd5d5' : type === 'success' ? '#dbffe0' : '#e2eafc';
let box = document.createElement('div');
box.textContent = msg;
box.style = `position:fixed;top:26px;right:24px;z-index:99999;padding:14px 22px;font-size:17px;font-weight:bold;border-radius:9px;background:${bg};color:${color};box-shadow:0 4px 24px #0002;opacity:0.98;`;
document.body.appendChild(box);
setTimeout(() => box.remove(), ms);
}
// === Persistance config
const INSTANCES_KEY = 'afficheUsenetInstances';
const STORAGE_SECTIONS_KEY = 'afficheModeSections';
const STORAGE_MINWIDTH_KEY = 'afficheMinWidth';
const STORAGE_FONT_SIZE_KEY = 'afficheRlzFontSize';
const STORAGE_SHOW_TMDB_KEY = 'afficheShowTmdb';
const STORAGE_RELEASE_NEWTAB_KEY = 'afficheReleaseNewTab';
const TMDB_CACHE_KEY = 'afficheTmdbCache';
function getInstances() { try { return JSON.parse(localStorage.getItem(INSTANCES_KEY)) || {}; } catch { return {}; } }
function setInstances(obj) { localStorage.setItem(INSTANCES_KEY, JSON.stringify(obj)); }
function getSections() {
let arr = [];
try { arr = JSON.parse(localStorage.getItem(STORAGE_SECTIONS_KEY)) || []; }
catch { arr = []; }
return Array.isArray(arr) ? arr : [];
}
function setSections(arr) { localStorage.setItem(STORAGE_SECTIONS_KEY, JSON.stringify(arr)); }
function minW() { return parseInt(localStorage.getItem(STORAGE_MINWIDTH_KEY) || '260', 10); }
function fontSz() { return parseInt(localStorage.getItem(STORAGE_FONT_SIZE_KEY) || '22', 10); }
function showTmdb() { return localStorage.getItem(STORAGE_SHOW_TMDB_KEY) !== '0'; }
function openReleasesInNewTab() { return localStorage.getItem(STORAGE_RELEASE_NEWTAB_KEY) === '1'; }
function imgIcon(url, alt) {
return `
`;
}
function getIcon(type) {
if(type==='tmdb') return imgIcon("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tmdb.svg", "TMDB");
if(type==='imdb') return imgIcon("https://www.svgrepo.com/show/349409/imdb.svg", "IMDB");
return '';
}
// === TMDB fetch/cache (similaire)
function fetchTmdb(type, id) {
if (!id) return Promise.resolve(null);
let cache = {};
try { cache = JSON.parse(localStorage.getItem(TMDB_CACHE_KEY)) || {}; } catch { cache = {}; }
const key = `${type}_${id}`;
const now = Date.now();
if (cache[key] && now - cache[key].fetched < 24 * 60 * 60 * 1000) return Promise.resolve(cache[key].data);
return fetch(`${location.origin}/proxy_tmdb?type=${type}&id=${id}`)
.then(r => r.json())
.then(d => { cache[key] = { data: d, fetched: now }; localStorage.setItem(TMDB_CACHE_KEY, JSON.stringify(cache)); return d; })
.catch(() => null);
}
// === Détection section
const CATEGORIES = [
{ key: 'HOME', label: 'Accueil' },
{ key: 'MOVIE', label: 'Films' },
{ key: 'TV', label: 'Séries/Mangas' }
];
function getSection() {
const p = new URLSearchParams(location.search);
const s = p.get('section');
const vm = p.get('vm');
return (!s && (vm === '1' || !vm)) ? 'HOME' : (s || '').toUpperCase();
}
// === Extract date/size
function extractDateAndSize(card) {
let date = '', size = '';
card.querySelectorAll('.badge').forEach(b => {
const t = b.textContent.trim();
if (!size && /([0-9]+(?:\.[0-9]+)?)( ?(GB|MB|G|M|Go|Mo))$/i.test(t)) size = t;
if (!date && /\d{2}\/\d{2}\/\d{2}/.test(t)) date = t.match(/\d{2}\/\d{2}\/\d{2}/)[0];
});
return { date, size };
}
// === Overlay (profil qualité)
function showProfileSelector(profiles, cb) {
document.querySelectorAll('.usenet-qprof-popover').forEach(e=>e.remove());
let pop = document.createElement('div');
pop.className = "usenet-qprof-popover";
pop.style = `position:fixed;z-index:99999;top:70px;right:32px;background:#23233a;color:#ffe388;
border:2px solid #ffe388;border-radius:12px;box-shadow:0 3px 16px #222a;
padding:18px 28px;min-width:260px;`;
pop.innerHTML = `Profil Qualité :
`;
profiles.forEach(prof => {
let btn = document.createElement('button');
btn.textContent = prof.name;
btn.style = `display:block;width:100%;margin:6px 0;padding:10px 0;font-size:17px;border:none;background:#f9d72c;color:#222;font-weight:bold;border-radius:8px;cursor:pointer;`;
btn.onclick = () => { pop.remove(); cb(prof.id); };
pop.appendChild(btn);
});
let cancel = document.createElement('button');
cancel.textContent = "Annuler";
cancel.style = `display:block;width:100%;margin:14px 0 0 0;padding:10px 0;font-size:15px;border:none;background:#ddd;color:#444;border-radius:8px;cursor:pointer;`;
cancel.onclick = ()=>pop.remove();
pop.appendChild(cancel);
document.body.appendChild(pop);
}
// === Affichage
function transformAffiches() {
const section = getSection();
const conf = getSections();
const activate = conf.includes(section);
if (!CATEGORIES.some(c => c.key === section) || !activate) return;
const cards = Array.from(document.querySelectorAll('.containert.article .card.affichet'));
const containers = document.querySelectorAll('.containert.article');
if (!cards.length || !containers.length) return;
const groups = new Map();
cards.forEach(card => {
const href = card.querySelector('a[href*="?d=fiche"]')?.getAttribute('href') || '';
let id = '', type = 'movie';
let m = href.match(/movieid=(\d+)/i);
if (m) { id = m[1]; }
else if ((m = href.match(/tvid=(\d+)/i)) || (m = href.match(/mangaid=(\d+)/i))) { id = m[1]; type = 'tv'; }
else {
const inp = card.querySelector('input#tmdb_id');
if (inp) id = inp.value;
}
if (!id) return;
const key = `${type}_${id}`;
if (!groups.has(key)) groups.set(key, { type, id, cards: [] });
groups.get(key).cards.push(card);
});
const gallery = Object.assign(document.createElement('div'), {
className: 'd-flex flex-wrap',
style: 'justify-content:center;margin-top:20px;gap:8px;padding:0 12px;width:100%;margin-left:auto;margin-right:auto;'
});
groups.forEach(group => {
const card0 = group.cards[0];
const img0 = card0.querySelector('img.card-img-top');
if (!img0) return;
const mH = img0.height || 330;
const cardDiv = Object.assign(document.createElement('div'), { style: `flex:0 0 ${minW()}px;max-width:${minW()}px;position:relative;display:block;` });
// === Badges TMDB/IMDB
if (showTmdb()) {
fetchTmdb(group.type, group.id).then(data => {
if (!data) return;
const badgeWrap = Object.assign(document.createElement('div'), { style: 'position:absolute;top:7px;left:8px;display:flex;flex-direction:column;gap:3px;z-index:15;pointer-events:none;' });
function addBadge(icon, score, votes, url, type='tmdb') {
if (!score || score === '?') return;
const color = "#111";
const el = Object.assign(document.createElement(url ? 'a' : 'span'), {
innerHTML: `
${getIcon(type)}
${score}
${votes}
`,
style: 'margin-bottom:5px;margin-right:3px;pointer-events:auto;text-decoration:none;'
});
if (url) { el.href = url; el.target = '_blank'; el.rel = 'noopener noreferrer'; el.title = 'Voir la fiche'; }
badgeWrap.appendChild(el);
}
addBadge('tmdb',
data.vote_average ? Number(data.vote_average).toFixed(1) : '?',
data.vote_count ? ` (${data.vote_count})` : '',
`https://www.themoviedb.org/${group.type === 'tv' ? 'tv' : 'movie'}/${group.id}`,
'tmdb');
addBadge('imdb',
data.note_imdb ? Number(data.note_imdb).toFixed(1) : '?',
data.vote_imdb ? ` (${data.vote_imdb})` : '',
null,
'imdb');
if (badgeWrap.childElementCount) cardDiv.appendChild(badgeWrap);
});
}
// === Image et overlay
const imgClone = img0.cloneNode(true);
imgClone.style.width = `${minW()}px`;
imgClone.style.cursor = 'pointer';
cardDiv.appendChild(imgClone);
// === Overlay releases
const tooltip = Object.assign(document.createElement('div'), {
className: 'affiche-tooltip',
style: `position:absolute;top:0;left:0;background:rgba(10,10,10,0.98);color:#fff;padding:22px 36px 26px 36px;border-radius:10px;font-size:${fontSz()}px;font-weight:400;width:${Math.min(innerWidth - 40, 1150)}px;max-width:99vw;min-height:${mH}px;z-index:1010;box-shadow:0 0 14px 6px rgba(0,0,0,0.7);white-space:normal;display:none;pointer-events:auto;overflow:hidden;`
});
const adjustW = () => {
tooltip.style.width = Math.min(innerWidth - 40, 1150) + 'px';
const cardRect = cardDiv.getBoundingClientRect();
const overlayW = tooltip.offsetWidth || Math.min(innerWidth - 40, 1150);
let left = cardRect.left;
if (left + overlayW > window.innerWidth - 20) {
tooltip.style.left = '';
tooltip.style.right = '0';
} else {
tooltip.style.left = '0';
tooltip.style.right = '';
}
};
addEventListener('resize', adjustW);
const typeLabel = group.type === 'tv' ? 'série' : 'film';
const typeGen = group.type === 'tv' ? 'la' : 'le';
tooltip.innerHTML = `