YggTorrent Plus

Connexion automatique, boutons de téléchargement dans les résultats de recherche, prévisualisation des images de torrent, et bien d'autres améliorations très cool...

// ==UserScript==
// @name           YggTorrent Plus
// @match          https://www.yggtorrent.top/*
// @version        1.5
// @author         Binouchette
// @license        MIT
// @description    Connexion automatique, boutons de téléchargement dans les résultats de recherche, prévisualisation des images de torrent, et bien d'autres améliorations très cool...
// @run-at         document-end
// @noframes
// @grant          GM.getValue
// @grant          GM.setValue
// @grant          GM.deleteValue
// @grant          GM_addStyle
// @grant          GM_getResourceURL
// @grant          GM_xmlhttpRequest
// @namespace      https://greasyfork.org/fr/scripts/537591-yggtorrent-plus
// @icon           https://www.google.com/s2/favicons?sz=64&domain=yggtorrent.top
// ==/UserScript==

(async function() {
    const CONFIG = {
        CHANGE_FONT: false, // Changer la police du site - Polices : 'Roboto Mono', 'Roboto Serif', 'Source Code Pro', OpenDyslexic
        HIDE_VOSTFR: true, // Afficher / masquer les films en VOSTFR
        FILTER_SIZE: true, // Filtrer par taille
        HIDE_OLD_TORRENTS: true, // Afficher / masquer les torrents vieux de X jours / année(s)
        RESIZE_COLUMNS: true, // Dans la liste des torrents, les tailles des colonnes sont plus harmonieuses
        RESIZE_COLUMNS_TODAY: true, // Dans la liste des torrents sur la page des "Torrents du jour", les tailles des colonnes sont plus harmonieuses
        DOWNLOAD_BUTTONS: true, // Ajoute un bouton pour télécharger un fichier dans la liste des torrents
        DOWNLOAD_BUTTONS_TODAY: true, // Ajoute un bouton pour télécharger un fichier dans la liste des torrents sur la page des "Torrents du jour"
        HOVER_TABLE: true, // Sur la liste des torrents, change la couleur de fond au passage de la souris
        LOGIN_AUTOMATICALLY: true, // Connexion automatique
        PREVIEWS_IMAGES_ON_HOVER: true, // Aperçu de l'image du torrent
        PREVIEWS_IMAGES_SIZE: 400, // Taille des images de prévisualisation (en pixels)
        HIDE_SIDEBAR: false, // Masquer la barre latérale
        LARGER_NFO: true, // Agrandir la fenêtre de prévisualisation du NFO
        SEARCH_BY_LATEST_FIRST: true, // Résultats de recherche par date de publication (du plus récent au plus ancien)
        KEEP_SEARCH_WHEN_CLICKING_ON_SUBCATEGORY_ICON: true, // Conserve les critères de recherche lorsqu'on clique sur une catégorie
        DARK_THEME: false, // Thème sombre
    };

    const SELECTORS = {
        HOVER_TABLE_LIGNE: '.table-responsive',
        REGISTER_BUTTON: '.register',
        TORRENT_NAME_LINK: 'a[id="torrent_name"]',
        RESULTS_TABLE_ROW: '.results table tbody tr',
        INPUT_USERNAME: 'input[name="id"]',
        INPUT_PASSWORD: 'input[name="pass"]',
        INPUT_SUBMIT: 'button[type="submit"]',
        LOGOUT_LINK: 'a[href="https://www.yggtorrent.top/user/logout"]',
        NFO_MODAL: 'nfoModal',
        SEARCH_FORM: 'form.search',
        LOGIN_FORM: '.login-form',
    };

    const CONSTANTS = {
        COOKIES_STORAGE_KEY: 'yggtorrent_credentials',
        MOUSEENTER_DELAY: 100,
        REGEX_URL: /engine\/search|torrents\/exclus/
    };

    /* Changer la Police */

    async function changeFont()
    {
        const selectedFont = 'Source Code Pro'; // Roboto Mono, Roboto Serif, Source Code Pro,

        // Charger les polices Google Fonts
        const fontsLink = document.createElement('link');
        fontsLink.rel = 'stylesheet';
        fontsLink.href = 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap';
        document.head.appendChild(fontsLink);

        GM_addStyle(`
            @font-face {
                font-family: OpenDyslexic;
                src:url(https://opendyslexic.org/assets/fonts/OpenDyslexic3-d2c01a97d426dbf4de2e249d3c69da4d.woff2) format('woff2'),
                url(https://opendyslexic.org/assets/fonts/OpenDyslexic3-387e0788d88667639fca4731ef1e20c4.woff) format('woff');
                font-weight: 400 !important;
                font-style: normal !important;
                font-display: swap !important;
            }

            * {
                font-family: '${selectedFont}', sans-serif !important;
            }
        `);
    }

    CONFIG.CHANGE_FONT && changeFont()

    GM_addStyle(`
        .hidden-vostfr { display: none !important; }
    `);

    async function hideLinesVostfr(masquer) {
        const lignes = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);
        const isVostRow = (ligne) => {
            const nameCell = ligne.querySelector('td:nth-child(2)');
            if (!nameCell) return false;

            // Essaye le lien de nom officiel si présent, sinon tout le texte de la cellule
            const link = nameCell.querySelector(SELECTORS.TORRENT_NAME_LINK);
            const rawText = (link?.textContent || nameCell.textContent || '').trim();

            // Normalise (retire les accents) puis majuscules
            const text = rawText
            .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
            .toUpperCase();

            // Variantes fréquentes
            return /\b(VOSTFR|VOST\-FR|VOST FR|SUBFRENCH|VOSTFRENCH)\b/.test(text);
        };

        lignes.forEach((ligne) => {
            if (isVostRow(ligne)) {
                if (masquer) {
                    ligne.classList.add('hidden-vostfr');
                } else {
                    ligne.classList.remove('hidden-vostfr');
                }
            }
        });
    }

    /*async function hideLinesVostfr(masquer) {
        const lignes = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);
        lignes.forEach((ligne) => {
            const td = ligne.querySelector('td:nth-child(2) a');
            if (td && /SUBFRENCH|VOSTFR/i.test(td.textContent)) {
                ligne.style.display = masquer ? 'none' : '';
            }
        });
    }*/

    async function buttonHideVostfr() {
        let etat = true; // true = masquer
        const btnVostfr = document.createElement('button');
        btnVostfr.innerHTML = 'Afficher les VOSTFR';
        btnVostfr.title = 'Afficher / masquer les torrents VOSTFR';
        btnVostfr.style.background = 'linear-gradient(90deg, rgba(110, 225, 225, 1) 0%, rgba(110, 220, 200, 1) 50%, rgba(100, 220, 175, 1) 100%)';
        btnVostfr.style.border = 'none';
        btnVostfr.style.borderRadius = '.25rem';
        btnVostfr.style.color = 'rgba(0, 0, 0, 1)';
        btnVostfr.style.cursor = 'pointer';
        btnVostfr.style.display = 'flex';
        btnVostfr.style.alignItems = 'center';
        btnVostfr.style.justifyContent = 'center';
        btnVostfr.style.fontSize = '1rem';
        btnVostfr.style.fontWeight = 'bold';
        btnVostfr.style.margin = '0';
        btnVostfr.style.padding = '1rem 2rem';
        btnVostfr.style.position = 'absolute';
        btnVostfr.style.right = '330px';
        btnVostfr.style.top = '175px';
        btnVostfr.style.width = '220px'
        btnVostfr.style.zIndex = '999';

        let observer = null;
        function observeTable() {
            const tbody = document.querySelector('.results table tbody');
            if (!tbody) return;
            if (observer) observer.disconnect();
            observer = new MutationObserver(() => hideLinesVostfr(etat));
            observer.observe(tbody, { childList: true, subtree: true });
        }

        btnVostfr.addEventListener('click', () => {
            etat = !etat;
            hideLinesVostfr(etat);
            btnVostfr.innerHTML = etat ? 'Afficher les VOSTFR' : 'Masquer les VOSTFR';
        });

        document.body.appendChild(btnVostfr);

        observeTable();
        hideLinesVostfr(etat);

        const mainObserver = new MutationObserver(observeTable);
        mainObserver.observe(document.body, { childList: true, subtree: true });
    }

    if (CONSTANTS.REGEX_URL.test(window.location.href)) {
        CONFIG.HIDE_VOSTFR && buttonHideVostfr();
    }

    /* Afficher / masquer les torrents vieux de 7, 14, 30 jours etc. */

    async function buttonTime() {
        // Liste des jours à masquer
        const optionsJours = [0, 7, 14, 30, 60, 120, 180, 365, (365 * 2), (365 * 5), (365 * 8)];
        let seuilActuel = 0;

        function hideOldTorrents(jours) {
            const lignes = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);
            const maintenant = new Date();
            const seuilMs = jours * 24 * 60 * 60 * 1000;

            lignes.forEach((ligne) => {
                const tdAge = ligne.querySelector('td:nth-child(5)');
                if (!tdAge) return;

                const hiddenTimestamp = tdAge.querySelector('div.hidden');
                if (!hiddenTimestamp) return;

                const timestamp = parseInt(hiddenTimestamp.textContent.trim(), 10);
                if (isNaN(timestamp)) return;

                const dateTorrent = new Date(timestamp * 1000);
                const tropVieux = (maintenant - dateTorrent) > seuilMs;

                ligne.style.display = (jours === 0 || !tropVieux) ? '' : 'none';
            });
        }

        const container = document.createElement('div');
            container.id = 'filter-container-date';
            container.style.backgroundColor = 'rgba(250, 250, 250, 1)';
        container.style.backgroundColor = 'rgba(250, 250, 250, 1)';
        container.style.borderRadius = '.25rem';
        container.style.display = 'flex';
        container.style.alignItems = 'center';
        container.style.justifyContent = 'center';
        container.style.flexDirection = 'column';
        container.style.fontSize = '1rem';
        container.style.fontWeight = 'bold';
        container.style.padding = '2rem 0';
        container.style.position = 'absolute';
        container.style.right = '330px';
        container.style.top = '235px';
        container.style.zIndex = '999';
        container.style.width = '220px'

        function formatOptionLabel(jours) {
            if (jours === 0) return 'Tous';

            if (jours % 365 === 0 && jours >= 365) {
                const annees = jours / 365;
                return `${annees} an${annees > 1 ? 's' : ''}`;
            }

            if (jours % 30 === 0 && jours >= 30) {
                const mois = jours / 30;
                return `${mois} mois`;
            }

            return `${jours} jour${jours > 1 ? 's' : ''}`;
        }

        const radioGroup = document.createElement('div');
        radioGroup.style.display = 'flex';
        container.style.alignItems = 'center';
        container.style.justifyContent = 'center';
        radioGroup.style.flexDirection = 'column';
        radioGroup.style.flexWrap = 'wrap';
        radioGroup.style.gap = '.5rem .75rem';

        optionsJours.forEach((jours) => {
            const id = `filter-radio-${jours}`;

            const label = document.createElement('label');
            label.setAttribute('for', id);
            label.style.alignItems = 'center';
            label.style.backgroundColor = 'rgba(255, 255, 255, 1)';
            label.style.border = '1px solid rgba(205, 210, 220, 1)';
            label.style.borderRadius = '.25rem';
            label.style.cursor = 'pointer';
            label.style.display = 'flex';
            label.style.padding = '.5rem .75rem';

            const input = document.createElement('input');
            input.type = 'radio';
            input.name = 'jour';
            input.value = jours;
            input.id = id;
            input.style.marginRight = '1rem';

            if (jours === 0) {
                input.checked = true;
                hideOldTorrents(0); // affiche tout par défaut
            }

            input.addEventListener('change', () => {
                seuilActuel = parseInt(input.value, 10);
                hideOldTorrents(seuilActuel);
            });

            label.appendChild(input);
            label.appendChild(document.createTextNode(formatOptionLabel(jours)));
            radioGroup.appendChild(label);
        });

        container.appendChild(radioGroup);
        document.body.appendChild(container);
    }

    if (CONSTANTS.REGEX_URL.test(window.location.href)) {
        CONFIG.HIDE_OLD_TORRENTS && buttonTime();
    }

    /* Largeurs des lignes */

    async function styleLignesTd() {
        const lignes = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);
        lignes.forEach(ligne => {
            const tds = ligne.querySelectorAll('td');

            if (tds[0]) { Object.assign(tds[0].style, { width: '90px', maxWidth: '90px', textAlign: 'center' }); } // Type
            if (tds[1]) { Object.assign(tds[1].style, { width: '720px', maxWidth: '720px', textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }); } // Nom
            if (tds[2]) { Object.assign(tds[2].style, { width: '120px', maxWidth: '120px', textAlign: 'center' }); } // NFO
            if (tds[3]) { Object.assign(tds[3].style, { width: '50px', maxWidth: '50px', textAlign: 'center' }); } // Commentaire
            if (tds[4]) { Object.assign(tds[4].style, { width: '150px', maxWidth: '150px', textAlign: 'center' }); } // Age
            if (tds[5]) { Object.assign(tds[5].style, { width: '70px', maxWidth: '70px', textAlign: 'center' }); } // Taille
            if (tds[6]) { Object.assign(tds[6].style, { width: '50px', maxWidth: '50px', textAlign: 'center' }); } // Complétés
            if (tds[7]) { Object.assign(tds[7].style, { width: '50px', maxWidth: '50px', textAlign: 'center' }); } // Sources
            if (tds[8]) { Object.assign(tds[8].style, { width: '50px', maxWidth: '50px', textAlign: 'center' }); } // Clients
        });
    }

    CONFIG.RESIZE_COLUMNS && styleLignesTd();

    /* Filtrer par taille */

    async function buttonSize() {
        let minBytes = 0;
        const optionsGo = [0, 1, 2, 5, 10, 20, 50];

        function parseSizeToBytes(txt) {
            if (!txt) { return NaN; }
            const s = txt.trim().replace(/\s+/g, '');
            const m = s.match(/^([\d.,]+)\s*(ko|mo|go|to|kb|mb|gb|tb)$/i);
            if (!m) { return NaN; }
            const value = parseFloat(m[1].replace(',', '.'));
            const unit = m[2].toLowerCase();

            const KB = 1024;
            const MB = 1024 * KB;
            const GB = 1024 * MB;
            const TB = 1024 * GB;

            switch (unit) {
                case 'ko':
                case 'kb': return value * KB;
                case 'mo':
                case 'mb': return value * MB;
                case 'go':
                case 'gb': return value * GB;
                case 'to':
                case 'tb': return value * TB;
                default: return NaN;
            }
        }

        function applySizeFilter(minB) {
            const rows = document.querySelectorAll(SELECTORS?.RESULTS_TABLE_ROW || '.results table tbody tr');
            rows.forEach((tr) => {
                const td = tr.querySelector('td:nth-child(6)');
                if (!td) { return; }
                const sizeTxt = td.textContent;
                const bytes = parseSizeToBytes(sizeTxt);
                if (isNaN(bytes)) { return; }

                tr.style.display = (minB === 0 || bytes >= minB) ? '' : 'none';
            });
        }

        const container = document.createElement('div');
        container.id = 'filter-container-size';
        container.style.backgroundColor = 'rgba(250, 250, 250, 1)';
        container.style.borderRadius = '.25rem';
        container.style.display = 'flex';
        container.style.alignItems = 'center';
        container.style.justifyContent = 'center';
        container.style.flexDirection = 'column';
        container.style.fontSize = '1rem';
        container.style.fontWeight = 'bold';
        container.style.padding = '2rem 0';
        container.style.position = 'absolute';
        container.style.right = '330px';
        container.style.zIndex = '999';
        container.style.width = '220px';

        const title = document.createElement('div');
        title.textContent = 'Taille';
        title.style.marginBottom = '.75rem';
        container.appendChild(title);

        const radioGroup = document.createElement('div');
        radioGroup.style.display = 'flex';
        radioGroup.style.flexDirection = 'column';
        radioGroup.style.gap = '.5rem';
        container.appendChild(radioGroup);

        function labelFor(go) {
            return go === 0 ? 'Tous' : `≥ ${go} Go`;
        }

        optionsGo.forEach((go) => {
            const id = `size-radio-${go}`;
            const label = document.createElement('label');
            label.setAttribute('for', id);
            label.style.alignItems = 'center';
            label.style.backgroundColor = 'rgba(255, 255, 255, 1)';
            label.style.border = '1px solid rgba(205, 210, 220, 1)';
            label.style.borderRadius = '.25rem';
            label.style.cursor = 'pointer';
            label.style.display = 'flex';
            label.style.padding = '.5rem .75rem';

            const input = document.createElement('input');
            input.type = 'radio';
            input.name = 'size-min';
            input.value = go;
            input.id = id;
            input.style.marginRight = '1rem';

            if (go === 0) {
                input.checked = true;
            }

            input.addEventListener('change', () => {
                minBytes = go === 0 ? 0 : go * 1024 * 1024 * 1024; // Go -> octets (base 1024)
                applySizeFilter(minBytes);
            });

            label.appendChild(input);
            label.appendChild(document.createTextNode(labelFor(go)));
            radioGroup.appendChild(label);
        });

        document.body.appendChild(container);

        const dateBox = document.getElementById('filter-container-date');
        if (dateBox) {
            const rect = dateBox.getBoundingClientRect();
            container.style.top = `${rect.bottom + 10}px`;
        } else {
            container.style.top = '540px';
        }

        let observer = null;
        function observeTable() {
            const tbody = document.querySelector('.results table tbody');
            if (!tbody) { return; }
            if (observer) observer.disconnect();
            observer = new MutationObserver(() => applySizeFilter(minBytes));
            observer.observe(tbody, { childList: true, subtree: true });
        }
        observeTable();
        applySizeFilter(minBytes);

        const mainObserver = new MutationObserver(observeTable);
        mainObserver.observe(document.body, { childList: true, subtree: true });
    }

    if (CONSTANTS.REGEX_URL.test(window.location.href)) {
        CONFIG.FILTER_SIZE && buttonSize();
    }

    /* Largeur des lignes des "Torrents du jours" (page spécial avec DataTables.js) */

    async function applyColumnStylesToRow(row) {
        const tds = row.querySelectorAll('td');

        const styles = [
            { width: '90px', textAlign: 'center' },
            { width: '720px', textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
            { width: '120px', textAlign: 'center' },
            { width: '50px', textAlign: 'center' },
            { width: '150px', textAlign: 'center' },
            { width: '70px', textAlign: 'center' },
            { width: '50px', textAlign: 'center' },
            { width: '50px', textAlign: 'center' },
            { width: '50px', textAlign: 'center' }
        ];

        styles.forEach((style, index) => {
            if (tds[index]) {
                Object.assign(tds[index].style, {
                    width: style.width,
                    maxWidth: style.width,
                    textAlign: style.textAlign,
                    overflow: style.overflow || '',
                    textOverflow: style.textOverflow || '',
                    whiteSpace: style.whiteSpace || ''
                });
            }
        });
    }

    async function applyStylesToAllVisibleRows() {
        const tables = document.querySelectorAll('table[id^="DataTables_Table_"]');

        tables.forEach(table => {
            const tbody = table.querySelector('tbody');
            if (!tbody) return;

            const rows = tbody.querySelectorAll('tr');
            rows.forEach(row => applyColumnStylesToRow(row));
        });
    }

    async function watchForNewTables() {
        const observer = new MutationObserver(() => {
            attachDrawHooks();
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    async function attachDrawHooks() {
        const tables = document.querySelectorAll('table[id^="DataTables_Table_"]');

        tables.forEach(table => {
            if (table.dataset.stylesApplied === 'true') return;

            table.dataset.stylesApplied = 'true';

            const dtInstance = $(table).DataTable();
            dtInstance.on('draw', () => {
                const rows = table.querySelectorAll('tbody > tr');
                rows.forEach(row => applyColumnStylesToRow(row));
            });

            const initialRows = table.querySelectorAll('tbody > tr');
            initialRows.forEach(row => applyColumnStylesToRow(row));
        });
    }

    async function startStylingAllTables() {
        if (typeof $ === 'undefined' || !$.fn.DataTable) {
            setTimeout(startStylingAllTables, 300);
            return;
        }

        attachDrawHooks();
        watchForNewTables();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startStylingAllTables);
    } else {
        CONFIG.RESIZE_COLUMNS_TODAY && startStylingAllTables();
    }

    /* Boutons Télécharger */

    async function addCustomButtons() {
        const torrents = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);

        torrents.forEach(torrent => {
            const torrentId = torrent.querySelector('a[target]')?.target;
            if (!torrentId) return;

            const nomCell = torrent.querySelector('td:nth-child(2)');
            let nom = '';
            if (nomCell) {
                const nomLink = nomCell.querySelector('a'); // 'a#torrent_name'
                if (nomLink) {
                    nom = nomLink.textContent.trim();
                }
            }

            const downloadIcon = document.createElement('span');
            downloadIcon.classList.add('ico_download', 'custom_ygg');

            const downloadButton = document.createElement('a');
            downloadButton.href = `/engine/download_torrent?id=${torrentId}`;
            downloadButton.title = `Télécharger le torrent`;

            const imgYGG = document.createElement('img');
            imgYGG.src = 'https://i.ibb.co/hRcq4zSM/favicon-copie-min.png';
            imgYGG.style = 'width: 20px;';
            downloadButton.appendChild(imgYGG);
            downloadButton.style = 'margin-right: 10px; vertical-align: middle;';

            const td = torrent.querySelector('td:nth-child(3)');
            if (td) {
                td.querySelectorAll('.custom_ygg').forEach(el => el.remove());
                td.prepend(downloadButton);
            }
        });
    }

    if (CONSTANTS.REGEX_URL.test(window.location.href)) {
        CONFIG.DOWNLOAD_BUTTONS && addCustomButtons();
    }

    /* Boutons Télécharger sur la pages des "Torrents du jours" (page spécial avec DataTables.js) */

    async function addCustomButtonsToYggRows() {
        document.querySelectorAll('table[id^="DataTables_Table_"] > tbody > tr').forEach(tr => {
            const tds = tr.querySelectorAll('td');
            if (tds.length < 3) return;

            const td = tds[2];

            // Nettoyage des anciens boutons
            td.querySelectorAll('.custom_ygg').forEach(el => el.remove());

            const nfoLink = td.querySelector('a[target]');
            const torrentId = nfoLink ? nfoLink.getAttribute('target') : null;
            if (!torrentId) return;

            // Création du bouton de téléchargement
            const downloadButton = document.createElement('a');
            downloadButton.href = `/engine/download_torrent?id=${torrentId}`;
            downloadButton.title = "Télécharger le torrent";
            downloadButton.className = "custom_ygg";
            downloadButton.style = 'margin-right: 10px; vertical-align: middle;';

            const imgYGG = document.createElement('img');
            imgYGG.src = 'https://i.ibb.co/7JzTJ2CY/favicon-1-min-1.png';
            imgYGG.style = 'width: 20px;';
            downloadButton.appendChild(imgYGG);

            td.prepend(downloadButton);
        });
    }

    async function attachYggButtonHooksToAllTables() {
        const tables = document.querySelectorAll('table[id^="DataTables_Table_"]');

        tables.forEach(table => {
            if (table.dataset.yggButtonsAttached === 'true') return;
            table.dataset.yggButtonsAttached = 'true';

            const dt = $(table).DataTable();
            dt.on('draw', () => {
                addCustomButtonsToYggRows();
            });

            // Exécution immédiate une fois
            addCustomButtonsToYggRows();
        });
    }

    async function watchYggButtonsOnTables() {
        // Attente que jQuery + DataTables soient chargés
        if (typeof $ === 'undefined' || !$.fn.DataTable) {
            setTimeout(watchYggButtonsOnTables, 300);
            return;
        }

        attachYggButtonHooksToAllTables();

        // Surveiller si de nouvelles DataTables apparaissent
        const observer = new MutationObserver(() => {
            attachYggButtonHooksToAllTables();
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Lancer au bon moment
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', watchYggButtonsOnTables);
    } else {
        CONFIG.DOWNLOAD_BUTTONS_TODAY && watchYggButtonsOnTables();
    }

    /* Changement de style au passage de la souris dans la liste des torrents */

    async function hoverTable() {
        const style = document.createElement('style');
        const lignes = document.querySelector(SELECTORS.HOVER_TABLE_LIGNE);

        if(lignes) {
            style.textContent = `
                .results .table td {
                    padding: .5rem !important;
                }

                .results .table tr:hover > * {
                    color: rgba(0, 0, 0, 1) !important;
                    background-color: rgba(0, 0, 0, 0.1) !important;
                }

                .results .table  :not(caption) > * > * {
                    box-shadow: inset 0 0 0 9999px transparent !important;
                }

                #get_nfo img {
                    height: 20px !important;
                    width: 20px !important;
                }
            `;

            document.head.appendChild(style);
        }
    }

    CONFIG.HOVER_TABLE && hoverTable();

    /* Connexion automatique */

    async function handleLogin() {
        const isNotLoggedIn = document.querySelector(SELECTORS.REGISTER_BUTTON);
        const savedCredentials = await GM.getValue(CONSTANTS.COOKIES_STORAGE_KEY);

        if (isNotLoggedIn && !savedCredentials) {
            await getCredentials();
        } else if (isNotLoggedIn && savedCredentials) {
            await autoLogin(savedCredentials);
        }
    }

    async function getCredentials() {
        try {
            const loginForm = document.querySelector(SELECTORS.LOGIN_FORM);

            if (loginForm) {
                loginForm.addEventListener('submit', async (e) => {
                    e.preventDefault();
                    const usernameInput = loginForm.querySelector(SELECTORS.INPUT_USERNAME);
                    const passwordInput = loginForm.querySelector(SELECTORS.INPUT_PASSWORD);
                    await saveCredentials(usernameInput.value, passwordInput.value);
                });
            }
        } catch (error) {
            console.error('Error getting credentials:', error);
        }
    }

    async function autoLogin(credentials) {
        try {
            const loginForm = document.querySelector(SELECTORS.LOGIN_FORM);

            if (loginForm) {
                const usernameInput = loginForm.querySelector(SELECTORS.INPUT_USERNAME);
                const passwordInput = loginForm.querySelector(SELECTORS.INPUT_PASSWORD);
                const submitButton = loginForm.querySelector(SELECTORS.INPUT_SUBMIT);

                if (usernameInput && passwordInput && submitButton) {
                    usernameInput.value = credentials.username;
                    passwordInput.value = credentials.password;

                    submitButton.click();
                }
            }
        } catch (error) {
            console.error('Error during login:', error);
        }
    }

    const logoutLink = document.querySelector(SELECTORS.LOGOUT_LINK);
    if (logoutLink) {
        logoutLink.addEventListener('click', deleteCredentials);
    }

    async function deleteCredentials() {
        try {
            await GM.deleteValue(CONSTANTS.COOKIES_STORAGE_KEY);
        } catch (error) {
            console.error('Error deleting credentials:', error);
        }
    }

    async function saveCredentials(username, password) {
        try {
            await GM.setValue(CONSTANTS.COOKIES_STORAGE_KEY, {
                username,
                password
            });
        } catch (error) {
            console.error('Error saving credentials:', error);
        }
    }

    CONFIG.LOGIN_AUTOMATICALLY && await handleLogin();

    /* Aperçu de l'image du torrent */

    function fetchImageSize(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => resolve({
                width: img.width,
                height: img.height,
                url
            });
            img.onerror = reject;
            img.src = url;
        });
    }

    function makeGetRequest(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: function(response) {
                    resolve(response.responseText);
                },
                onerror: function(error) {
                    reject(error);
                },
            });
        });
    }

    async function getImages(url) {
        const imageRet = [];
        try {
            const response = await makeGetRequest(url);
            const parser = new DOMParser();
            const doc = parser.parseFromString(response, 'text/html');
            if (doc.title.includes('Just a moment...')) {
                imageRet.push({
                    width: 500,
                    height: 500,
                    url: 'https://i.ibb.co/39tFNwS1/Error.png',
                });
                return imageRet;
            }
            const imgElements = doc.getElementsByTagName('img');
            for (const imgElement of imgElements) {
                if (imgElement.src.includes('summer_icon') || imgElement.src.includes('yggtorrent') || imgElement.src.includes('avatars') || imgElement.src.startsWith('data:image/png;base64')) {
                    continue;
                }
                const image = await fetchImageSize(imgElement.src);
                if (image.width > 250 && image.height > 250) {
                    imageRet.push(image);
                    break;
                }
            }
        } catch (error) {
            console.error('Error fetching images:', error);
        }
        return imageRet;
    }

    function displayImageHandler() {
        createImageModal();
        let timeout = null;
        let canDisplay = false;
        const torrents = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);

        if (!torrents) return;

        torrents.forEach((torrent) => {
            const torrentNameLink = torrent.querySelector(SELECTORS.TORRENT_NAME_LINK);
            if (!torrentNameLink) return;

            torrentNameLink.addEventListener('mouseenter', async (e) => {
                timeout = setTimeout(async () => {
                    if (e.target.id !== 'torrent_name') return;
                    canDisplay = true;
                    const images = await getImages(e.target.href);
                    if (canDisplay) {
                        displayImageModal(e, images[0]);
                    }
                }, CONSTANTS.MOUSEENTER_DELAY);
            });

            torrentNameLink.addEventListener('mousemove', (e) => {
                const modal = document.getElementById('imageModal');
                if (modal) {
                    const mouseY = e.clientY - 25;
                    const outOfScreen = mouseY + document.getElementById('imageModalImage').offsetHeight - window.innerHeight;
                    modal.style.top = outOfScreen > -25 ? mouseY - outOfScreen - 25 + 'px' : mouseY + 'px';
                    modal.style.left = e.clientX + 125 + 'px';
                }
            });

            torrentNameLink.addEventListener('mouseout', () => {
                document.getElementById('imageModal').style.display = 'none';
                canDisplay = false;
                clearTimeout(timeout);
            });
        });
    }

    function createImageModal() {
        const modal = document.createElement('div');
        modal.id = 'imageModal';
        modal.classList.add('modal');
        modal.style.display = 'none';

        const modalImage = document.createElement('img');
        modalImage.id = 'imageModalImage';
        modalImage.classList.add('modal-content');
        modalImage.style = `min-width: ${CONFIG.PREVIEWS_IMAGES_SIZE}px; max-width: ${CONFIG.PREVIEWS_IMAGES_SIZE}px;`;

        modal.appendChild(modalImage);
        document.body.append(modal);
    }

    function displayImageModal(event, image) {
        const modal = document.getElementById('imageModal');
        if (modal) {
            const mouseY = event.clientY - 25;

            const modalImage = document.getElementById('imageModalImage');
            modalImage.src = image.url;
            modal.style.display = 'block';
            modal.style.left = event.clientX + 125 + 'px';

            const outOfScreen = mouseY + modalImage.offsetHeight - window.innerHeight;
            modal.style.top = outOfScreen > -25 ? mouseY - outOfScreen - 25 + 'px' : mouseY + 'px';
        }
    }

    CONFIG.PREVIEWS_IMAGES_ON_HOVER && displayImageHandler();

    /* Masquer la barre latérale */

    function hideSidebar() {
        const sidebar = document.getElementById('cat');
        if (sidebar && sidebar.classList.contains('active')) {
            sidebar.querySelector('.open').click();
        }
    }

    CONFIG.HIDE_SIDEBAR && hideSidebar();

    /* Modification du NFO */

    function displayLargerNfo() {
        const modal = document.getElementById(SELECTORS.NFO_MODAL);
        if (!modal) return;

        const modalDialog = modal.querySelector('.modal-dialog');

        modalDialog.classList.remove('modal-sm');
        modalDialog.classList.add('modal-lg');

        const nfoDiv = document.querySelector('#nfo');

        if (nfoDiv) {
            GM_addStyle(`
                @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap&display=swap');
            `);

            GM_addStyle(`
                #nfo {
                    padding: 0 !important;
                }

                #nfo pre {
                    background-color: rgba(255, 255, 255, 1) !important;
                    color: rgba(77, 77, 77, 1) !important;
                    font-family: 'Source Code Pro', monospace !important;
                    font-optical-sizing: auto !important;
                    font-style: normal !important;
                    font-weight: 500 !important;
                    margin: 0 !important;
                    padding: 2rem !important;
                    text-align: center !important;
                }
            `);
        }
    }

    CONFIG.LARGER_NFO && displayLargerNfo();

    /* Résultats de recherche par date de publication (du plus récent au plus ancien) */

    function searchByLatestFirst() {
        const searchForm = document.querySelector(SELECTORS.SEARCH_FORM);
        if (!searchForm) return;

        const orderInput = document.createElement('input');
        orderInput.name = 'order';
        orderInput.value = 'desc';
        orderInput.style = 'display: none';

        const sortInput = document.createElement('input');
        sortInput.name = 'sort';
        sortInput.value = 'publish_date';
        sortInput.style = 'display: none';

        searchForm.append(orderInput);
        searchForm.append(sortInput);
    }

    CONFIG.SEARCH_BY_LATEST_FIRST && searchByLatestFirst();

    /* Conserve les critères de recherche lorsqu'on clique sur une catégorie */

    function keepSearchWhenClickingOnSubcategoryIcon() {
        document.querySelectorAll('[class^="tag_subcat_"]').forEach((node) => {
            const subcategoryId = node.className.split('tag_subcat_')[1];
            node.parentNode.href = `${document.URL}&sub_category=${subcategoryId}`;
        });
    }

    CONFIG.KEEP_SEARCH_WHEN_CLICKING_ON_SUBCATEGORY_ICON && keepSearchWhenClickingOnSubcategoryIcon();

    /**
     * Thème Sombre
     * Par https://github.com/a-carvallo/YggTorrentDark
     * Dernière mise à jour : 25 mars 2025
    */

    async function darkTheme() {
        const customStyles = `
            .donate.pulse{display:none}#middle .row .results tr:nth-child(odd) td{background-color:#3d444e;color:#cbd1da}#middle .row .results tr:nth-child(2n) td{background-color:#2c343f;color:#cbd1da}#middle .row .results tr td:nth-child(8){color:#4caf50}#middle .row .results tr td:nth-child(9){color:#f44336}#commentary li .message .add,#connect a:hover,#connect h3,#connect input:focus,#middle #commentary h4,#middle .comment h4,#middle .default font,#middle .row .results td .ico_comment,#middle .row table td a{color:#cbd1da}#middle .row .results td{background-color:#2c343f;border:1px solid #1b1e24;color:#cbd1da}#middle .row table td.adv_search_option,#middle .row table td:first-child,#middle .row table.infos-torrent td.adv_search_option,#middle .row table.infos-torrent td:first-child,.inbox thead th,.results thead th{background:#354150;color:#cbd1da}#middle .row .results tr:hover td{background:#245b44}#commentary,#middle .row .search-criteria,.card{background:#354150}#middle .row .search-criteria td.adv_search_option,#middle .row .search-criteria td:first-child{background:#354150;color:#cbd1da!important}#middle .row .search-criteria td,#middle .row table td:not(.bg-transparent),#middle .row table.infos-torrent td{background:#354150;color:#cbd1da;border-right:1px solid #1b1e24;border-bottom:1px solid #1b1e24}#middle .search-criteria td input{background:#2a313c;color:#6c798d}#middle .search-criteria td button.solo{transition:.1s ease-in-out;background:0 0;max-width:100%;color:#5ad9a4;top:-1px;font-size:11px;font-weight:700;text-transform:uppercase;border:3px solid #5ad9a4;border-radius:25px;padding:5px 10px}#middle .search-criteria td button.solo:hover{color:#fff;background:#5ad9a4;text-decoration:none}.form-control,.select2-selection__rendered{background:#2a313c;border:1px solid #1b1e24}.select2-container--bootstrap .select2-selection--single{background-color:#2a313c}#connect a,.field-label-responsive label,.form-control,.select2-container--bootstrap .select2-selection--single .select2-selection__rendered,.select2-selection__rendered,.wysibb-toolbar-btn{color:#6c798d}.select2-dropdown{background-color:#2a313c;color:#6c798d}.inbox thead td,.select2-container--bootstrap .select2-dropdown,.table-bordered{border:1px solid #1b1e24}.select2-container--bootstrap .select2-search--dropdown .select2-search__field{border:1px solid #1b1e24;background-color:#2a313c;color:#6c798d}.select2-container--bootstrap .select2-results__option[aria-selected=true]{background-color:#354150}.select2-container--bootstrap .select2-selection{background-color:#2a313c;border:1px solid #1b1e24!important}.select2-container--bootstrap .select2-selection .select2-selection__rendered{border:none}.select2-container,.select2-container--bootstrap{width:400px!important}input:focus{border-color:#6c798d!important;color:#cbd1da}#middle .pagination,#middle .pagination li a{background:#2a313c;color:#cbd1da}#nfoModal .modal-body,#nfoModal .modal-footer,div.bg-light,div.row.justify-content-center.py-5.bg-white{background-color:#354150!important}#middle .pagination li{border-left:1px solid #1b1e24}#middle .pagination li a:hover,.bottom-resize-line.drag,.bottom-resize-line:hover{background:#6c798d}.description-header{background:#354150;border-bottom:1px solid #1b1e24}#nfoModal .modal-footer,#nfoModal .modal-header{border-color:rgba(0,0,0,.125)}#middle table td .red{color:#ef5f5f}#middle .default{background:#354150!important;color:#cbd1da}#middle .default a{color:#5ad9a4}#nfoModal .modal-sm{max-width:60%!important}#nfoModal .modal-body{color:#cbd1da}#middle .add-comment,#middle .add-note{background:#354150;border-bottom:3px solid #6c798d}#commentary li{border-top:1px solid #6c798d}#commentary li .message{background:#6c798d;border:1px solid #6c798d}#commentary li .message:after,#commentary li .message:before{border-right:15px solid #6c798d}#commentary li .message a,.wysibb-body{color:#fff}#commentary li .left{background-color:#6c798d;border:1px solid #1b1e24}#comment-list li img[src$="/assets/img/avatar.jpg"],#middle section.content div.row div.card img[src$="/assets/img/avatar.jpg"]{filter:invert(70%)}#comment-list li .left .ratio{font-size:10px}#comment-list li .ratio .red{color:#b50616}#comment-list li .ratio .green{color:#0a490e}#comment-list .utilisateur .message{color:#eee}.wysibb{background:#354150;border:1px solid #1b1e24}.wysibb .wysibb-toolbar .wysibb-toolbar-container .wysibb-toolbar-btn .fonticon{color:#6c798d;text-shadow:none}.wysibb .wysibb-toolbar{border-bottom:1px solid #1b1e24}.wysibb .wysibb-toolbar .wysibb-toolbar-container{border-right:1px solid #1b1e24}#middle .row table td .input-table{background:#2a313c;color:#cbd1da;border:1px solid #1b1e24;border-radius:5px}#connect{background:#2c343f}#connect input{background:#2a313c;color:#6c798d;border-top:1px solid #1b1e24}.form-control:focus{background:#2a313c;color:#cbd1da;border-color:#6c798d!important}#middle .row table.detail-account{border-left:2px solid #6c798d}.card-footer{border-top:1px solid rgba(27,30,36,.4)!important;background:#354150}#top_panel img[src$="/assets/img/avatar.jpg"]{filter:invert(70%);border-color:rgba(0,0,0,.125)}.well{background-color:#343a40;border-color:rgba(0,0,0,.125)}.well[style^="background: #fff"]{background-color:#6c757d!important;border-color:rgba(0,0,0,.125)}.well[style^="background: #e0ffd7"]{background-color:#48a648!important}.well[style^="background: #f0f0f0"]{text-decoration:line-through;background-color:#555!important;border-color:rgba(0,0,0,.125)}.well strong[style^="color: #3b454e"]{color:#cbd1da!important}[role=button],a,area,button,input,label,select,summary,textarea{touch-action:auto}#middle #description .date,#middle .default .date{background:#2c343f;border-top:1px solid rgba(27,30,36,.4)}h4.text-dark,p.text-dark{color:#f8f9fa!important}.text-primary{color:#eff1f2!important}div.card.mb-0.border.border-dark{background-color:#434b56;border:1px solid #555!important}font.bg-white,table.bg-white{background-color:#434b56!important;border:1px solid #555!important}img.img-thumbnail{background-color:#434b56;border:1px solid #555}table.bg-white.table-hover tbody tr:hover{background-color:#333}#middle table.bg-white tbody tr td{border-right:1px solid #555;border-bottom:1px solid #555;border-top:1px solid #555}div font b span.text-danger{color:#28a745!important}
        `;
        GM_addStyle(customStyles);
    }

    CONFIG.DARK_THEME && darkTheme();
})();