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 - z A - Z 0 - 9 ] { 48 } \b / ) , match2 = html . match ( / \b ( [ a - z A - Z 0 - 9 ] { 16 } ) \b .* ?\b ( [ a - z A - Z 0 - 9 ] { 16 } ) \b .* ?\b ( [ a - z A - Z 0 - 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 ***************************************************************/
16121621let 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