// ==UserScript== // @name AniLINK - Episode Link Extractor // @namespace https://greasyfork.org/en/users/781076-jery-js // @version 4.1.0 // @description Stream or download your favorite anime series effortlessly with AniLINK! Unlock the power to play any anime series directly in your preferred video player or download entire seasons in a single click using popular download managers like IDM. AniLINK generates direct download links for all episodes, conveniently sorted by quality. Elevate your anime-watching experience now! // @icon https://www.google.com/s2/favicons?domain=animepahe.ru // @author Jery // @license MIT // @match https://anitaku.*/* // @match https://anitaku.so/* // @match https://gogoanime.*/* // @match https://gogoanime3.co/* // @match https://gogoanime3.*/* // @match https://animepahe.*/play/* // @match https://animepahe.ru/play/* // @match https://animepahe.com/play/* // @match https://animepahe.org/play/* // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== class Episode { constructor(number, title, links, type, thumbnail) { this.number = number; this.title = title; this.links = links; this.type = type; this.thumbnail = thumbnail; this.name = `${this.title} - ${this.number}`; } } const websites = [ { name: 'GoGoAnime', url: ['anitaku.to/', 'gogoanime3.co/', 'gogoanime3', 'anitaku', 'gogoanime'], epLinks: '#episode_related > li > a', epTitle: '.title_name > h2', linkElems: '.cf-download > a', thumbnail: '.headnav_left > a > img', addStartButton: function() { const button = document.createElement('a'); button.id = "AniLINK_startBtn"; button.style.cssText = `cursor: pointer; background-color: #145132;`; button.innerHTML = ' Generate Download Links'; button.addEventListener('click', extractEpisodes); // Add the button to the page if user is logged in otherwise show placeholder if (document.querySelector('.cf-download')) { document.querySelector('.cf-download').appendChild(button); } else { const loginMessage = document.querySelector('.list_dowload > div > span'); loginMessage.innerHTML = `AniLINK: Please log in to be able to batch download animes.`; } }, extractEpisodes: async function (status) { status.textContent = 'Starting...'; let episodes = {}; const episodePromises = Array.from(document.querySelectorAll(this.epLinks)).map(async epLink => { const response = await fetchHtml(epLink.href); const page = (new DOMParser()).parseFromString(response, 'text/html'); const [, epTitle, epNumber] = page.querySelector(this.epTitle).textContent.match(/(.+?) Episode (\d+)(?:.+)$/); const episodeTitle = `${epNumber.padStart(3, '0')} - ${epTitle}`; const thumbnail = page.querySelector(this.thumbnail).src; const links = [...page.querySelectorAll(this.linkElems)].reduce((obj, elem) => ({ ...obj, [elem.textContent.trim()]: elem.href }), {}); status.textContent = `Extracting ${epTitle} - ${epNumber.padStart(3, '0')}...`; episodes[episodeTitle] = new Episode(epNumber.padStart(3, '0'), epTitle, links, 'mp4', thumbnail); }); await Promise.all(episodePromises); return episodes; } }, { name: 'AnimePahe', url: ['animepahe.ru', 'animepahe.com', 'animepahe.org', 'animepahe'], epLinks: '.dropup.episode-menu .dropdown-item', epTitle: '.theatre-info > h1', linkElems: '#resolutionMenu > button', thumbnail: '.theatre-info > a > img', addStartButton: null, extractEpisodes: async function (status) { status.textContent = 'Starting...'; let episodes = {}; const episodePromises = Array.from(document.querySelectorAll(this.epLinks)).map(async epLink => { const response = await fetchHtml(epLink.href); const page = (new DOMParser()).parseFromString(response, 'text/html'); const [, epTitle, epNumber] = page.querySelector(this.epTitle).outerText.split(/Watch (.+) - (\d+) Online$/); const episodeTitle = `${epNumber.padStart(3, '0')} - ${epTitle}`; const thumbnail = page.querySelector(this.thumbnail).src; status.textContent = `Extracting ${epTitle} - ${epNumber.padStart(3, "0")}...`; async function getVideoUrl(kwikUrl) { const response = await fetch(kwikUrl, { headers: { "Referer": "https://animepahe.com" } }); const data = await response.text(); return eval(/(eval)(\(f.*?)(\n<\/script>)/s.exec(data)[2].replace("eval", "")).match(/https.*?m3u8/)[0]; } let links = {}; for (const elm of [...page.querySelectorAll(this.linkElems)]) { links[elm.textContent] = await getVideoUrl(elm.getAttribute('data-src')); } episodes[episodeTitle] = new Episode(epNumber.padStart(3, '0'), epTitle, links, 'm3u8', thumbnail); }); await Promise.all(episodePromises); console.log(episodes); return episodes; }, styles: `div#AniLINK_LinksContainer { font-size: 10px; } #Quality > b > div > ul {font-size: 16px;}` } ]; async function fetchHtml(url) { const response = await fetch(url); if (response.ok) { return response.text(); } else { alert(`Failed to fetch HTML for ${url}`); throw new Error(`Failed to fetch HTML for ${url}`); } } GM_registerMenuCommand('Extract Episodes', extractEpisodes); // initialize console.log('Initializing AniLINK...'); const site = websites.find(site => site.url.some(url => window.location.href.includes(url))); // attach button to page site.addStartButton(); // append site specific css styles document.body.style.cssText += (site.styles || ''); // This function creates an overlay on the page and displays a list of episodes extracted from a website. // The function is triggered by a user command registered with `GM_registerMenuCommand`. // The episode list is generated by calling the `extractEpisodes` method of a website object that matches the current URL. async function extractEpisodes() { // Restore last overlay if it exists if (document.getElementById("AniLINK_Overlay")) { document.getElementById("AniLINK_Overlay").style.display = "flex"; return; } // Create an overlay to cover the page const overlayDiv = document.createElement("div"); overlayDiv.id = "AniLINK_Overlay"; overlayDiv.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 999; display: flex; align-items: center; justify-content: center;"; document.body.appendChild(overlayDiv); overlayDiv.onclick = event => linksContainer.contains(event.target) ? null : overlayDiv.style.display = "none"; // Create a form to display the Episodes list const linksContainer = document.createElement('div'); linksContainer.id = "AniLINK_LinksContainer"; linksContainer.style.cssText = "position:relative; height:70%; width:60%; color:cyan; background-color:#0b0b0b; overflow:auto; border: groove rgb(75, 81, 84); border-radius: 10px; padding: 10px 5px; resize: both; scrollbar-width: thin; scrollbar-color: cyan transparent; display: flex; justify-content: center; align-items: center;"; overlayDiv.appendChild(linksContainer); // Create a progress bar to display the progress of the episode extraction process const statusBar = document.createElement('span'); statusBar.id = "AniLINK_StatusBar"; statusBar.textContent = "Extracting Links..." statusBar.style.cssText = "background-color: #0b0b0b; color: cyan;"; linksContainer.appendChild(statusBar); // Extract episodes const episodes = await site.extractEpisodes(statusBar); console.log(episodes); // Get all links into format - {[qual1]:[ep1,2,3,4], [qual2]:[ep1,2,3,4], ...} const sortedEpisodes = Object.values(episodes).sort((a, b) => a.number - b.number); const sortedLinks = sortedEpisodes.reduce((acc, episode) => { for (let quality in episode.links) (acc[quality] ??= []).push(episode); return acc; }, {}); console.log('sorted', sortedLinks); const qualityLinkLists = Object.entries(sortedLinks).map(([quality, episode]) => { const listOfLinks = episode.map(ep => { return `