Skip to content

Commit 74bdf8d

Browse files
committed
[AniLINK] handle megacloud extractor rate limiting + smarter, modern toasts
1 parent c7dc131 commit 74bdf8d

1 file changed

Lines changed: 101 additions & 44 deletions

File tree

AniLINK/AniLINK_Episode-Link-Extractor.user.js

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name AniLINK - Episode Link Extractor
33
// @namespace https://greasyfork.org/en/users/781076-jery-js
4-
// @version 6.19.1
4+
// @version 6.20.0
55
// @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!
66
// @icon https://www.google.com/s2/favicons?domain=animepahe.ru
77
// @author Jery
@@ -636,12 +636,12 @@ const Websites = [
636636
{
637637
name: 'HiAnime',
638638
url: ['hianime.to/', 'hianimez.is/', 'hianimez.to/', 'hianime.nz/', 'hianime.bz/', 'hianime.pe/', 'hianime.cx/', 'hianime.gs/'],
639-
_chunkSize: 6, // Number of episodes to extract in parallel
639+
_chunkSize: 1, // Number of episodes to extract in parallel
640640
extractEpisodes: async function* (status) {
641641
for (let i = 0, epList = await applyEpisodeRangeFilter($('.ss-list > a').get()); i < epList.length; i += this._chunkSize) {
642642
yield* yieldEpisodesFromPromises(epList.slice(i, i + this._chunkSize).map(async e => {
643643
const [epId, epNum, epTitle] = [$(e).data('id'), $(e).data('number'), $(e).find('.ep-name').text()]; let thumbnail = '';
644-
status.text = `Extracting Episodes ${epNum-Math.min(this._chunkSize, epNum)+1} - ${epNum}...`;
644+
status.text = `Extracting Episode ${epNum-Math.min(this._chunkSize, epNum)+1}...`;
645645
const servers = await $((await $.get(`/ajax/v2/episode/servers?episodeId=${epId}`, r => $(r).responseJSON)).html).find('.server-item').map((_, i) => [[$(i).text().trim(), { id: $(i).data('id'), type: $(i).data('type') }]]).get();
646646
// Prefer HD-2 if available. (HD-1 and HD-3 might have CORS issues)
647647
const filteredServers = servers.filter(([s]) => !['HD-1', 'HD-3'].includes(s));
@@ -821,12 +821,18 @@ const Extractors = {
821821
},
822822
'megacloud.blog': async function (embed, referer) {
823823
// adapted from https://github.com/yuzono/aniyomi-extensions/blob/master/lib/megacloud-extractor/src/main/java/eu/kanade/tachiyomi/lib/megacloudextractor/MegaCloudExtractor.kt
824-
const html = await GM_fetch(embed, { headers: { referer, 'User-Agent': USER_AGENT_HEADER } }).then(r => r.text());
824+
const res = await GM_fetch(embed, { headers: { referer, 'User-Agent': USER_AGENT_HEADER } });
825+
const retryAfter = res.headers.get('Retry-After'); // Rate limit Policy: 10 requests per minute
826+
if (retryAfter) {
827+
const hhmmss = new Date(new Date().getTime() + parseInt(retryAfter) * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true });
828+
showToast(`Rate limited by megacloud.blog, retrying in ${retryAfter} seconds (at ${hhmmss})...`, parseInt(retryAfter) * 1000);
829+
return await new Promise(res => setTimeout(res, 500 + parseInt(retryAfter) * 1000)).then(() => Extractors['megacloud.blog'](embed, referer)); // recursive retry
830+
}
831+
const html = await res.text();
825832
const match1 = html.match(/\b[a-zA-Z0-9]{48}\b/), match2 = html.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/);
826833
const nonce = match1?.[0] || (match2 ? match2[1] + match2[2] + match2[3] : null);
827834
if (!nonce) throw new Error('Failed to extract nonce from response');
828835
const sId = embed.split('/e-1/')[1]?.split('?')[0];
829-
if (!sId) throw new Error('Failed to extract ID from URL');
830836
const host = (new URL(embed)).host;
831837
const url = `https://${host}/embed-2/v3/e-1/getSources?id=${sId}&_k=${nonce}`;
832838
const data = await GM_fetch(url, { headers: { 'Accept': '*/*', 'X-Requested-With': 'XMLHttpRequest', 'Referer': `https://${host}/` } }).then(r => r.json());
@@ -1127,6 +1133,7 @@ async function extractEpisodes() {
11271133
try {
11281134
const episodeGenerator = site.extractEpisodes(status);
11291135
const qualityLinkLists = {}; // Stores lists of links for each quality
1136+
const startTime = Date.now();
11301137

11311138
for await (const episode of episodeGenerator) {
11321139
if (!status.isExtracting) { // Check if extraction is stopped
@@ -1144,15 +1151,17 @@ async function extractEpisodes() {
11441151
// Update UI in real-time - RENDER UI HERE BASED ON qualityLinkLists
11451152
renderQualityLinkLists(qualityLinkLists, qualitiesContainer);
11461153
}
1154+
1155+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
11471156
statusIconElement.querySelector('i').classList.remove('extracting');
11481157
if (qualityLinkLists && Object.keys(qualityLinkLists).length > 0) {
1149-
status = { isExtracting: false, text: "Extraction Complete!" };
1158+
status = { isExtracting: false, text: `Extraction Complete in ${duration} seconds` };
11501159
} else {
11511160
status = { isExtracting: false, text: "No episodes found." };
11521161
}
11531162
} catch (error) {
11541163
console.error('Error during episode extraction:', error);
1155-
status = { isExtracting: false, text: "Extraction Failed.", error: error.message || error.toString() };
1164+
status = { isExtracting: false, text: `Extraction Failed after ${duration} seconds.`, error: error.message || error.toString() };
11561165
}
11571166

11581167
// Renders quality link lists inside a given container element
@@ -1611,55 +1620,103 @@ async function applyEpisodeRangeFilter(allEpLinks) {
16111620
***************************************************************/
16121621
let toasts = [];
16131622

1614-
function showToast(message) {
1623+
function showToast(message, duration = 5000) {
16151624
const maxToastHeight = window.innerHeight * 0.5;
1616-
const toastHeight = 50; // Approximate height of each toast
1625+
const toastHeight = 70;
16171626
const maxToasts = Math.floor(maxToastHeight / toastHeight);
16181627

16191628
console.log(message);
16201629

1630+
// Inject toast styles if not already present
1631+
if (!document.getElementById('anlink-toast-styles')) {
1632+
GM_addStyle(`
1633+
@keyframes anlink-toast-slide-in { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
1634+
@keyframes anlink-toast-slide-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } }
1635+
.anlink-toast { position: fixed; right: 20px; min-width: 300px; max-width: 400px; background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 12px; padding: 16px 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08); z-index: 10000; display: flex; align-items: flex-start; gap: 12px; animation: anlink-toast-slide-in 0.3s cubic-bezier(0.16, 1, 0.3, 1); backdrop-filter: blur(10px); transition: top 0.4s cubic-bezier(0.16, 1, 0.3, 1); }
1636+
.anlink-toast.slide-out { animation: anlink-toast-slide-out 0.3s cubic-bezier(0.7, 0, 0.84, 0) forwards; }
1637+
.anlink-toast-icon { flex-shrink: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #26a69a 0%, #20847a 100%); border-radius: 50%; color: white; font-size: 14px; font-weight: bold; }
1638+
.anlink-toast-content { flex: 1; color: #1a1a1a; font-size: 14px; line-height: 1.5; font-weight: 500; }
1639+
.anlink-toast-content a { color: #26a69a; text-decoration: none; font-weight: 600; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
1640+
.anlink-toast-content a:hover { border-bottom-color: #26a69a; }
1641+
.anlink-toast-close { flex-shrink: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.05); border: none; border-radius: 50%; color: #666; cursor: pointer; font-size: 16px; line-height: 1; transition: all 0.2s; padding: 0; }
1642+
.anlink-toast-close:hover { background: rgba(0, 0, 0, 0.1); color: #1a1a1a; transform: scale(1.1); }
1643+
/* Dark mode support */
1644+
@media (prefers-color-scheme: dark) {
1645+
.anlink-toast { background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%); border-color: rgba(255, 255, 255, 0.1); }
1646+
.anlink-toast-content { color: #e0e0e0; }
1647+
.anlink-toast-close { background: rgba(255, 255, 255, 0.1); color: #ccc; }
1648+
.anlink-toast-close:hover { background: rgba(255, 255, 255, 0.2); color: #fff; }
1649+
}
1650+
`);
1651+
const styleTag = document.createElement('style');
1652+
styleTag.id = 'anlink-toast-styles';
1653+
document.head.appendChild(styleTag);
1654+
}
1655+
16211656
// Create the new toast element
1622-
const x = document.createElement("div");
1623-
x.innerHTML = message;
1624-
x.style.color = "#000";
1625-
x.style.backgroundColor = "#fdba2f";
1626-
x.style.borderRadius = "10px";
1627-
x.style.padding = "10px";
1628-
x.style.position = "fixed";
1629-
x.style.top = `${toasts.length * toastHeight}px`;
1630-
x.style.right = "5px";
1631-
x.style.fontSize = "large";
1632-
x.style.fontWeight = "bold";
1633-
x.style.zIndex = "10000";
1634-
x.style.display = "block";
1635-
x.style.borderColor = "#565e64";
1636-
x.style.transition = "right 2s ease-in-out, top 0.5s ease-in-out";
1637-
document.body.appendChild(x);
1657+
const toast = document.createElement("div");
1658+
toast.className = "anlink-toast";
1659+
toast.style.top = `${20 + toasts.length * toastHeight}px`;
1660+
1661+
// Infer toast type and icon from message content
1662+
const lowerMsg = message.toLowerCase();
1663+
const iconMap = { error: ['❌', '#ef5350'], success: ['✅', '#66bb6a'], warning: ['⚠️', '#ffa726'], loading: ['⏳', '#42a5f5'], help: ['💡', '#ab47bc'], info: ['ℹ️', null] };
1664+
const typeChecks = [
1665+
[['error', 'failed', 'couldn\'t', 'could not'], 'error'],
1666+
[['success', 'complete', 'copied', 'exported', 'sent to'], 'success'],
1667+
[['warning', 'no episodes', 'not found', 'rate limited'], 'warning'],
1668+
[['loading', 'fetching', 'extracting', 'processing'], 'loading'],
1669+
[['install', 'mpv', 'handler'], 'help']
1670+
];
1671+
const toastType = typeChecks.find(([keywords]) => keywords.some(k => lowerMsg.includes(k)))?.[1] || 'info';
1672+
const [icon, borderColor] = iconMap[toastType];
1673+
if (borderColor) toast.style.borderLeft = `4px solid ${borderColor}`;
1674+
1675+
toast.innerHTML = `
1676+
<div class="anlink-toast-icon">${icon}</div>
1677+
<div class="anlink-toast-content">${message}</div>
1678+
<button class="anlink-toast-close" aria-label="Close">×</button>
1679+
`;
1680+
1681+
document.body.appendChild(toast);
1682+
1683+
// Close button handler
1684+
const closeBtn = toast.querySelector('.anlink-toast-close');
1685+
const removeToast = () => {
1686+
toast.classList.add('slide-out');
1687+
setTimeout(() => {
1688+
if (document.body.contains(toast)) document.body.removeChild(toast);
1689+
toasts = toasts.filter(t => t !== toast);
1690+
// Reposition remaining toasts
1691+
toasts.forEach((t, index) => {
1692+
t.style.top = `${20 + index * toastHeight}px`;
1693+
});
1694+
}, 300);
1695+
};
1696+
1697+
closeBtn.addEventListener('click', removeToast);
16381698

16391699
// Add the new toast to the list
1640-
toasts.push(x);
1641-
1642-
// Remove the toast after it slides out
1643-
setTimeout(() => {
1644-
x.style.right = "-1000px";
1645-
}, 3000);
1646-
1647-
setTimeout(() => {
1648-
x.style.display = "none";
1649-
if (document.body.contains(x)) document.body.removeChild(x);
1650-
toasts = toasts.filter(toast => toast !== x);
1651-
// Move remaining toasts up
1652-
toasts.forEach((toast, index) => {
1653-
toast.style.top = `${index * toastHeight}px`;
1654-
});
1655-
}, 4000);
1700+
toasts.push(toast);
1701+
1702+
// Auto-remove after delay (or dont remove if duration is 0)
1703+
if (duration > 0) {
1704+
setTimeout(() => removeToast(), duration);
1705+
}
16561706

16571707
// Limit the number of toasts to maxToasts
16581708
if (toasts.length > maxToasts) {
16591709
const oldestToast = toasts.shift();
1660-
document.body.removeChild(oldestToast);
1661-
toasts.forEach((toast, index) => {
1662-
toast.style.top = `${index * toastHeight}px`;
1710+
oldestToast.classList.add('slide-out');
1711+
setTimeout(() => {
1712+
if (document.body.contains(oldestToast)) {
1713+
document.body.removeChild(oldestToast);
1714+
}
1715+
}, 300);
1716+
1717+
// Reposition remaining toasts
1718+
toasts.forEach((t, index) => {
1719+
t.style.top = `${20 + index * toastHeight}px`;
16631720
});
16641721
}
16651722
}

0 commit comments

Comments
 (0)