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.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           YggTorrent Plus
// @match          https://www.ygg.re/*
// @version        1.25.1
// @author         J.H / clemente / 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.
// @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/497739
// @icon           https://www.google.com/s2/favicons?sz=64&domain=ygg.re
// ==/UserScript==

(async function() {
    const CONFIG = {
        HOVER_TABLE: true, // Change la couleur de fond du torrent au survol dans les résultats de recherche
        LOGIN_AUTOMATICALLY: true, // Se connecte automatiquement (première connexion manuel obligatoire)
        PREVIEWS_IMAGES_ON_HOVER: true, // Affiche une prévisualisation de l'image du torrent au survol
        PREVIEWS_IMAGES_SIZE: 400, // Taille des images de prévisualisation (en pixels)
        DOWNLOAD_BUTTONS_IN_RESULTS: true, // Ajoute un bouton de téléchargement dans les résultats de recherche
        HIDE_SIDEBAR: false, // Masque la barre latérale
        LARGER_NFO: true, // Agrandit la fenêtre de prévisualisation du NFO
        SEARCH_BY_LATEST_FIRST: true, // Affiche les résultats de recherche par date de publication (du plus récent au plus ancien)
        KEEP_SEARCH_WHEN_CLICKING_ON_SUBCATEGORY_ICON: true, // Garde les critères de recherche lorsqu'on clique sur une catégorie
    };

    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.ygg.re/user/logout"]',
        NFO_MODAL: 'nfoModal',
        SEARCH_FORM: 'form.search',
        LOGIN_FORM: '.login-form',
    };

    const CONSTANTS = {
        IMAGE_MODAL_STYLE: `min-width: ${CONFIG.PREVIEWS_IMAGES_SIZE}px; max-width: ${CONFIG.PREVIEWS_IMAGES_SIZE}px;`,
        COOKIES_STORAGE_KEY: 'yggtorrent_credentials',
        MOUSEENTER_DELAY: 100,
    };

    CONFIG.HOVER_TABLE && hoverTable();
    CONFIG.DOWNLOAD_BUTTONS_IN_RESULTS && addDownloadButtonToTorrents();
    CONFIG.HIDE_SIDEBAR && hideSidebar();
    CONFIG.LARGER_NFO && displayLargerNfo();
    CONFIG.SEARCH_BY_LATEST_FIRST && searchByLatestFirst();
    CONFIG.KEEP_SEARCH_WHEN_CLICKING_ON_SUBCATEGORY_ICON && keepSearchWhenClickingOnSubcategoryIcon();
    CONFIG.PREVIEWS_IMAGES_ON_HOVER && displayImageHandler();
    CONFIG.LOGIN_AUTOMATICALLY && await handleLogin();


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

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

                .table td:hover {
                    background-color: rgba(246, 246, 246, 0.5) !important;
                }

                .table tr:hover {
                    background-color: rgba(246, 246, 246, 0.5) !important;
                    opacity: .8 !important;
                }
            `;

            document.head.appendChild(style);
        }
    }

    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);
        }
    }

    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: 250,
                    height: 250,
                    url: 'https://i.ibb.co/DQb8WVj/cflr.jpg',
                });
                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 = CONSTANTS.IMAGE_MODAL_STYLE;

        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.left = event.clientX + 125 + 'px';
            modal.style.display = 'block';
            const outOfScreen =
                mouseY + modalImage.offsetHeight - window.innerHeight;
            modal.style.top =
                outOfScreen > -25 ? mouseY - outOfScreen - 25 + 'px' : mouseY + 'px';
        }
    }

    function addDownloadButtonToTorrents() {
        const torrents = document.querySelectorAll(SELECTORS.RESULTS_TABLE_ROW);
        torrents.forEach((torrent) => {
            const torrentId = torrent.querySelector('a[target]')?.target;
            if (!torrentId) return;

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

            const downloadButton = document.createElement('a');
            downloadButton.href = `/engine/download_torrent?id=${torrentId}`;
            downloadButton.append(downloadIcon);
            downloadButton.style = 'color: rgb(98, 219, 168); vertical-align: middle; margin-right: 10px;';

            const nameLink = torrent.querySelector('td:nth-child(3) a');
            if (nameLink) {
                nameLink.parentNode.insertBefore(downloadButton, nameLink);
            }
        });
    }

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

	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 {
                    font-family: 'Source Code Pro', monospace !important;
                    font-optical-sizing: auto !important;
                    font-weight: 500 !important;
                    font-style: normal !important;
                    background-color: #fff !important;
                    color: #4d4d4d !important;
                    margin: 0 !important;
                    padding: 2rem !important;
                    text-align: center !important;
                }
            `);

            /*
            nfoDiv.style.background = '#ffffff !important';
            nfoDiv.style.color = '#4d4d4d !important';
            nfoDiv.style.margin = 'auto !important';
            */
        }
	}

    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);
    }

    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}`;
        });
    }
})();