1let animeId = null; 2let epId = null; 3let epNumber = null; 4let isLoggedIn = user.is_logged_in || false; 5window.addEventListener('hianime:user-ready', function (event) { 6 isLoggedIn = !!(event.detail && event.detail.is_logged_in); 7}); 8let watchingLogInterval = null; 9let watchingLogTimeout = null; 10let lastContinueWatchingKey = ''; 11let continueWatchingRequestInFlight = false; 12let currentVideoData = { 13 currentTime: 0, 14 duration: 0 15}; 16const hiEpisodeCacheTtl = 6 * 60 * 60 * 1000; 17const hiContinueWatchingInitialDelay = 60 * 1000; 18const hiContinueWatchingInterval = 60 * 1000; 19const hiPendingRequests = {}; 20let hiPageFullyLoaded = document.readyState === 'complete'; 21 22window.addEventListener('load', function () { 23 hiPageFullyLoaded = true; 24}); 25 26function hiRestUrl(path) { 27 const baseUrl = (hianime_ep_ajax.rest_url || '').replace(/\/+$/, ''); 28 return baseUrl + '/' + String(path || '').replace(/^\/+/, ''); 29} 30 31function hiCacheGet(key) { 32 try { 33 const cached = sessionStorage.getItem(key); 34 if (!cached) return null; 35 36 const item = JSON.parse(cached); 37 if (!item || !item.expires || item.expires < Date.now()) { 38 sessionStorage.removeItem(key); 39 return null; 40 } 41 42 return item.value; 43 } catch (e) { 44 return null; 45 } 46} 47 48function hiCacheSet(key, value) { 49 try { 50 sessionStorage.setItem(key, JSON.stringify({ 51 expires: Date.now() + hiEpisodeCacheTtl, 52 value: value 53 })); 54 } catch (e) {} 55} 56 57function hiFetchPublicJson(url, options = {}) { 58 const timeout = options.timeout || 3000; 59 const retries = options.retries || 3; 60 const credentials = options.credentials || 'omit'; 61 62 function attempt(remaining) { 63 if (window.fetch) { 64 const controller = window.AbortController ? new AbortController() : null; 65 const timer = controller ? setTimeout(() => controller.abort(), timeout) : null; 66 67 return fetch(url, { 68 method: 'GET', 69 credentials: credentials, 70 cache: 'no-store', 71 signal: controller ? controller.signal : undefined, 72 headers: { 73 'Accept': 'application/json', 74 'Cache-Control': 'no-cache' 75 } 76 }).then((response) => { 77 if (timer) clearTimeout(timer); 78 if (!response.ok) { 79 throw new Error(response.statusText || 'Request failed'); 80 } 81 82 return response.json(); 83 }).catch((error) => { 84 if (timer) clearTimeout(timer); 85 if (remaining > 1) { 86 return attempt(remaining - 1); 87 } 88 throw error; 89 }); 90 } 91 92 return $.ajax({ 93 url: url, 94 type: 'GET', 95 timeout: timeout 96 }).catch((error) => { 97 if (remaining > 1) { 98 return attempt(remaining - 1); 99 } 100 throw error; 101 }); 102 } 103 104 return attempt(retries); 105} 106 107function hiAfterPageLoad(callback) { 108 if (hiPageFullyLoaded || document.readyState === 'complete') { 109 setTimeout(callback, 0); 110 return; 111 } 112 113 window.addEventListener('load', function () { 114 setTimeout(callback, 0); 115 }, { once: true }); 116} 117 118function selectEpisode($episode, options = {}) { 119 if (!$episode || !$episode.length) { 120 return; 121 } 122 123 const episodeId = $episode.data('id'); 124 if (!episodeId) { 125 console.error('Episode ID not found'); 126 return; 127 } 128 129 const isSameEpisode = String(episodeId) === String(epId || ''); 130 131 $('.ep-item').removeClass('active'); 132 $episode.addClass('active'); 133 epId = episodeId; 134 epNumber = $episode.data('number'); 135 136 if (!isSameEpisode || options.reloadSources) { 137 loadSources(episodeId); 138 } else if (!$('.player-frame iframe').length) { 139 selectServer($('#servers-content .server-item').first()); 140 } 141 142 if (!options.skipHistory) { 143 const episodeUrl = $episode.attr('href'); 144 if (episodeUrl && episodeUrl !== window.location.href && episodeUrl !== window.location.pathname) { 145 history.pushState(null, '', episodeUrl); 146 } 147 } 148 149 $('#wp-admin-bar-edit a').attr("href", "/wp-admin/post.php?post=" + epId + "&action=edit"); 150} 151 152function selectServer($serverItem) { 153 if (!$serverItem || !$serverItem.length) { 154 return; 155 } 156 157 const serverName = $serverItem.data('server-name'); 158 const serverHash = $serverItem.data('hash'); 159 if (!serverName || !serverHash) { 160 console.error('Server name or hash not found'); 161 return; 162 } 163 164 $('#servers-content .server-item .btn').removeClass('active'); 165 $serverItem.find('.btn').addClass('active'); 166 167 const embedUrl = atob(serverHash); 168 const iframe = document.createElement('iframe'); 169 iframe.title = serverName; 170 iframe.frameBorder = '0'; 171 iframe.allowFullscreen = true; 172 iframe.src = embedUrl; 173 174 $('.player-frame').empty().append(iframe); 175} 176 177function handleEpisodeListResponse(response) { 178 if (!response?.status) { 179 console.error('Failed to load episode list'); 180 $('#episodes-content').html('<div class="alert alert-warning">Failed to load episodes. Please refresh the page.</div>'); 181 return; 182 } 183 184 if (response.totalItems > 1) { 185 $('.block-prev, .block-next').show(); 186 } 187 188 $('#episodes-content').html(response.html); 189 190 if ($('.toggle-lang').hasClass('off')) { 191 toggleAnimeName(); 192 } 193 194 const $selectedEpisode = $('.ep-item[data-id="' + epId + '"]').first(); 195 if ($selectedEpisode.length) { 196 selectEpisode($selectedEpisode, { skipHistory: true });
197 } 198 199 if ($(".ep-page-item").length > 0) { 200 const activePage = $(".ep-item.active").parent().data("page"); 201 $(".ep-page-item[data-page=" + activePage + "]").click(); 202 } else { 203 const activePage = $(".ep-item.active").parent().data("page"); 204 $("#episodes-page-" + activePage).show(); 205 } 206} 207 208function handleSourcesResponse(response) { 209 if (!response?.status || !response?.html) { 210 console.error('Failed to load sources'); 211 $('#servers-content').html('<div class="alert alert-warning">Failed to load sources. Please refresh the page.</div>'); 212 return; 213 } 214 215 $('#servers-content').html(response.html); 216 $('#servers-content .loading-relative').remove(); 217 218 const firstServerItem = $('#servers-content .server-item').first(); 219 if (firstServerItem.length) { 220 selectServer(firstServerItem); 221 currentVideoData = { 222 currentTime: 0, 223 duration: 0 224 }; 225 startLoggingContinueWatching(); 226 } 227} 228 229function hydrateInitialWatchPage() { 230 if (!$('#episodes-content .ep-item').length) { 231 loadEpisodeList(); 232 return; 233 } 234 235 if ($('.ep-item').length > 1) { 236 $('.block-prev, .block-next').show(); 237 } 238 239 let $selectedEpisode = $('.ep-item[data-id="' + epId + '"]').first(); 240 if (!$selectedEpisode.length) { 241 $selectedEpisode = $('.ep-item.active').first(); 242 } 243 if (!$selectedEpisode.length) { 244 $selectedEpisode = $('.ep-item').first(); 245 } 246 247 if ($selectedEpisode.length) { 248 $('.ep-item').removeClass('active'); 249 $selectedEpisode.addClass('active'); 250 epId = $selectedEpisode.data('id'); 251 epNumber = $selectedEpisode.data('number'); 252 $('#wp-admin-bar-edit a').attr("href", "/wp-admin/post.php?post=" + epId + "&action=edit"); 253 } 254 255 if ($(".ep-page-item").length > 0) { 256 const activePage = $(".ep-item.active").parent().data("page"); 257 $(".ep-page-item").removeClass("active"); 258 $(".ep-page-item .ic-active").hide(); 259 const $pageItem = $(".ep-page-item[data-page=" + activePage
259+ "]").first(); 260 $pageItem.addClass("active"); 261 $pageItem.find(".ic-active").show(); 262 $(".ss-list-min").hide().removeClass("active"); 263 $("#episodes-page-" + activePage).show().addClass("active"); 264 $("#current-page").text($pageItem.text().trim()); 265 } else { 266 $(".ss-list").show().addClass("active"); 267 } 268 269 if ($('.toggle-lang').hasClass('off')) { 270 toggleAnimeName(); 271 } 272 273 const initialServersHtml = $('#servers-content').html(); 274 if (initialServersHtml && epId) { 275 hiCacheSet('hi_episode_servers_' + epId, { 276 status: true, 277 html: initialServersHtml 278 }); 279 } 280 281 hiAfterPageLoad(function () { 282 const firstServerItem = $('#servers-content .server-item').first(); 283 if (firstServerItem.length) { 284 selectServer(firstServerItem); 285 currentVideoData = { 286 currentTime: 0, 287 duration: 0 288 }; 289 startLoggingContinueWatching(); 290 } else { 291 $('.player-frame').empty(); 292 } 293 }); 294} 295// Main initialization 296$(document).ready(() => { 297 initializeAnimeId(); 298 299 $(document).on('click', '.ep-item', function (e) { 300 e.preventDefault(); 301 selectEpisode($(this)); 302 }); 303 304 $(document).on('click', '#servers-content .server-item', function (e) { 305 e.preventDefault(); 306 selectServer($(this)); 307 }); 308 309 $(document).on("click", ".ep-page-item", function () { 310 $(".ep-page-item").removeClass("active"), 311 $(".ep-page-item .ic-active").hide(), 312 $(this).addClass("active"), 313 $(this).find(".ic-active").show(), 314 $(".ss-list-min").hide(), 315 $(".ss-list-min").removeClass("active"), 316 $("#episodes-page-" + $(this).data("page")).show(), 317 $("#episodes-page-" + $(this).data("page")).addClass("active"), 318 $("#current-page").text($(this).text().trim()) 319 }); 320 // Search episode functionality 321 $(document).on("keyup", "#search-ep", function (e) { 322 e.preventDefault(); 323 324 var targetElement, inputValue = e.target.value; 325 326 // Remove highlight from all episode items 327 $(".ep-item").removeClass("highlight"); 328 329 if (inputValue) { 330 // Find episode item with matching data-number 331 var episodeItem = $(".ep-item[data-number=" + inputValue + "]"); 332 333 if (episodeItem.length > 0) { 334 // Get the parent container 335 targetElement = episodeItem.parent(); 336 337 // Click on the corresponding page item 338 $(".ep-page-item[data-page=" + targetElement.data("page") + "]").click(); 339 340 // If Enter key was pressed (keyCode 13) 341 if (e.keyCode === 13) { 342 // Clear the input and click the episode item 343 $(e.target).val(""); 344 episodeItem.click(); 345 } else { 346 // Just highlight the episode item 347 episodeItem.addClass("highlight"); 348 } 349 } 350 } else { 351 // If input is empty, go to the page containing the active episode 352 targetElement = $(".ep-item.active").parent().data("page"); 353 $(".ep-page-item[data-page=" + targetElement + "]").click(); 354 } 355 }); 356 357 $("#media-resize").click(function (e) { 358 $(".anis-watch-wrap").toggleClass("extend"); 359 if ($(".anis-watch-wrap").hasClass("extend")) { 360 $(this).html('<i class="fas fa-compress mr-1"></i>Collapse'); 361 } else { 362 $(this).html('<i class="fas fa-expand mr-1"></i>Expand'); 363 } 364 }); 365 $("#turn-off-light").click(function (e) { 366 $("#mask-overlay, .anis-watch-wrap").toggleClass("active"); 367 }); 368 $("#mask-overlay").click(function (e) { 369 $("#mask-overlay, .anis-watch-wrap").removeClass("active"); 370 $("#turn-off-light").removeClass("off"); 371 });
372 $(".dt-comment").click(function () { 373 window.scrollTo({ 374 top: document.getElementById("comment-block").offsetTop, 375 behavior: 'smooth' 376 }); 377 }); 378 379 // Auto play setting 380 if (parseInt(getUserSetting('auto_play', '0')) === 1) { 381 $('.quick-settings[data-option="auto_play"]').removeClass("off"); 382 } 383 384 // Auto next setting 385 if (parseInt(getUserSetting('auto_next', '0')) === 1) { 386 $('.quick-settings[data-option="auto_next"]').removeClass("off"); 387 } 388 389 // Auto skip intro setting 390 if (parseInt(getUserSetting('auto_skip_intro', '0')) === 1) { 391 $('.quick-settings[data-option="auto_skip_intro"]').removeClass("off"); 392 } 393 394 // Quick settings click handler 395 $(".quick-settings").click(function () { 396 const option = $(this).data("option"); 397 const isOff = $(this).hasClass("off"); 398 updateLocalStorageSetting(option, isOff ? '0' : '1'); 399 }); 400 401 $(".btn-emo").click(function (){ 402 $(this).closest('.block-rating').addClass('rated'); 403 $(this).siblings().removeClass('emo-rated'); 404 $(this).addClass('emo-rated'); 405 let ratings = window.localStorage.getItem('emo-ratings') || '{}'; 406 window.localStorage.setItem('emo-ratings', JSON.stringify({...JSON.parse(ratings), [`${$(this).data('id')}`]: $(this).data('mark')})); 407 }) 408 409 // If user has rated this anime, auto-select the rating button 410 let animeId = $('#ani_detail').data('anime-id'); 411 const ratings = JSON.parse(window.localStorage.getItem('emo-ratings') || '{}'); 412 if (ratings && ratings[animeId]) { 413 $(`.btn-emo[data-id="${animeId}"][data-mark="${ratings[animeId]}"]`).click(); 414 } 415 416 // Quick settings toggle handler 417 $(".quick-settings-toggle").click(function () { 418 $(this).toggleClass("active"); 419 const option = $(this).data("option"); 420 const isActive = $(this).hasClass("active"); 421 updateUserSetting(option, isActive ? 1 : 0); 422 }); 423 424 updateScheduleDate(); 425 426}); 427 428function updateScheduleDate() { 429 const $scheduleAlert = $(".schedule-alert"); 430 431 if ($scheduleAlert.length === 0) { 432 return; 433 } 434 435 const $scheduleDate = $("#schedule-date"); 436 const dateValue = $scheduleDate.data("value"); 437 438 if (!dateValue) { 439 return; 440 } 441 442 const isoDateString = dateValue.split(" ").join("T") + "Z"; 443 const date = new Date(isoDateString); 444 445 if (!isNaN(date.getTime())) { 446 const localizedDate = date.toLocaleString("en-US"); 447 $scheduleDate.text(localizedDate); 448 } 449} 450 451function initializeAnimeId() { 452 453 if (animeId) return; 454 455 // Try to get anime ID from detail link // id="ani_detail" data-anime-id 456 const animeDetail = $('#ani_detail'); 457 if (animeDetail.length) { 458 animeId = animeDetail.data('anime-id'); 459 epId = animeDetail.data('id'); 460 } 461 462 463 464 // Fallback: data attribute 465 if (!animeId) { 466 return; 467 } 468 if (animeId) { 469 hydrateInitialWatchPage(); 470 } 471} 472 473function loadEpisodeList() { 474 if (!animeId) { 475 console.error('Anime ID not found'); 476 return; 477 } 478 479 const cacheKey = 'hi_episode_list_' + animeId; 480 const cachedResponse = hiCacheGet(cacheKey); 481 if (cachedResponse) { 482 handleEpisodeListResponse(cachedResponse); 483 return; 484 } 485 486 if (hiPendingRequests[cacheKey]) { 487 return; 488 } 489 490 $('#episodes-content').html(` 491 <div class="loading-relative loading-box loading-box-episodes"> 492 <div class="loading"> 493 <div class="span1"></div> 494 <div class="span2"></div> 495 <div class="span3"></div> 496 </div> 497 </div> 498 `); 499 500 hiFetchPublicJson(hiRestUrl('episode/list/' + animeId), { timeout: 250000, retries: 3 }) 501 .then((response) => { 502 if (response?.status) { 503 hiCacheSet(cacheKey, response); 504 } 505 handleEpisodeListResponse(response); 506 delete hiPendingRequests[cacheKey]; 507 }, (error) => { 508 console.error('AJAX error loading episode list:', error); 509 $('#episodes-content').html(`<div class="alert alert-danger">Error loading episodes: ${error.message || error}</div>`); 510 delete hiPendingRequests[cacheKey]; 511 }); 512 513 hiPendingRequests[cacheKey] = true; 514 515 516} 517 518let activeSourcesController = null; 519 520function loadSources(episodeId) { 521 if (!episodeId) { 522 console.error('Episode ID not found'); 523 return; 524 } 525 526 if (activeSourcesController) { 527 activeSourcesController.abort(); 528 activeSourcesController = null; 529 } 530 531 const cacheKey = 'hi_episode_servers_' + episodeId; 532 const cachedResponse = hiCacheGet(cacheKey); 533 if (cachedResponse) { 534 handleSourcesResponse(cachedResponse); 535 return; 536 } 537 538 if (hiPendingRequests[cacheKey]) { 539 return; 540 } 541 542 $('#servers-content, .player-frame').html(` 543 <div class="loading-relative loading-box"> 544 <div class="loading"> 545 <div class="span1"></div> 546 <div class="span2"></div> 547 <div class="span3"></div> 548 </div> 549 </div> 550 `); 551 552 const controller = window.AbortController ? new AbortController() : null; 553 activeSourcesController = controller; 554 555 const url = hiRestUrl('episode/servers/' + episodeId); 556 const fetchPromise = controller 557 ? fetch(url, { 558 method: 'GET', 559 credentials: 'omit', 560 cache: 'no-store', 561 signal: controller.signal,
562 headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } 563 }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }) 564 : hiFetchPublicJson(url, { timeout: 250000, retries: 3 }); 565 566 hiPendingRequests[cacheKey] = true; 567 568 fetchPromise.then((response) => { 569 if (response?.status && response?.html) { 570 hiCacheSet(cacheKey, response); 571 } 572 handleSourcesResponse(response); 573 delete hiPendingRequests[cacheKey]; 574 if (activeSourcesController === controller) activeSourcesController = null; 575 }, (error) => { 576 if (error?.name === 'AbortError') return; 577 console.error('AJAX error loading sources:', error); 578 $('#servers-content').html(`<div class="alert alert-danger">Error loading sources: ${error.message || error}</div>`); 579 delete hiPendingRequests[cacheKey]; 580 if (activeSourcesController === controller) activeSourcesController = null; 581 }); 582} 583 584 585function toggleAnimeName() { 586 $(".dynamic-name").each(function () { 587 var currentName = $(this).text(), 588 jName = $(this).data("jname"), 589 _this = $(this); 590 _this.animate( 591 { 592 opacity: 0, 593 }, 594 200, 595 function () { 596 if (jName.length > 0) { 597 _this.text(jName).animate( 598 { 599 opacity: 1, 600 }, 601 200 602 ); 603 _this.data("jname", currentName); 604 } 605 } 606 ); 607 }); 608} 609 610function nextEpisode() { 611 var e, t; 612 $(".ep-item.active").is(":last-child") ? (e = (t = $(".ep-page-item.active").next()).data("page"), 613 0 < t.length && (t.click(), 614 $("#episodes-page-" + e + " .ep-item").first().click())) : 0 < (t = $(".ep-item.active").next()).length && t.click() 615} 616 617function prevEpisode() { 618 var e, t; 619 $(".ep-item.active").is(":first-child") ? (e = (t = $(".ep-page-item.active").prev()).data("page"), 620 0 < t.length && (t.click(), 621 $("#episodes-page-" + e + " .ep-item").last().click())) : 0 < (t = $(".ep-item.active").prev()).length && t.click() 622} 623 624function startLoggingContinueWatching() { 625 if (watchingLogInterval) { 626 clearInterval(watchingLogInterval); 627 } 628 if (watchingLogTimeout) { 629 clearTimeout(watchingLogTimeout); 630 } 631 632 lastContinueWatchingKey = ''; 633 634 if (!isLoggedIn) { 635 return; 636 } 637 638 watchingLogTimeout = setTimeout(() => { 639 updateContinueWatching(animeId, epNumber, epId, currentVideoData.currentTime, currentVideoData.duration); 640 }, hiContinueWatchingInitialDelay); 641 642 watchingLogInterval = setInterval(() => { 643 if (animeId && epId && isLoggedIn && !document.hidden) { 644 updateContinueWatching(animeId, epNumber, epId, currentVideoData.currentTime, currentVideoData.duration); 645 } 646 }, hiContinueWatchingInterval); 647 648 // Update continue watching when user leaves the page 649 // window.addEventListener('beforeunload', function() { 650 // if (animeId && epId && isLoggedIn) { 651 // updateContinueWatching(animeId, epNumber, epId, currentVideoData.currentTime, currentVideoData.duration); 652 // } 653 // }); 654 655 // // Update continue watching when page becomes hidden 656 // document.addEventListener('visibilitychange', function() { 657 // if (document.hidden && animeId && epId && isLoggedIn) { 658 // updateContinueWatching(animeId, epNumber, epId, currentVideoData.currentTime, currentVideoData.duration); 659 // } 660 // }); 661} 662 663window.addEventListener('message', function (event) { 664 // Handle specific event data 665 if (event.data && typeof event.data === 'object') { 666 const data = event.data; 667 if (data.type === 'watching-log') { 668 currentVideoData.currentTime = data.currentTime || 0; 669 currentVideoData.duration = data.duration || 0; 670 if (data.currentTime >= data.duration && parseInt(getUserSetting('auto_next', '0'))) { 671 nextEpisode(); 672 } 673 return; 674 } 675 } 676}); 677 678function updateContinueWatching(animeId, episodeNumber, episodeId, time, duration) { 679 if (!isLoggedIn || !animeId || !episodeId || continueWatchingRequestInFlight) { 680 return; 681 } 682 683 time = parseInt(time, 10) || 0; 684 duration = parseInt(duration, 10) || 0; 685 686 if (duration > 0 && time < 5) { 687 return; 688 } 689 690 const continueWatchingKey = [ 691 animeId, 692 episodeNumber || 0, 693 episodeId, 694 Math.floor(time), 695 duration 696 ].join(':'); 697 if (continueWatchingKey === lastContinueWatchingKey) { 698 return; 699 } 700 701 lastContinueWatchingKey = continueWatchingKey; 702 continueWatchingRequestInFlight = true; 703 704 $.ajax({
705 url: user.rest_url + 'continue-watching', 706 type: 'POST', 707 contentType: 'application/json', 708 data: JSON.stringify({ 709 anime_id: animeId, 710 ep_id: episodeId, 711 ep: episodeNumber, 712 time: time, 713 duration: duration 714 }), 715 success: (response) => { 716 if (response && response.success) { 717 console.log('Continue watching updated successfully'); 718 } else { 719 console.error('Failed to update continue watching:', response); 720 } 721 }, 722 error: (xhr, status, error) => { 723 console.error('REST error updating continue watching:', error); 724 }, 725 complete: () => { 726 continueWatchingRequestInFlight = false; 727 } 728 }); 729}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.