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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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}`;
        });
    }
})();