// ==UserScript== // @name AniLINK - Episode Link Extractor // @namespace https://greasyfork.org/en/users/781076-jery-js // @version 4.3.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/* // @match https://yugenanime.*/anime/*/*/watch/ // @match https://yugenanime.tv/anime/*/*/watch/ // @match https://yugenanime.sx/anime/*/*/watch/ // @grant GM_registerMenuCommand // @grant GM_addStyle // @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'; // 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.`; } return button; }, extractEpisodes: async function (status) { status.textContent = 'Starting...'; let episodes = {}; const episodePromises = Array.from(document.querySelectorAll(this.epLinks)).map(async epLink => { try { const page = await fetchPage(epLink.href); const [, epTitle, epNumber] = page.querySelector(this.epTitle).textContent.match(/(.+?) Episode (\d+(?:\.\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); } catch (e) { showToast(e) } }); await Promise.all(episodePromises); return episodes; } }, { name: 'YugenAnime', url: ['yugenanime.tv', 'yugenanime.sx'], epLinks: '.ep-card > a.ep-thumbnail', animeTitle: '.ani-info-ep .link h1', epTitle: 'div.col.col-w-65 > div.box > h1', thumbnail: 'a.ep-thumbnail img', addStartButton: function() { return document.querySelector(".content .navigation").appendChild(Object.assign(document.createElement('a'), { id: "AniLINK_startBtn", className: "link p-15", textContent: "Generate Download Links" })); }, extractEpisodes: async function (status) { status.textContent = 'Getting list of episodes...'; let episodes = {}; const epLinks = Array.from(document.querySelectorAll(this.epLinks)); const throttleLimit = 6; // Number of episodes to extract in parallel for (let i = 0; i < epLinks.length; i += throttleLimit) { const chunk = epLinks.slice(i, i + throttleLimit); let episodePromises = chunk.map(async (epLink, index) => { try { status.textContent = `Loading ${epLink.pathname}` const page = await fetchPage(epLink.href); const animeTitle = page.querySelector(this.animeTitle).textContent; const epNumber = epLink.href.match(/(\d+)\/?$/)[1]; const epTitle = page.querySelector(this.epTitle).textContent.match(/^${epNumber} : (.+)$/) || animeTitle; const thumbnail = document.querySelectorAll(this.thumbnail)[index].src; const episodeTitle = `${epNumber.padStart(3, '0')} - ${animeTitle}` + (epTitle != animeTitle ? `- ${epTitle}` : ''); status.textContent = `Extracting ${episodeTitle}...`; const links = await this._getVideoLinks(page, status, episodeTitle); episodes[episodeTitle] = new Episode(epNumber.padStart(3, '0'), epTitle, links, 'mp4', thumbnail); } catch (e) { showToast(e) }}); await Promise.all(episodePromises); } return episodes; }, // BASED ON N-SUDY's anime_scrapper [https://github.com/N-SUDY/anime_scrapper/blob/80a3c985923a32116fef621050c5de56884a4794/scrape.py#L20] _getVideoLinks: async function (page, status, episodeTitle) { const embedLinkId = page.body.innerHTML.match(new RegExp(`src="//${page.domain}/e/(.*?)/"`))[1]; const embedApiResponse = await fetch(`https://${page.domain}/api/embed/`, { method: 'POST', headers: {"X-Requested-With": "XMLHttpRequest"}, body: new URLSearchParams({ id: embedLinkId, ac: "0" }) }); const json = await embedApiResponse.json(); const m3u8GeneralLink = json.hls[0]; status.textContent = `Parsing ${episodeTitle}...`; // Fetch the m3u8 file content const m3u8Response = await fetch(m3u8GeneralLink); const m3u8Text = await m3u8Response.text(); // Parse the m3u8 file to extract different qualities const qualityMatches = m3u8Text.matchAll(/#EXT-X-STREAM-INF:.*RESOLUTION=\d+x\d+.*NAME="(\d+p)"\n(.*\.m3u8)/g); const links = {}; for (const match of qualityMatches) { const [_, quality, m3u8File] = match; links[quality] = `${m3u8GeneralLink.slice(0, m3u8GeneralLink.lastIndexOf('/') + 1)}${m3u8File}`; } return links; } }, { 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: function() { GM_addStyle(`.theatre-settings .col-sm-3 { max-width: 20%; }`); document.querySelector("div.theatre-settings > div.row").innerHTML += `
Generate Download Links
`; return document.getElementById("AniLINK_startBtn"); }, extractEpisodes: async function (status) { status.textContent = 'Starting...'; let episodes = {}; const episodePromises = Array.from(document.querySelectorAll(this.epLinks)).map(async epLink => { try { const page = await fetchPage(epLink.href); if (page.querySelector(this.epTitle) == null) return; const [, epTitle, epNumber] = page.querySelector(this.epTitle).outerText.split(/Watch (.+) - (\d+(?:\.\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')); status.textContent = `Parsed ${episodeTitle}`; } episodes[episodeTitle] = new Episode(epNumber.padStart(3, '0'), epTitle, links, 'm3u8', thumbnail); } catch (e) { showToast(e) } }); await Promise.all(episodePromises); console.log(episodes); return episodes; }, styles: `div#AniLINK_LinksContainer { font-size: 10px; } #Quality > b > div > ul {font-size: 16px;}` }, ]; /** * Fetches the HTML content of a given URL and parses it into a DOM object. * * @param {string} url - The URL of the page to fetch. * @returns {Promise} A promise that resolves to a DOM Document object. * @throws {Error} If the fetch operation fails. */ async function fetchPage(url) { const response = await fetch(url); if (response.ok) { const page = (new DOMParser()).parseFromString(await response.text(), 'text/html'); return page; } else { showToast(`Failed to fetch HTML for ${url} : ${response.status}`); throw new Error(`Failed to fetch HTML for ${url} : ${response.status}`); } } 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().addEventListener('click', extractEpisodes); // 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 ``; }).join(""); return `
    -------------------${quality}-------------------\n ${listOfLinks}


`; }); // Update the linksContainer with the finally generated links under each quality option header 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;"; linksContainer.innerHTML = qualityLinkLists.join(""); // Add hover event listeners to update link text on hover linksContainer.querySelectorAll('#EpisodeLink').forEach(element => { const episode = element.querySelector('a'); const link = episode.href; const name = decodeURIComponent(episode.download); element.addEventListener('mouseenter', () => window.getSelection().isCollapsed && (episode.textContent = name)); element.addEventListener('mouseleave', () => episode.textContent = decodeURIComponent(link)); }); // Add hover event listeners to quality headers to transform them into speed dials document.querySelectorAll('#Quality b').forEach(header => { const style = `style="background-color: #00A651; padding: 5px 10px; border: none; border-radius: 5px; cursor: pointer; user-select: none;"` const sdHTML = `
` let headerHTML = header.innerHTML; header.parentElement.addEventListener('mouseenter', () => (header.innerHTML = sdHTML, attachBtnClickListeners())); header.parentElement.addEventListener('mouseleave', () => (header.innerHTML = headerHTML)); }); // Attach click listeners to the speed dial buttons function attachBtnClickListeners() { const buttonIds = [ { id: 'AniLINK_selectLinks', handler: onSelectBtnPressed }, { id: 'AniLINK_copyLinks', handler: onCopyBtnClicked }, { id: 'AniLINK_exportLinks', handler: onExportBtnClicked }, { id: 'AniLINK_playLinks', handler: onPlayBtnClicked } ]; buttonIds.forEach(({ id, handler }) => { const button = document.querySelector(`#${id}`); button.addEventListener('click', () => handler(button)); }); // Select Button click event handler function onSelectBtnPressed(it) { const links = it.closest('ol').querySelectorAll('li'); const range = new Range(); range.selectNodeContents(links[0]); range.setEndAfter(links[links.length - 1]); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); it.textContent = 'Selected!!'; setTimeout(() => { it.textContent = 'Select'; }, 1000); } // copySelectedLinks click event handler function onCopyBtnClicked(it) { const links = it.closest('ol').querySelectorAll('li'); const string = [...links].map(link => link.children[1].href).join('\n'); navigator.clipboard.writeText(string); it.textContent = 'Copied!!'; setTimeout(() => { it.textContent = 'Copy'; }, 1000); } // exportToPlaylist click event handler function onExportBtnClicked(it) { // Export all links under the quality header into a playlist file const links = it.closest('ol').querySelectorAll('li'); let string = '#EXTM3U\n'; links.forEach(link => { const episode = decodeURIComponent(link.children[1].download); string += `#EXTINF:-1,${episode}\n` + link.children[1].href + '\n'; }); const fileName = links[0].querySelector('a').title + '.m3u'; const file = new Blob([string], { type: 'application/vnd.apple.mpegurl' }); const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(file), download: fileName }); a.click(); it.textContent = 'Exported!!'; setTimeout(() => { it.textContent = 'Export'; }, 1000); } // PlayWithVLC click event handler function onPlayBtnClicked(it) { // Export all links under the quality header into a playlist file const links = it.closest('ol').querySelectorAll('li'); let string = '#EXTM3U\n'; links.forEach(link => { const episode = decodeURIComponent(link.children[1].download); string += `#EXTINF:-1,${episode}\n` + link.children[1].href + '\n'; }); const file = new Blob([string], { type: 'application/vnd.apple.mpegurl' }); const fileUrl = URL.createObjectURL(file); window.open(fileUrl); it.textContent = 'Launching VLC!!'; setTimeout(() => { it.textContent = 'Play with VLC'; }, 2000); alert("Due to browser limitations, there is a high possibility that this feature may not work correctly.\nIf the video does not automatically play, please utilize the export button and manually open the playlist file manually."); } return { onSelectBtnPressed, onCopyBtnClicked, onExportBtnClicked, onPlayBtnClicked }; } } /*************************************************************** * Display a simple toast message on the top right of the screen ***************************************************************/ let toasts = []; function showToast(message) { const maxToastHeight = window.innerHeight * 0.5; const toastHeight = 50; // Approximate height of each toast const maxToasts = Math.floor(maxToastHeight / toastHeight); console.log(message); // Create the new toast element const x = document.createElement("div"); x.innerHTML = message; x.style.color = "#000"; x.style.backgroundColor = "#fdba2f"; x.style.borderRadius = "10px"; x.style.padding = "10px"; x.style.position = "fixed"; x.style.top = `${toasts.length * toastHeight}px`; x.style.right = "5px"; x.style.fontSize = "large"; x.style.fontWeight = "bold"; x.style.zIndex = "10000"; x.style.display = "block"; x.style.borderColor = "#565e64"; x.style.transition = "right 2s ease-in-out, top 0.5s ease-in-out"; document.body.appendChild(x); // Add the new toast to the list toasts.push(x); // Remove the toast after it slides out setTimeout(() => { x.style.right = "-1000px"; }, 3000); setTimeout(() => { x.style.display = "none"; if (document.body.contains(x)) document.body.removeChild(x); toasts = toasts.filter(toast => toast !== x); // Move remaining toasts up toasts.forEach((toast, index) => { toast.style.top = `${index * toastHeight}px`; }); }, 4000); // Limit the number of toasts to maxToasts if (toasts.length > maxToasts) { const oldestToast = toasts.shift(); document.body.removeChild(oldestToast); toasts.forEach((toast, index) => { toast.style.top = `${index * toastHeight}px`; }); } }