2025-07-12 08:46:54 +00:00
// ==UserScript==
2025-07-12 21:45:55 +00:00
// @name UseNet Enhanced (Overseerr/Jellyseerr TV Fix + Nouvel Onglet)
// @version 8.4.3
2025-07-12 08:46:54 +00:00
// @date 12.07.25
2025-07-12 21:45:55 +00:00
// @description Galerie d'affiches, Radarr/Sonarr/Overseerr/Jellyseerr, badges TMDB/IMDB, options menu stables
// @author Aerya
// @match *://*/*
2025-07-12 08:46:54 +00:00
// @grant none
// ==/UserScript==
( function ( ) {
'use strict' ;
2025-07-12 21:45:55 +00:00
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 1. AUTO ‑ UPDATE DISCRET MULTI - URL
* === === === === === === === === === === === === === === === === === === === === === === * /
const LOCAL _VERSION = '8.3.9' ; // Mets ta version ici (à garder synchronisée)
const UPDATE _INTERVAL _MS = 12 * 60 * 60 * 1000 ;
const UPDATE _LS _KEY = 'afficheLastUpdateCheck' ;
2025-07-12 08:46:54 +00:00
2025-07-12 21:45:55 +00:00
const UPDATE _URLS = [
` ${ location . protocol } //tig. ${ location . hostname } /Aerya/Mode-Affiches/raw/branch/main/mode_affiches.js ` ,
'https://raw.githubusercontent.com/Aerya/Mode-Affiches/main/mode_affiches.js'
2025-07-12 08:46:54 +00:00
] ;
2025-07-12 21:45:55 +00:00
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 ;
} ) ;
window . open ( valid [ 0 ] . url , '_blank' ) ;
} ) ;
} ) ( ) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 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 ) ;
}
2025-07-12 08:46:54 +00:00
2025-07-12 21:45:55 +00:00
// === 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 : [ ] ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
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' ; }
// === Icons
function svg4kBadge ( svg ) {
return ` <span style="position:relative;display:inline-block;width:36px;height:36px;"> ${ svg }
< span style = "position:absolute;bottom:-7px;right:-11px;background:rgba(255,255,255,0.88);color:#1a1a1a;font-weight:900;font-family:sans-serif;font-size:15px;padding:1px 8px 1px 8px;border-radius:16px;border:2px solid #fff;box-shadow:0 1px 6px #0006;line-height:15px;text-shadow:0 1px 2px #fff7;letter-spacing:-1px;" > 4 K < / s p a n >
< / s p a n > ` ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
function imgIcon ( url , alt ) {
return ` <span style="background:rgba(255,255,255,0.78);border-radius:10px;padding:2px 2px 2px 2px;display:inline-block;">
< img src = "${url}" alt = "${alt}" style = "width:36px;height:36px;vertical-align:middle;filter: drop-shadow(0 0 5px #0002);border-radius:8px;background:transparent;border:none;display:block;" >
< / s p a n > ` ;
}
function getIcon ( type ) {
if ( type === 'radarr' ) return imgIcon ( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg" , "Radarr" ) ;
if ( type === 'radarr4k' ) return svg4kBadge ( imgIcon ( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr-4k.svg" , "Radarr 4K" ) ) ;
if ( type === 'sonarr' ) return imgIcon ( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg" , "Sonarr" ) ;
if ( type === 'sonarr4k' ) return svg4kBadge ( imgIcon ( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr-4k.svg" , "Sonarr 4K" ) ) ;
if ( type === 'overseerr' ) return imgIcon ( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg" , "Overseerr" ) ;
if ( type === 'jellyseerr' ) return imgIcon ( "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyseerr.svg" , "Jellyseerr" ) ;
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 '' ;
}
// === API Info
const API _INFO = {
radarr : { name : "Radarr" , api : "radarr" , add : "movie" , qual : "qualityProfile" , root : "rootfolder" , icon : "radarr" , tmdb : true } ,
radarr4k : { name : "Radarr 4K" , api : "radarr4k" , add : "movie" , qual : "qualityProfile" , root : "rootfolder" , icon : "radarr4k" , tmdb : true } ,
sonarr : { name : "Sonarr" , api : "sonarr" , add : "series" , qual : "qualityProfile" , root : "rootfolder" , icon : "sonarr" , tmdb : false } ,
sonarr4k : { name : "Sonarr 4K" , api : "sonarr4k" , add : "series" , qual : "qualityProfile" , root : "rootfolder" , icon : "sonarr4k" , tmdb : false } ,
overseerr : { name : "Overseerr" , api : "overseerr" , add : "both" , icon : "overseerr" } ,
jellyseerr : { name : "Jellyseerr" , api : "jellyseerr" , add : "both" , icon : "jellyseerr" }
} ;
// === 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 } ` )
2025-07-12 08:46:54 +00:00
. then ( r => r . json ( ) )
2025-07-12 21:45:55 +00:00
. then ( d => { cache [ key ] = { data : d , fetched : now } ; localStorage . setItem ( TMDB _CACHE _KEY , JSON . stringify ( cache ) ) ; return d ; } )
. catch ( ( ) => null ) ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
// === Détection section
const CATEGORIES = [
{ key : 'HOME' , label : 'Accueil' } ,
{ key : 'MOVIE' , label : 'Films' } ,
{ key : 'TV' , label : 'Séries' }
] ;
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
2025-07-12 08:46:54 +00:00
function extractDateAndSize ( card ) {
let date = '' , size = '' ;
2025-07-12 21:45:55 +00:00
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 ] ;
2025-07-12 08:46:54 +00:00
} ) ;
return { date , size } ;
}
2025-07-12 21:45:55 +00:00
// === 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 : 2 px solid # ffe388 ; border - radius : 12 px ; box - shadow : 0 3 px 16 px # 222 a ;
padding : 18 px 28 px ; min - width : 260 px ; ` ;
pop . innerHTML = ` <b>Profil Qualité :</b><br><br> ` ;
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 ) ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
// === Affichage
2025-07-12 08:46:54 +00:00
function transformAffiches ( ) {
2025-07-12 21:45:55 +00:00
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' ) ) ;
2025-07-12 08:46:54 +00:00
const containers = document . querySelectorAll ( '.containert.article' ) ;
if ( ! cards . length || ! containers . length ) return ;
2025-07-12 21:45:55 +00:00
const groups = new Map ( ) ;
2025-07-12 08:46:54 +00:00
cards . forEach ( card => {
2025-07-12 21:45:55 +00:00
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 ) ) ) { id = m [ 1 ] ; type = 'tv' ; }
else {
2025-07-12 08:46:54 +00:00
const inp = card . querySelector ( 'input#tmdb_id' ) ;
2025-07-12 21:45:55 +00:00
if ( inp ) id = inp . value ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
if ( ! id ) return ;
const key = ` ${ type } _ ${ id } ` ;
if ( ! groups . has ( key ) ) groups . set ( key , { type , id , cards : [ ] } ) ;
groups . get ( key ) . cards . push ( card ) ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
const inst = getInstances ( ) ;
const hasRadarr = inst . radarr && inst . radarr . url && inst . radarr . apiKey ;
const hasRadarr4k = inst . radarr4k && inst . radarr4k . url && inst . radarr4k . apiKey ;
const hasSonarr = inst . sonarr && inst . sonarr . url && inst . sonarr . apiKey ;
const hasSonarr4k = inst . sonarr4k && inst . sonarr4k . url && inst . sonarr4k . apiKey ;
const hasOverseerr = inst . overseerr && inst . overseerr . url && inst . overseerr . apiKey ;
const hasJellyseerr = inst . jellyseerr && inst . jellyseerr . url && inst . jellyseerr . apiKey ;
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;'
} ) ;
2025-07-12 09:56:09 +00:00
2025-07-12 21:45:55 +00:00
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; ` } ) ;
// === Boutons
let btnIdx = 0 ;
const btnGen = ( kind , instObj , apiData ) => {
const btn = document . createElement ( 'button' ) ;
btn . innerHTML = getIcon ( kind ) ;
btn . title = ` Ajouter à ${ apiData . name } ` ;
btn . className = "affiche-usenet-btn" ;
btn . style = ` position:absolute; bottom:12px; right: ${ 12 + btnIdx * ( 36 + 6 ) } px; z-index:22; width:36px; height:36px; background:rgba(255,255,255,0.78); border:2.5px solid #e6e6e6; border-radius:11px; padding:0; display:flex; align-items:center; justify-content:center; box-shadow:0 2px 10px #0002; cursor:pointer; transition:transform 0.11s; opacity:0.98; ` ;
btn . onmouseenter = ( ) => btn . style . opacity = "1" ;
btn . onmouseleave = ( ) => btn . style . opacity = "0.98" ;
btn . onclick = ( e ) => {
e . stopPropagation ( ) ;
btn . disabled = true ;
// Radarr/Sonarr
if ( kind . startsWith ( "radarr" ) || kind . startsWith ( "sonarr" ) ) {
toast ( "Chargement profils/dossiers..." , "info" , 1500 ) ;
Promise . all ( [
fetch ( ` ${ instObj . url } /api/v3/ ${ apiData . qual } ` , { headers : { 'X-Api-Key' : instObj . apiKey } } ) . then ( r => r . json ( ) ) ,
fetch ( ` ${ instObj . url } /api/v3/ ${ apiData . root } ` , { headers : { 'X-Api-Key' : instObj . apiKey } } ) . then ( r => r . json ( ) )
] ) . then ( ( [ profs , roots ] ) => {
if ( ! Array . isArray ( profs ) || ! profs . length ) throw new Error ( 'Aucun profil qualité' ) ;
if ( ! Array . isArray ( roots ) || ! roots . length ) throw new Error ( 'Aucun dossier cible trouvé' ) ;
showProfileSelector ( profs , function ( qpid ) {
const rootFolder = roots . find ( r => r . path && ! r . unmappedFolders ? . length ) || roots [ 0 ] ;
if ( ! rootFolder || ! rootFolder . path ) return toast ( "Aucun dossier cible valide trouvé" , "error" ) ;
toast ( "Ajout en cours..." , "info" , 1200 ) ;
let body = { } ;
if ( apiData . add === 'movie' ) {
fetchTmdb ( 'movie' , group . id ) . then ( data => {
const tmdbId = group . id ;
const year = data && data . release _date ? ( data . release _date + '' ) . substring ( 0 , 4 ) : '' ;
body = {
"title" : data ? . title || '' ,
"qualityProfileId" : qpid ,
"tmdbId" : parseInt ( tmdbId ) ,
"year" : year ? parseInt ( year ) : undefined ,
"monitored" : true ,
"rootFolderPath" : rootFolder . path ,
"addOptions" : { "searchForMovie" : true }
} ;
fetch ( ` ${ instObj . url } /api/v3/movie ` , {
method : 'POST' ,
headers : { 'X-Api-Key' : instObj . apiKey , 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( body )
} ) . then ( async resp => {
btn . disabled = false ;
if ( resp . status === 201 ) {
toast ( "✅ Ajouté à " + apiData . name + " !" , "success" ) ;
} else if ( resp . status === 400 ) {
let r = await resp . json ( ) ;
if ( r && r [ 0 ] && r [ 0 ] . errorMessage && r [ 0 ] . errorMessage . match ( /already exists/i ) )
toast ( "Déjà présent dans " + apiData . name , "info" ) ;
else
toast ( "Erreur " + apiData . name + " : " + ( r [ 0 ] ? . errorMessage || resp . statusText ) , "error" ) ;
} else {
toast ( "Erreur " + apiData . name + " (" + resp . status + ")" , "error" ) ;
}
} ) . catch ( e => {
btn . disabled = false ;
toast ( "Erreur de connexion " + apiData . name , "error" ) ;
} ) ;
} ) ;
} else {
fetchTmdb ( 'tv' , group . id ) . then ( data => {
const tvdbId = data ? . external _ids ? . tvdb _id || data ? . tvdb _id ;
body = {
"title" : data ? . name || '' ,
"qualityProfileId" : qpid ,
"tvdbId" : parseInt ( tvdbId ) ,
"monitored" : true ,
"rootFolderPath" : rootFolder . path ,
"addOptions" : { "searchForMissingEpisodes" : true }
} ;
fetch ( ` ${ instObj . url } /api/v3/series ` , {
method : 'POST' ,
headers : { 'X-Api-Key' : instObj . apiKey , 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( body )
} ) . then ( async resp => {
btn . disabled = false ;
if ( resp . status === 201 ) {
toast ( "✅ Ajouté à " + apiData . name + " !" , "success" ) ;
} else if ( resp . status === 400 ) {
let r = await resp . json ( ) ;
if ( r && r [ 0 ] && r [ 0 ] . errorMessage && r [ 0 ] . errorMessage . match ( /already exists/i ) )
toast ( "Déjà présent dans " + apiData . name , "info" ) ;
else
toast ( "Erreur " + apiData . name + " : " + ( r [ 0 ] ? . errorMessage || resp . statusText ) , "error" ) ;
} else {
toast ( "Erreur " + apiData . name + " (" + resp . status + ")" , "error" ) ;
}
} ) . catch ( e => {
btn . disabled = false ;
toast ( "Erreur de connexion " + apiData . name , "error" ) ;
} ) ;
} ) ;
}
} ) ;
} ) . catch ( e => {
btn . disabled = false ;
toast ( "Erreur de récupération profils/dossiers" , "error" ) ;
} ) ;
2025-07-12 09:56:09 +00:00
}
2025-07-12 21:45:55 +00:00
// Overseerr / Jellyseerr
if ( kind === "overseerr" || kind === "jellyseerr" ) {
if ( group . type === "movie" ) {
fetch ( ` ${ instObj . url } /api/v1/request ` , {
method : 'POST' ,
headers : {
'X-Api-Key' : instObj . apiKey ,
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( { mediaType : "movie" , mediaId : parseInt ( group . id ) } )
} ) . then ( async resp => {
btn . disabled = false ;
if ( resp . status === 201 || resp . status === 202 ) {
toast ( "✅ Ajouté à " + apiData . name + " !" , "success" ) ;
} else {
let txt = await resp . text ( ) ;
toast ( "Erreur " + apiData . name + " : " + txt , "error" ) ;
}
} ) . catch ( ( ) => {
btn . disabled = false ;
toast ( "Erreur de connexion " + apiData . name , "error" ) ;
} ) ;
}
if ( group . type === "tv" ) {
fetchTmdb ( 'tv' , group . id ) . then ( data => {
let seasons = [ ] ;
if ( data && data . seasons ) {
seasons = data . seasons
. filter ( s => s . season _number && s . season _number > 0 )
. map ( s => s . season _number ) ;
}
if ( ! seasons . length ) {
toast ( "Erreur : aucune saison trouvée pour cette série." , "error" ) ;
btn . disabled = false ;
return ;
}
fetch ( ` ${ instObj . url } /api/v1/request ` , {
method : 'POST' ,
headers : {
'X-Api-Key' : instObj . apiKey ,
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( {
mediaType : "tv" ,
mediaId : parseInt ( group . id ) ,
seasons : seasons
} )
} ) . then ( async resp => {
btn . disabled = false ;
if ( resp . status === 201 || resp . status === 202 ) {
toast ( "✅ Ajouté à " + apiData . name + " !" , "success" ) ;
} else {
let txt = await resp . text ( ) ;
toast ( "Erreur " + apiData . name + " : " + txt , "error" ) ;
}
} ) . catch ( ( ) => {
btn . disabled = false ;
toast ( "Erreur de connexion " + apiData . name , "error" ) ;
} ) ;
} ) ;
}
2025-07-12 09:56:09 +00:00
}
2025-07-12 21:45:55 +00:00
} ;
cardDiv . appendChild ( btn ) ;
btnIdx ++ ;
} ;
if ( group . type === 'movie' ) {
if ( hasRadarr ) btnGen ( 'radarr' , inst . radarr , API _INFO . radarr ) ;
if ( hasRadarr4k ) btnGen ( 'radarr4k' , inst . radarr4k , API _INFO . radarr4k ) ;
if ( hasOverseerr ) btnGen ( 'overseerr' , inst . overseerr , API _INFO . overseerr ) ;
if ( hasJellyseerr ) btnGen ( 'jellyseerr' , inst . jellyseerr , API _INFO . jellyseerr ) ;
}
if ( group . type === 'tv' ) {
if ( hasSonarr ) btnGen ( 'sonarr' , inst . sonarr , API _INFO . sonarr ) ;
if ( hasSonarr4k ) btnGen ( 'sonarr4k' , inst . sonarr4k , API _INFO . sonarr4k ) ;
if ( hasOverseerr ) btnGen ( 'overseerr' , inst . overseerr , API _INFO . overseerr ) ;
if ( hasJellyseerr ) btnGen ( 'jellyseerr' , inst . jellyseerr , API _INFO . jellyseerr ) ;
}
2025-07-12 08:46:54 +00:00
2025-07-12 21:45:55 +00:00
// === 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 : `
< span style = "background:rgba(255,255,255,0.78);border-radius:9px;display:flex;align-items:center;padding:2px 10px 2px 6px;box-shadow:0 2px 8px #0002;" >
$ { getIcon ( type ) }
< span style = "font-size:19px;font-weight:bold;color:${color};text-shadow:0 1px 2px #fff9,0 1px 2px #2221;margin-left:2px;" > $ { score } < / s p a n >
< span style = "font-size:13px;font-weight:bold;color:${color};margin-left:5px;" > $ { votes } < / s p a n >
< / s p a n > ` ,
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 ) ;
2025-07-12 09:56:09 +00:00
}
2025-07-12 21:45:55 +00:00
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 ) ;
2025-07-12 09:56:09 +00:00
} ) ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
// === 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 = ` <div style="display:flex;align-items:center;justify-content:flex-start;margin-bottom:18px;width:100%;">
< span style = "font-size:${fontSz()}px;font-weight:600;color:#45e3ee;margin-right:24px;display:flex;align-items:center;" >
< span style = "font-size:${fontSz() + 2}px;vertical-align:middle;" > & # 8595 ; < / s p a n > & n b s p ; & n b s p ; L e s d e r n i è r e s r e l e a s e s & n b s p ; & n b s p ; < s p a n s t y l e = " f o n t - s i z e : $ { f o n t S z ( ) + 2 } p x ; v e r t i c a l - a l i g n : m i d d l e ; " > & # 8 5 9 5 ; < / s p a n >
< / s p a n >
< a href = "${card0.querySelector('a[href*=" ? d = fiche "]')?.href || '#'}" style = "color:#ffd04e;font-size:${fontSz() - 1}px;font-weight:600;text-decoration:none;" $ { openReleasesInNewTab ( ) ? ' target="_blank"' : '' } > Voir toutes les releases pour $ { typeGen } $ { typeLabel } < / a >
< / d i v > ` ;
// === Details releases
let html = '' ;
group . cards . forEach ( sub => {
const title = sub . querySelector ( '.card-header' ) ? . textContent . trim ( ) || '' ;
const { date , size } = extractDateAndSize ( sub ) ;
let bodyHTML = sub . querySelector ( '.card-body' ) ? . innerHTML || '' ;
let nfoHTML = '' ;
if ( bodyHTML ) {
const tmp = document . createElement ( 'div' ) ;
tmp . innerHTML = bodyHTML ;
const spans = tmp . querySelectorAll ( 'span.mx-1' ) ;
for ( const s of spans ) {
const a = s . querySelector ( 'a[data-target="#NFO"]' ) ;
if ( a ) { nfoHTML = s . outerHTML ; s . remove ( ) ; break ; }
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
Array . from ( tmp . querySelectorAll ( 'span.mx-1' ) ) . slice ( 4 ) . forEach ( s => s . remove ( ) ) ;
bodyHTML = Array . from ( tmp . childNodes ) . map ( x => x . outerHTML || '' ) . join ( '' ) ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
html += ` <div style="margin-bottom:12px;display:flex;align-items:center;gap:12px;">
< span style = "flex:3 1 70%;font-size:${fontSz()}px;font-weight:400;color:#cde5fc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title = "${title}" > $ { title } < / s p a n >
< span style = "margin-left:12px;font-size:${fontSz() - 2}px;font-weight:400;color:#b5dbff;white-space:nowrap;" > $ { size } $ { size && date ? ' • ' : '' } $ { date } < / s p a n >
< div style = "display:inline-flex;gap:10px;margin-left:12px;align-items:center;" > $ { bodyHTML } $ { nfoHTML } < / d i v >
< / d i v > ` ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
const wrap = document . createElement ( 'div' ) ;
wrap . className = 'affiche-releases' ;
wrap . style . maxHeight = group . cards . length > 10 ? '430px' : 'none' ;
wrap . style . overflowY = group . cards . length > 10 ? 'auto' : 'visible' ;
wrap . innerHTML = html ;
tooltip . appendChild ( wrap ) ;
2025-07-12 08:46:54 +00:00
setTimeout ( ( ) => { tooltip . style . minHeight = '' ; tooltip . style . height = '' ; tooltip . style . maxHeight = 'none' ; } , 10 ) ;
2025-07-12 21:45:55 +00:00
let open = false ;
imgClone . addEventListener ( 'click' , e => {
2025-07-12 08:46:54 +00:00
e . stopPropagation ( ) ;
2025-07-12 21:45:55 +00:00
adjustW ( ) ;
document . querySelectorAll ( '.affiche-tooltip' ) . forEach ( d => d . style . display = 'none' ) ;
tooltip . style . display = open ? 'none' : 'block' ;
open = ! open ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
document . addEventListener ( 'click' , e => {
if ( ! cardDiv . contains ( e . target ) ) {
2025-07-12 08:46:54 +00:00
tooltip . style . display = 'none' ;
2025-07-12 21:45:55 +00:00
open = false ;
2025-07-12 08:46:54 +00:00
}
} ) ;
2025-07-12 21:45:55 +00:00
cardDiv . appendChild ( tooltip ) ;
gallery . appendChild ( cardDiv ) ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
containers . forEach ( ( c , i ) => { c . innerHTML = '' ; if ( i === 0 ) c . appendChild ( gallery ) ; } ) ;
2025-07-12 08:46:54 +00:00
}
2025-07-12 21:45:55 +00:00
// === Menu config
function createMenu ( ) {
if ( document . getElementById ( 'affiche-mode-menu-container' ) ) return ;
const box = Object . assign ( document . createElement ( 'div' ) , {
id : 'affiche-mode-menu-container' ,
style : 'position:fixed;top:12px;right:12px;z-index:19999;background:#18181c;border:1px solid #444;border-radius:8px;padding:12px 18px 16px 18px;font-size:14px;box-shadow:0 2px 12px #000d;color:#fff;min-width:350px;'
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
const toggle = Object . assign ( document . createElement ( 'button' ) , {
textContent : '🎬 Mode Affiches Alternatif' ,
style : 'cursor:pointer;font-weight:bold;background:#309d98;color:#fff;border:none;border-radius:5px;padding:6px 12px;margin-bottom:8px;z-index:19999;'
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
toggle . addEventListener ( 'click' , ( ) => { menu . style . display = menu . style . display === 'none' ? 'block' : 'none' ; } ) ;
const menu = Object . assign ( document . createElement ( 'div' ) , { style : 'display:none;margin-top:8px;z-index:20001;' } ) ;
// Instances Usenet
const usenetWrap = document . createElement ( 'div' ) ;
usenetWrap . style = "margin:12px 0 12px 0; padding:10px 7px 7px 7px; background:#212124; border-radius:7px;" ;
usenetWrap . innerHTML = "<b style='color:#ffe388'>Instances :</b>" ;
const types = [ 'radarr' , 'radarr4k' , 'sonarr' , 'sonarr4k' , 'overseerr' , 'jellyseerr' ] ;
const inst = getInstances ( ) ;
types . forEach ( t => {
const d = document . createElement ( 'div' ) ;
d . style = "margin:7px 0 7px 0;display:flex;align-items:center;" ;
d . innerHTML = ` <span style="display:inline-block;width:38px;height:38px;vertical-align:middle;margin-right:8px;"> ${ getIcon ( t ) } </span>
< input type = "text" value = "${inst[t]?.name||''}" placeholder = "Nom" style = "width:80px;margin-right:7px;background:#161618;color:#ffe388;border:1px solid #393;" >
< input type = "text" value = "${inst[t]?.url||''}" placeholder = "URL" style = "width:160px;margin-right:7px;background:#161618;color:#ffe388;border:1px solid #393;" >
< input type = "text" value = "${inst[t]?.apiKey||''}" placeholder = "API Key" style = "width:102px;background:#161618;color:#ffe388;border:1px solid #393;" > ` ;
const [ name , url , key ] = d . querySelectorAll ( 'input' ) ;
name . onchange = ( ) => { inst [ t ] = inst [ t ] || { } ; inst [ t ] . name = name . value ; } ;
url . onchange = ( ) => { inst [ t ] = inst [ t ] || { } ; inst [ t ] . url = url . value ; } ;
key . onchange = ( ) => { inst [ t ] = inst [ t ] || { } ; inst [ t ] . apiKey = key . value ; } ;
usenetWrap . appendChild ( d ) ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
let validBtn = document . createElement ( 'button' ) ;
validBtn . textContent = "Sauvegarder" ;
validBtn . style = "margin-left:18px;margin-top:10px;background:#18c659;color:#222;font-weight:bold;border:none;border-radius:8px;padding:8px 24px;font-size:15px;cursor:pointer;box-shadow:0 2px 5px #1a7c43a0;" ;
validBtn . onclick = ( ) => { setInstances ( inst ) ; toast ( "Configuration instances enregistrée !" , "success" ) ; setTimeout ( ( ) => location . reload ( ) , 500 ) ; } ;
usenetWrap . appendChild ( validBtn ) ;
menu . appendChild ( usenetWrap ) ;
// === Sections à activer
const conf = getSections ( ) ;
CATEGORIES . forEach ( cat => {
const wrapper = document . createElement ( 'div' ) ;
const cb = Object . assign ( document . createElement ( 'input' ) , { type : 'checkbox' , id : ` chk_ ${ cat . key } ` , checked : conf . includes ( cat . key ) } ) ;
cb . addEventListener ( 'change' , ( ) => {
let newConf = getSections ( ) ;
if ( cb . checked && ! newConf . includes ( cat . key ) ) newConf . push ( cat . key ) ;
if ( ! cb . checked && newConf . includes ( cat . key ) ) newConf = newConf . filter ( x => x !== cat . key ) ;
setSections ( newConf ) ;
location . reload ( ) ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
const lab = Object . assign ( document . createElement ( 'label' ) , { textContent : cat . label , htmlFor : cb . id , style : 'margin-left:7px;color:#ffe388;' } ) ;
wrapper . appendChild ( cb ) ; wrapper . appendChild ( lab ) ; menu . appendChild ( wrapper ) ;
2025-07-12 08:46:54 +00:00
} ) ;
2025-07-12 21:45:55 +00:00
// === TMDB
const tmdbRow = document . createElement ( 'div' ) ; tmdbRow . style . marginTop = '14px' ;
const cbTmdb = Object . assign ( document . createElement ( 'input' ) , { type : 'checkbox' , id : 'chk_tmdb' , checked : showTmdb ( ) } ) ;
cbTmdb . addEventListener ( 'change' , ( ) => { localStorage . setItem ( STORAGE _SHOW _TMDB _KEY , cbTmdb . checked ? '1' : '0' ) ; location . reload ( ) ; } ) ;
const labTmdb = Object . assign ( document . createElement ( 'label' ) , { textContent : "Afficher la note TMDB sur l'affiche" , htmlFor : cbTmdb . id , style : 'margin-left:7px;color:#ffe388;' } ) ;
tmdbRow . appendChild ( cbTmdb ) ; tmdbRow . appendChild ( labTmdb ) ; menu . appendChild ( tmdbRow ) ;
// === Taille affiche
const sizeRow = Object . assign ( document . createElement ( 'div' ) , { textContent : 'Taille des affiches :' , style : 'margin-top:14px;margin-bottom:2px;color:#ffe388;' } ) ;
const sliderSize = Object . assign ( document . createElement ( 'input' ) , { type : 'range' , min : '200' , max : '360' , step : '10' , value : minW ( ) , style : 'width:180px;vertical-align:middle;' } ) ;
const spanSize = Object . assign ( document . createElement ( 'span' ) , { textContent : ` ${ minW ( ) } px ` , style : 'margin-left:8px;color:#ffe388;' } ) ;
sliderSize . addEventListener ( 'input' , ( ) => { spanSize . textContent = ` ${ sliderSize . value } px ` ; } ) ;
sliderSize . addEventListener ( 'change' , ( ) => { localStorage . setItem ( STORAGE _MINWIDTH _KEY , sliderSize . value ) ; location . reload ( ) ; } ) ;
menu . appendChild ( sizeRow ) ; menu . appendChild ( sliderSize ) ; menu . appendChild ( spanSize ) ;
// === Police overlay
const fontRow = Object . assign ( document . createElement ( 'div' ) , { textContent : 'Taille du texte (overlay) :' , style : 'margin-top:16px;margin-bottom:2px;color:#ffe388;' } ) ;
const sliderFont = Object . assign ( document . createElement ( 'input' ) , { type : 'range' , min : '14' , max : '28' , step : '2' , value : fontSz ( ) , style : 'width:140px;vertical-align:middle;' } ) ;
const spanFont = Object . assign ( document . createElement ( 'span' ) , { textContent : ` ${ fontSz ( ) } px ` , style : 'margin-left:8px;color:#ffe388;' } ) ;
sliderFont . addEventListener ( 'input' , ( ) => { spanFont . textContent = ` ${ sliderFont . value } px ` ; document . querySelectorAll ( '.affiche-tooltip' ) . forEach ( d => d . style . fontSize = sliderFont . value + 'px' ) ; } ) ;
sliderFont . addEventListener ( 'change' , ( ) => { localStorage . setItem ( STORAGE _FONT _SIZE _KEY , sliderFont . value ) ; location . reload ( ) ; } ) ;
menu . appendChild ( fontRow ) ; menu . appendChild ( sliderFont ) ; menu . appendChild ( spanFont ) ;
// === Option nouvel onglet releases
const newTabRow = document . createElement ( 'div' ) ; newTabRow . style . marginTop = '14px' ;
const cbNewTab = Object . assign ( document . createElement ( 'input' ) , { type : 'checkbox' , id : 'chk_newtab' , checked : openReleasesInNewTab ( ) } ) ;
cbNewTab . addEventListener ( 'change' , ( ) => { localStorage . setItem ( STORAGE _RELEASE _NEWTAB _KEY , cbNewTab . checked ? '1' : '0' ) ; location . reload ( ) ; } ) ;
const labNewTab = Object . assign ( document . createElement ( 'label' ) , { textContent : "Ouvrir « Voir toutes les releases » dans un nouvel onglet" , htmlFor : cbNewTab . id , style : 'margin-left:7px;color:#ffe388;' } ) ;
newTabRow . appendChild ( cbNewTab ) ; newTabRow . appendChild ( labNewTab ) ; menu . appendChild ( newTabRow ) ;
box . appendChild ( toggle ) ; box . appendChild ( menu ) ;
document . body . appendChild ( box ) ;
// Haut de page
2025-07-12 08:46:54 +00:00
if ( ! document . getElementById ( 'remonter-haut-btn' ) ) {
2025-07-12 21:45:55 +00:00
const up = Object . assign ( document . createElement ( 'button' ) , { id : 'remonter-haut-btn' , textContent : '↑ Haut de page' , style : 'position:fixed;bottom:22px;right:26px;background:#309d98;color:#fff;border:none;border-radius:6px;padding:8px 20px;font-weight:bold;font-size:16px;cursor:pointer;box-shadow:0 3px 12px rgba(0,0,0,0.14);z-index:99999;' } ) ;
up . addEventListener ( 'click' , ( ) => scrollTo ( { top : 0 , behavior : 'smooth' } ) ) ;
document . body . appendChild ( up ) ;
2025-07-12 08:46:54 +00:00
}
}
2025-07-12 21:45:55 +00:00
// === Init
function start ( ) { createMenu ( ) ; transformAffiches ( ) ; }
2025-07-12 08:46:54 +00:00
if ( document . readyState !== 'loading' ) start ( ) ;
else document . addEventListener ( 'DOMContentLoaded' , start ) ;
2025-07-12 09:56:09 +00:00
2025-07-12 21:45:55 +00:00
} ) ( ) ;