// ==UserScript==
// @name AniLINK - Episode Link Extractor
// @namespace https://greasyfork.org/en/users/781076-jery-js
// @version 6.0.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.bz/*
// @match https://gogoanime.*/*
// @match https://gogoanime3.cc/*
// @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/
// @match https://hianime.*/watch/*
// @match https://hianime.to/watch/*
// @match https://hianime.nz/watch/*
// @match https://hianime.sz/watch/*
// @match https://otaku-streamers.com/info/*/*
// @match https://animeheaven.me/anime.php?*
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @downloadURL none
// ==/UserScript==
class Episode {
constructor(number, title, links, type, thumbnail) {
this.number = number; // The episode number
this.title = title; // The title of the episode (this can be the specific ep title or just the anime name).
this.links = links; // An object containing the download links for the episode, keyed by quality (eg: {"source1":"http://linktovideo.mp4", "source2":"vid2.mp4"}).
this.type = type; // The file type of the video links (eg: "mp4", "m3u8").
this.thumbnail = thumbnail; // The URL of the episode's thumbnail image (if unavailable, then just any image is fine. Thumbnail property isnt really used in the script yet).
this.name = `${this.title} - ${this.number.padStart(3, '0')}.${this.type}`; // The formatted name of the episode, combining title and number.
}
}
/**
* @typedef {Object} Websites[]
* @property {string} name - The name of the website (required).
* @property {string[]} url - An array of URL patterns that identify the website (required).
* @property {string} thumbnail - A CSS selector to identify the episode thumbnail on the website (required).
* @property {Function} addStartButton - A function to add the "Generate Download Links" button to the website (required).
* @property {AsyncGeneratorFunction} extractEpisodes - An async generator function to extract episode information from the website (required).
* @property {string} epLinks - A CSS selector to identify the episode links on the website (optional).
* @property {string} epTitle - A CSS selector to identify the episode title on the website (optional).
* @property {string} linkElems - A CSS selector to identify the download link elements on the website (optional).
* @property {string} [animeTitle] - A CSS selector to identify the anime title on the website (optional).
* @property {string} [epNum] - A CSS selector to identify the episode number on the website (optional).
* @property {Function} [_getVideoLinks] - A function to extract video links from the website (optional).
* @property {string} [styles] - Custom CSS styles to be applied to the website (optional).
*
* @description An array of website configurations for extracting episode links.
*
* @note To add a new website, follow these steps:
* 1. Create a new object with the following properties:
* - `name`: The name of the website.
* - `url`: An array of URL patterns that identify the website.
* - `thumbnail`: A CSS selector to identify the episode thumbnail on the website.
* - `addStartButton`: A function to add the "Generate Download Links" button to the website.
* - `extractEpisodes`: An async generator function to extract episode information from the website.
* 2. Optionally, add the following properties if needed (they arent used by the script, but they will come in handy when the animesite changes its layout):
* - `animeTitle`: A CSS selector to identify the anime title on the website.
* - `epLinks`: A CSS selector to identify the episode links on the website.
* - `epTitle`: A CSS selector to identify the episode title on the website.
* - `linkElems`: A CSS selector to identify the download link elements on the website.
* - `epNum`: A CSS selector to identify the episode number on the website.
* - `_getVideoLinks`: A function to extract video links from the website.
* - `styles`: Custom CSS styles to be applied to the website.
* 3. Implement the `addStartButton` function to add the "Generate Download Links" button to the website.
* - This function should create a element and append it to the appropriate location on the website.
* - The button should have an ID of "AniLINK_startBtn".
* 4. Implement the `extractEpisodes` function to extract episode information from the website.
* - This function should be an async generator function that yields Episode objects (To ensure fast processing, using chunks is recommended).
* - Use the `fetchPage` function to fetch the HTML content of each episode page.
* - Parse the HTML content to extract the episode title, number, links, and thumbnail.
* - Create an `Episode` object for each episode and yield it using the `yieldEpisodesFromPromises` function.
* 5. Optionally, implement the `_getVideoLinks` function to extract video links from the website.
* - This function should return a promise that resolves to an object containing video links.
* - Use this function if the video links require additional processing or API calls.
* - Tip: use GM_xmlhttpRequest to make cross-origin requests if needed (I've used proxy.sh so far which I plan to change in the future since GM_XHR seems more reliable).
*/
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 = Object.assign(document.createElement('a'), {
id: "AniLINK_startBtn",
style: "cursor: pointer; background-color: #145132;",
innerHTML: document.querySelector("div.user_auth a[href='/login.html']")
? `AniLINK: Please log in to download`
: ' Generate Download Links'
});
const target = location.href.includes('/category/') ? '#episode_page' : '.cf-download';
document.querySelector(target)?.appendChild(button);
return button;
},
extractEpisodes: async function* (status) {
status.textContent = 'Starting...';
const throttleLimit = 12; // Number of episodes to extract in parallel
const epLinks = Array.from(document.querySelectorAll(this.epLinks));
for (let i = 0; i < epLinks.length; i += throttleLimit) {
const chunk = epLinks.slice(i, i + throttleLimit);
const episodePromises = chunk.map(async epLink => { try {
const page = await fetchPage(epLink.href);
const [, epTitle, epNumber] = page.querySelector(this.epTitle).textContent.match(/(.+?) Episode (\d+(?:\.\d+)?)/);
const thumbnail = page.querySelector(this.thumbnail).src;
status.textContent = `Extracting ${epTitle} - ${epNumber.padStart(3, '0')}...`;
const links = [...page.querySelectorAll(this.linkElems)].reduce((obj, elem) => ({ ...obj, [elem.textContent.trim()]: elem.href }), {});
status.textContent = `Extracted ${epTitle} - ${epNumber.padStart(3, '0')}`;
return new Episode(epNumber, epTitle, links, 'mp4', thumbnail); // Return Episode object
} catch (e) { showToast(e); return null; } }); // Handle errors and return null
yield* yieldEpisodesFromPromises(episodePromises); // Use helper function
}
}
},
{
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...';
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);
const 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;
status.textContent = `Extracting ${`${epNumber.padStart(3, '0')} - ${animeTitle}` + (epTitle != animeTitle ? `- ${epTitle}` : '')}...`;
const links = await this._getVideoLinks(page, status, (`${epNumber.padStart(3, '0')} - ${animeTitle}` + (epTitle != animeTitle ? `- ${epTitle}` : '')));
return new Episode(epNumber, epTitle, links, 'm3u8', thumbnail); // Return Episode object
} catch (e) { showToast(e); return null; } }); // Handle errors and return null
yield* yieldEpisodesFromPromises(episodePromises); // Use helper function
}
},
_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 += `