(function ($) { "use strict"; // ------------------------------------------------------------------------- // SAFE JSON PARSER // Strips any stray PHP warnings / BOM / whitespace that appears before or // after the actual JSON payload, then parses. On failure it logs the exact // character position so you can trace the bad field immediately. // ------------------------------------------------------------------------- function safeParseResponse(raw, context) { if (raw === null || raw === undefined) { console.error("[" + context + "] Response is null/undefined"); return null; } // 1. Convert to string just in case jQuery already parsed it if (typeof raw === "object") return raw; var str = String(raw); // 2. Strip UTF-8 BOM if present if (str.charCodeAt(0) === 0xFEFF) { str = str.slice(1); } // 3. Check for Cloudflare/Server 5xx HTML errors early if (str.indexOf("(.*?)<\/title>/i); var title = titleMatch ? titleMatch[1] : "Server Error"; console.error("[" + context + "] Error Title:", title); return null; } // 4. Strip any PHP notice/warning lines that leak before the JSON. // JSON must start with { or [ — drop everything before that. var firstBrace = -1; for (var i = 0; i < str.length; i++) { if (str[i] === "{" || str[i] === "[") { firstBrace = i; break; } } if (firstBrace === -1) { console.error("[" + context + "] No JSON object found in response."); console.error("[" + context + "] Raw response:", str.substring(0, 500)); return null; } if (firstBrace > 0) { console.warn("[" + context + "] Stripped " + firstBrace + " leading chars (PHP output?):", str.substring(0, firstBrace)); str = str.slice(firstBrace); } // 5. Strip anything after the last } or ] var lastBrace = Math.max(str.lastIndexOf("}"), str.lastIndexOf("]")); if (lastBrace !== -1 && lastBrace < str.length - 1) { console.warn("[" + context + "] Stripped trailing chars after position " + lastBrace); str = str.slice(0, lastBrace + 1); } // 6. Parse try { return JSON.parse(str); } catch (e) { var posMatch = e.message.match(/position (\d+)/i); var pos = posMatch ? parseInt(posMatch[1], 10) : NaN; console.error("[" + context + "] JSON.parse failed:", e.message); if (!isNaN(pos)) { console.error("[" + context + "] ~50 chars around error position " + pos + ":", JSON.stringify(str.substring(Math.max(0, pos - 50), pos + 50))); } console.error("[" + context + "] Full cleaned string (first 2000 chars):", str.substring(0, 2000)); return null; } } // ------------------------------------------------------------------------- // MAIN PLUGIN CODE // ------------------------------------------------------------------------- $(function () { // -- DOM elements ------------------------------------------------------ const buttonCheckApi = $("#api-check"); const buttonRollCrawl = $("#roll-crawl"); const buttonUpdateCrawl = $("#update-crawl"); const buttonFullCrawl = $("#full-crawl"); const buttonPageFromTo = $("#page-from-to"); const buttonSelectedCrawl = $("#selected-crawl"); const buttonPauseCrawl = $("#pause-crawl"); const buttonResumeCrawl = $("#resume-crawl"); const buttonOneCrawl = $("#onemovie-crawl"); const alertBox = $("#alert-box"); const moviesListDiv = $("#movies-list"); const divCurrentPage = $("#current-page-crawl"); const inputPageFrom = $("input[name=page-from]"); const inputPageTo = $("input[name=page-to]"); const contentSection = $("#content"); const statsSection = $("#stats-section"); const crawlingSection = $("#crawling-section"); // -- State variables --------------------------------------------------- let latestPageList = []; let fullPageList = []; let pageFromToList = []; let tempPageList = []; let tempMoviesId = []; let tempMovies = []; let tempHour = ""; let apiUrl = ""; let isStopByUser = false; let maxPageTo = 0; let currentMovie = null; // -- Crawl statistics -------------------------------------------------- let crawlStats = {}; function resetCrawlStats() { crawlStats = { totalMovies: 0, processedMovies:0, skippedMovies: 0, createdPosts: 0, updatedPosts: 0, failedMovies: 0, skippedReasons: { missingFields: 0, missingEpisodes: 0, missingLinkEmbed:0, alreadyExists: 0, jsonError: 0, other: 0 } }; } resetCrawlStats(); function updateCrawlStatsDisplay() { const statsHtml = `
?? Crawl Statistics
Total Movies: ${crawlStats.totalMovies}
Processed: ${crawlStats.processedMovies}
Skipped: ${crawlStats.skippedMovies}
Created: ${crawlStats.createdPosts}
Updated: ${crawlStats.updatedPosts}
Failed: ${crawlStats.failedMovies}
${crawlStats.skippedMovies > 0 ? `
Skipped Reasons:
` : ""}
`; let statsContainer = document.getElementById("crawl-stats-container"); if (statsContainer) { statsContainer.innerHTML = statsHtml; } else { statsContainer = document.createElement("div"); statsContainer.id = "crawl-stats-container"; statsContainer.innerHTML = statsHtml; const mld = document.getElementById("movies-list"); if (mld) mld.insertBefore(statsContainer, mld.firstChild); } } // -- UI helpers -------------------------------------------------------- function showSection(section) { section.removeClass("hidden"); } function hideSection(section) { section.addClass("hidden"); } function showAlert(msg, type) { alertBox.removeClass().addClass("alert alert-" + (type || "info")); alertBox.html(msg).removeClass("hidden"); } function hideAlert() { alertBox.addClass("hidden"); } function updateButtonStates(enabled) { buttonRollCrawl.prop("disabled", !enabled); buttonUpdateCrawl.prop("disabled", !enabled); buttonFullCrawl.prop("disabled", !enabled); buttonPageFromTo.prop("disabled", !enabled); buttonSelectedCrawl.prop("disabled", !enabled); if (!enabled) { buttonPauseCrawl.prop("disabled", false); buttonResumeCrawl.prop("disabled", true); } else { buttonPauseCrawl.prop("disabled", true); buttonResumeCrawl.prop("disabled", true); } } function setLoadingState(button, loading) { const spinner = button.find(".spinner-border"); if (loading) { spinner.removeClass("hidden"); button.prop("disabled", true); } else { spinner.addClass("hidden"); button.prop("disabled", false); } } // -- Bootstrap modal helper -------------------------------------------- let globalModalInstance = null; function showModal(type, message) { const map = { danger: { title: "Error", cls: "bg-danger text-white" }, warning: { title: "Warning", cls: "bg-warning text-dark" }, success: { title: "Success", cls: "bg-success text-white" }, info: { title: "Info", cls: "bg-info text-white" } }; const cfg = map[type] || { title: "Message", cls: "" }; const modalTitle = document.getElementById("globalMessageModalLabel"); const modalBody = document.getElementById("globalMessageModalBody"); if (modalTitle) { modalTitle.textContent = cfg.title; modalTitle.className = "modal-title " + cfg.cls; } if (modalBody) { const isMobile = window.innerWidth <= 768; modalBody.innerHTML = message + (isMobile ? '
?? Tap anywhere to close
' : ""); } const modalEl = document.getElementById("globalMessageModal"); if (globalModalInstance) { globalModalInstance.dispose(); } else { const existing = bootstrap.Modal.getInstance(modalEl); if (existing) existing.dispose(); } globalModalInstance = new bootstrap.Modal(modalEl, { backdrop: false, keyboard: true, focus: true }); globalModalInstance.show(); modalEl.addEventListener("hidden.bs.modal", function () { document.body.classList.remove("modal-open"); document.body.style.overflow = ""; document.body.style.paddingRight = ""; }, { once: true }); const bodyEl = document.getElementById("globalMessageModalBody"); if (bodyEl) { bodyEl.onclick = function (e) { if (e.target.closest(".btn") || e.target.closest(".btn-close")) return; if (globalModalInstance) globalModalInstance.hide(); }; } } function showError(message) { showModal("danger", "? " + message); } // -- Final-completion helper (used in two places) ---------------------- function onCrawlComplete() { updateCrawlStatsDisplay(); showModal("success", `?? Crawling completed successfully!

Final Statistics:
• Total Movies: ${crawlStats.totalMovies}
• Processed: ${crawlStats.processedMovies}
• Skipped: ${crawlStats.skippedMovies}
• Created: ${crawlStats.createdPosts}
• Updated: ${crawlStats.updatedPosts}
• Failed: ${crawlStats.failedMovies}

${crawlStats.skippedMovies > 0 ? "Check the statistics above for details about skipped movies." : ""}` ); hideSection(moviesListDiv); updateButtonStates(true); buttonSelectedCrawl.html("Collect Selected Pages"); buttonUpdateCrawl.html("Collect Recent Videos"); buttonFullCrawl.html("Collect All Videos"); buttonRollCrawl.html("Randomize Order"); tempPageList = []; pageFromToList = []; tempHour = ""; tempMoviesId = []; tempMovies = []; currentMovie = null; resetCrawlStats(); } // -- Init -------------------------------------------------------------- updateButtonStates(false); // --------------------------------------------------------------------- // CHECK API // --------------------------------------------------------------------- buttonCheckApi.click(function (e) { e.preventDefault(); const generatedUrl = $("#generated-api-url").text(); const inputUrl = $("#jsonapi-url").val(); apiUrl = generatedUrl || inputUrl; if (!apiUrl) { showError("Please select API Provider or enter API URL."); return false; } $("#movies-table tbody").html(""); hideSection(moviesListDiv); hideSection(contentSection); hideSection(statsSection); hideSection(crawlingSection); hideAlert(); setLoadingState($(this), true); $(this).html(` Checking API...`); $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_crawler_api", api: apiUrl }, success: function (response) { setLoadingState(buttonCheckApi, false); buttonCheckApi.html("Check API Connection"); const data = safeParseResponse(response, "buttonCheckApi"); if (!data) { showError("Server returned an invalid response. Check console for details."); return; } if (data.code > 1) { showModal("danger", `? ${data.message}`); } else { showModal("success", `? API connection successful! Found ${data.total} total pages.`); showSection(contentSection); showSection(statsSection); showSection(crawlingSection); const crawlSection = document.getElementById("crawling-section"); if (crawlSection) crawlSection.scrollIntoView({ behavior: "auto", block: "start" }); $("#last-page").html(data.last_page); $("#per-page").html(data.per_page); $("#movies-total").html(data.total); updateButtonStates(true); latestPageList = data.latest_list_page; fullPageList = data.full_list_page; maxPageTo = data.last_page; inputPageFrom.val(1); inputPageTo.val(Math.min(10, data.last_page)); } }, error: function () { setLoadingState(buttonCheckApi, false); buttonCheckApi.html("Check API Connection"); showError("Failed to connect to API. Please check your URL and try again."); } }); }); // --------------------------------------------------------------------- // CRAWL ONE MOVIE // --------------------------------------------------------------------- if (buttonOneCrawl.length) { buttonOneCrawl.click(function (e) { e.preventDefault(); const oneLink = $("#onemovie-link").val(); if (!oneLink) { showError("Movie link cannot be empty."); return false; } setLoadingState($(this), true); $(this).html(` Collecting...`); $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_crawler_link_api", api: oneLink }, success: function (response) { const data = safeParseResponse(response, "buttonOneCrawl"); if (!data) { showError("Invalid server response. Check console for details."); } else if (data.code > 1) { showModal("danger", `? ${data.message}`); } else { showModal("success", `? ${data.message}`); } setLoadingState(buttonOneCrawl, false); buttonOneCrawl.html("Collect immediately"); }, error: function () { showError("Something went wrong. Please try again."); setLoadingState(buttonOneCrawl, false); buttonOneCrawl.html("Collect immediately"); } }); }); } // --------------------------------------------------------------------- // PAGE FROM–TO // --------------------------------------------------------------------- buttonPageFromTo.click(function (e) { e.preventDefault(); hideAlert(); const pageFrom = parseInt(inputPageFrom.val(), 10); const pageTo = parseInt(inputPageTo.val(), 10); if (pageTo > maxPageTo || pageFrom > pageTo || pageFrom <= 0 || pageTo <= 0 || isNaN(pageFrom) || isNaN(pageTo)) { showError(`Invalid page range. Please enter valid page numbers between 1 and ${maxPageTo}.`); return; } const pages = []; for (let i = pageFrom; i <= pageTo; i++) pages.push(i); pageFromToList = pages; showModal("success", `? Page range set: ${pageFrom} to ${pageTo} (${pages.length} pages)`); }); // --------------------------------------------------------------------- // SELECTED CRAWL // --------------------------------------------------------------------- buttonSelectedCrawl.click(function (e) { e.preventDefault(); if (pageFromToList.length === 0) { showModal("warning", "Please set a page range first."); const s = document.getElementById("content"); if (s) s.scrollIntoView({ behavior: "auto", block: "start" }); return; } showSection(moviesListDiv); setLoadingState($(this), true); $(this).html(` Starting...`); crawl_movies_page(pageFromToList, "", null); // Pass null for options to use defaults }); // --------------------------------------------------------------------- // UPDATE / FULL / ROLL crawl triggers // --------------------------------------------------------------------- buttonUpdateCrawl.click(function (e) { e.preventDefault(); showSection(moviesListDiv); setLoadingState($(this), true); $(this).html(` Starting...`); crawl_movies_page(latestPageList, "", null); }); buttonFullCrawl.click(function (e) { e.preventDefault(); showSection(moviesListDiv); setLoadingState($(this), true); $(this).html(` Starting...`); crawl_movies_page(fullPageList, "", null); }); buttonRollCrawl.click(function (e) { e.preventDefault(); setLoadingState($(this), true); $(this).html(` Randomizing...`); setTimeout(() => { fullPageList.sort(() => 0.5 - Math.random()); latestPageList.sort(() => 0.5 - Math.random()); pageFromToList.sort(() => 0.5 - Math.random()); setLoadingState($(this), false); $(this).html("? Order Randomized"); showModal("success", "? Crawling order has been randomized successfully!"); }, 1000); }); // --------------------------------------------------------------------- // PAUSE / RESUME // --------------------------------------------------------------------- buttonPauseCrawl.click(function (e) { e.preventDefault(); isStopByUser = true; buttonResumeCrawl.prop("disabled", false); buttonPauseCrawl.prop("disabled", true); showModal("info", "?? Crawling paused. Click 'Resume' to continue."); }); buttonResumeCrawl.click(function (e) { e.preventDefault(); isStopByUser = false; buttonPauseCrawl.prop("disabled", false); buttonResumeCrawl.prop("disabled", true); showModal("success", "?? Crawling resumed."); if (currentMovie) { crawl_movie_by_id(tempMoviesId, tempMovies, false, currentMovie, tempCrawlOptions); } else if (tempMoviesId && tempMoviesId.length > 0) { crawl_movie_by_id(tempMoviesId, tempMovies, false, null, tempCrawlOptions); } else if (tempPageList && tempPageList.length > 0) { crawl_movies_page(tempPageList, "", tempCrawlOptions); } }); // --------------------------------------------------------------------- // DEBUG LOG TOGGLE // --------------------------------------------------------------------- $("#view-debug-log").click(function () { const dbg = $("#debug-info"); if (dbg.is(":visible")) { dbg.hide(); $(this).html("?? Debug Log"); } else { dbg.show(); $(this).html("?? Hide Debug"); } }); // --------------------------------------------------------------------- // MULTI-LINK IMPORT // --------------------------------------------------------------------- $("#multi-link-import-btn").click(function (e) { e.preventDefault(); const linksText = $("#multi-link-textarea").val().trim(); const forceUpdate = $("#multi-link-force-update").is(":checked"); if (!linksText) { showError("Please paste at least one API link."); return; } const links = linksText.split("\n").map(l => l.trim()).filter(l => l.length > 0); if (links.length === 0) { showError("No valid links found."); return; } const invalidLinks = links.filter(l => !l.startsWith("http")); if (invalidLinks.length > 0) { showError(`Found ${invalidLinks.length} invalid links. All links must start with http/https.`); return; } setLoadingState($(this), true); $(this).html(` Preparing...`); let moviesToCrawl = []; const totalLinks = links.length; const fetchLinkDetails = (index) => { if (index >= totalLinks) { setLoadingState($("#multi-link-import-btn"), false); $("#multi-link-import-btn").html("Import Videos"); if (moviesToCrawl.length === 0) { showError("Could not fetch details for any of the provided links."); return; } showModal("success", `? Found ${moviesToCrawl.length} videos. Starting import...`); showSection(moviesListDiv); resetCrawlStats(); crawlStats.totalMovies = moviesToCrawl.length; updateCrawlStatsDisplay(); const opts = { forceUpdate: forceUpdate, crawlImage: true }; crawl_movie_by_id([...moviesToCrawl], moviesToCrawl, true, null, opts); return; } $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_get_movies_page", api: links[index], param: "" }, success: function (response) { const data = safeParseResponse(response, "fetchLinkDetails[" + index + "]"); if (data && data.code === 1 && data.movies && data.movies.length > 0) { moviesToCrawl = moviesToCrawl.concat(data.movies); } fetchLinkDetails(index + 1); }, error: function () { console.error("Error fetching link: " + links[index]); fetchLinkDetails(index + 1); } }); }; fetchLinkDetails(0); }); // --------------------------------------------------------------------- // REPLACE URL / DOMAIN // --------------------------------------------------------------------- $("#replace-url-btn").click(function (e) { e.preventDefault(); const oldUrl = $("#old-url-input").val().trim(); const newUrl = $("#new-url-input").val().trim(); const btn = $(this); let selectedMetaKeys = []; $(".replace-meta-checkbox:checked").each(function () { selectedMetaKeys.push($(this).val()); }); if (selectedMetaKeys.length === 0) { showError("Please select at least one meta field to apply the replacement to."); return; } if (!oldUrl) { showError("Please enter the old domain/url to replace."); return; } if (oldUrl === newUrl) { showError("The old and new domains are exactly the same."); return; } if (!confirm(`Are you absolutely sure you want to replace ALL occurrences of "${oldUrl}" with "${newUrl}" in the selected fields? This action cannot be easily undone.`)) return; setLoadingState(btn, true); $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_replace_url", old_url: oldUrl, new_url: newUrl, meta_keys: selectedMetaKeys }, success: function (response) { setLoadingState(btn, false); const data = safeParseResponse(response, "replace-url-btn"); if (!data) { showModal("danger", "? Invalid response from server."); return; } if (data.code > 1) { showModal("danger", `? ${data.message}`); $("#replace-url-result").html(`
${data.message}
`); } else { showModal("success", `? ${data.message}`); $("#replace-url-result").html(`
${data.message}
`); $("#old-url-input").val(""); $("#new-url-input").val(""); } }, error: function () { setLoadingState(btn, false); showModal("danger", "? Request failed. Please try again."); } }); }); // Variable to store options for resume functionality let tempCrawlOptions = null; // --------------------------------------------------------------------- // CRAWL MOVIES PAGE // --------------------------------------------------------------------- const crawl_movies_page = (pagesList, customUrl = null, options = null) => { if (isStopByUser) return; // Default options const opts = options || { forceUpdate: $("#forceUpdate").is(":checked"), crawlImage: $("#crawlImage").is(":checked") }; tempCrawlOptions = opts; // Save for resume if (pagesList.length === 0) { onCrawlComplete(); return; } const currentPage = pagesList.shift(); tempPageList = pagesList; const targetUrl = customUrl || apiUrl; $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_get_movies_page", api: targetUrl, param: `pg=${currentPage}` }, beforeSend: function () { if (isStopByUser) return false; // abort request showSection(divCurrentPage); $("#current-page-crawl h3").html(`?? Crawling Page ${currentPage}`); updateButtonStates(false); buttonResumeCrawl.prop("disabled", true); showSection(moviesListDiv); }, success: function (response) { if (isStopByUser) return; const data = safeParseResponse(response, "crawl_movies_page[" + currentPage + "]"); if (!data) { showModal("danger", "? Server returned invalid JSON on page " + currentPage + ". Check console."); // Skip page and continue crawl_movies_page(tempPageList, customUrl, opts); return; } if (data.code > 1) { showModal("danger", `? ${data.message}`); // Don't stop completely on one page error, maybe just log or skip // crawl_movies_page(tempPageList, customUrl, opts); } else { const avList = data.movies; crawlStats.totalMovies += avList.length; updateCrawlStatsDisplay(); crawl_movie_by_id(avList, data.movies, true, null, opts); } }, error: function (xhr) { console.error("Error fetching page " + currentPage, xhr.status); // Skip page on error and continue crawl_movies_page(tempPageList, customUrl, opts); } }); }; // --------------------------------------------------------------------- // CRAWL MOVIE BY ID // --------------------------------------------------------------------- const crawl_movie_by_id = (avList, movies, showList = true, resumeMovie = null, options = null) => { if (isStopByUser) return; // Default options handling const opts = options || { forceUpdate: false, crawlImage: true }; tempCrawlOptions = opts; // Update global resume options if (!avList || avList.length === 0) { $("#movies-table tbody").html(""); currentMovie = null; if (tempPageList && tempPageList.length > 0) { crawl_movies_page(tempPageList, "", opts); } else { onCrawlComplete(); } return; } if (showList) display_movies(movies); let av; if (resumeMovie) { av = resumeMovie; } else { av = avList.shift(); } tempMoviesId = avList; tempMovies = movies; currentMovie = av; if (av == null) { $("#movies-table tbody").html(""); currentMovie = null; if (tempPageList && tempPageList.length > 0) crawl_movies_page(tempPageList, "", opts); return; } // Safely stringify the movie — guard against circular refs or bad chars let avJson; try { avJson = JSON.stringify(av); } catch (jsonErr) { console.error("[crawl_movie_by_id] Failed to JSON.stringify movie:", jsonErr, av); crawlStats.failedMovies++; crawlStats.skippedMovies++; crawlStats.skippedReasons.jsonError++; update_movies("movie-" + av.id, "? Client-side JSON error"); updateCrawlStatsDisplay(); currentMovie = null; crawl_movie_by_id(avList, movies, false, null, opts); return; } $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_crawl_by_id", crawl_image: opts.crawlImage ? 1 : 0, force_update: opts.forceUpdate ? 1 : 0, av: avJson }, beforeSend: function () { if (isStopByUser) return false; // abort request }, success: function (response) { if (isStopByUser) return; const data = safeParseResponse(response, "crawl_movie_by_id[" + av.id + "]"); crawlStats.processedMovies++; if (!data) { // Server returned garbage — count as json error, skip, continue crawlStats.failedMovies++; crawlStats.skippedMovies++; crawlStats.skippedReasons.jsonError++; update_movies("movie-" + av.id, "? Invalid server response (JSON error)"); updateCrawlStatsDisplay(); currentMovie = null; crawl_movie_by_id(avList, movies, false, null, opts); return; } if (data.code > 1) { if (data.skipped) { crawlStats.skippedMovies++; crawlStats.skippedReasons.alreadyExists++; update_movies("movie-" + av.id, "?? Skipped (Exists)"); } else { crawlStats.failedMovies++; const msg = (data.message || "").toLowerCase(); if (msg.includes("missing required fields")) crawlStats.skippedReasons.missingFields++; else if (msg.includes("missing episodes data")) crawlStats.skippedReasons.missingEpisodes++; else if (msg.includes("missing link_embed")) crawlStats.skippedReasons.missingLinkEmbed++; else if (msg.includes("json model")) crawlStats.skippedReasons.jsonError++; else crawlStats.skippedReasons.other++; crawlStats.skippedMovies++; update_movies("movie-" + av.id, "? " + data.message); } } else { const msg = (data.message || "").toLowerCase(); if (msg.includes("successfully created")) crawlStats.createdPosts++; else if (msg.includes("successfully updated")) crawlStats.updatedPosts++; update_movies("movie-" + av.id, "? " + data.message); } updateCrawlStatsDisplay(); currentMovie = null; crawl_movie_by_id(avList, movies, false, null, opts); }, error: function () { crawlStats.failedMovies++; crawlStats.skippedMovies++; crawlStats.skippedReasons.other++; update_movies("movie-" + av.id, "? Request failed"); updateCrawlStatsDisplay(); currentMovie = null; crawl_movie_by_id(avList, movies, false, null, opts); } }); }; // --------------------------------------------------------------------- // DISPLAY / UPDATE MOVIE ROWS // --------------------------------------------------------------------- const display_movies = (movies) => { let trHTML = ""; $.each(movies, function (idx, movie) { trHTML += ` ${idx + 1} ${movie.name} Processing... `; }); $("#movies-table tbody").append(trHTML); }; const update_movies = (id, message) => { message = message || "100%"; const doneIcon = ` `; if (message.includes("?")) { $("#" + id + " td:last-child").html(doneIcon + " " + message); } else if (message.includes("?")) { $("#" + id + " td:last-child").html(`? ${message.replace("? ", "")}`); } else { $("#" + id + " td:last-child").html(message); } }; // Expose to global scope for inline scripts window.display_movies = display_movies; window.crawl_movie_by_id = crawl_movie_by_id; // --------------------------------------------------------------------- // CRONJOB SETTINGS // --------------------------------------------------------------------- let cronjobSettings = {}; function loadCronjobSettings() { $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_get_cronjob_settings" }, success: function (response) { const data = safeParseResponse(response, "loadCronjobSettings"); if (data && data.success) { cronjobSettings = data.data; updateCronjobUI(); } }, error: function () { console.log("Failed to load cronjob settings"); } }); } function updateCronjobUI() { $("#cronjob-enabled-status").text(cronjobSettings.enabled ? "Enabled" : "Disabled"); $("#cronjob-next-run").text(cronjobSettings.next_run_formatted || "Disabled"); $("#cronjob-last-run").text(cronjobSettings.last_run_formatted || "Never"); $("#cronjob-total-runs").text(cronjobSettings.total_runs || 0); $("#cronjob-last-status").text(cronjobSettings.last_status || "No status available"); $("#cronjob-enabled").prop("checked", cronjobSettings.enabled); const apiUrlVal = cronjobSettings.api_url || "https://avdbapi.com/api.php/provide/vod/?ac=detail"; $("#cronjob-api-url").val(apiUrlVal); let detectedProvider = "custom"; if (apiUrlVal.includes("avdbapi.com")) detectedProvider = "avdbapi"; else if (apiUrlVal.includes("xvidapi.com")) detectedProvider = "xvidapi"; const categoryMatch = apiUrlVal.match(/[?&]t=(\d+)/); const detectedCategory = categoryMatch ? categoryMatch[1] : ""; $("#cronjob-api-provider").val(detectedProvider).trigger("change"); if (detectedCategory && detectedProvider !== "custom") { setTimeout(() => { $("#cronjob-api-category").val(detectedCategory).trigger("change"); }, 100); } $(`input[name="crawling_method"][value="${cronjobSettings.crawling_method || "recent"}"]`).prop("checked", true); $("#selected-pages-from").val(cronjobSettings.selected_pages_from || 1); $("#selected-pages-to").val(cronjobSettings.selected_pages_to || 5); $("#cronjob-schedule").val(cronjobSettings.schedule || "twicedaily"); $("#cronjob-download-images").prop("checked", cronjobSettings.download_images === true); $("#cronjob-force-update").prop("checked", cronjobSettings.force_update === true); if (cronjobSettings.enabled) { $("#cronjob-settings").show(); } else { $("#cronjob-settings").hide(); } if (cronjobSettings.crawling_method === "selected") { $("#selected-pages-range").show(); } else { $("#selected-pages-range").hide(); } } loadCronjobSettings(); // Manual API config detection function updateManualApiUI() { const currentApiUrl = $("#jsonapi-url").val() || ""; let detectedProvider = ""; if (currentApiUrl.includes("avdbapi.com")) detectedProvider = "avdbapi"; else if (currentApiUrl.includes("xvidapi.com")) detectedProvider = "xvidapi"; else if (currentApiUrl) detectedProvider = "custom"; const categoryMatch = currentApiUrl.match(/[?&]t=(\d+)/); const detectedCategory = categoryMatch ? categoryMatch[1] : ""; $("#api-provider").val(detectedProvider).trigger("change"); if (detectedCategory && detectedProvider !== "custom") { setTimeout(() => { $("#api-category").val(detectedCategory).trigger("change"); }, 100); } } updateManualApiUI(); // Cronjob enabled toggle $("#cronjob-enabled").change(function () { if ($(this).is(":checked")) { $("#cronjob-settings").show(); } else { $("#cronjob-settings").hide(); } }); // Crawling method radio $("input[name='crawling_method']").change(function () { if ($(this).val() === "selected") { $("#selected-pages-range").show(); } else { $("#selected-pages-range").hide(); } }); // -- Save cronjob settings --------------------------------------------- $("#save-cronjob-settings").click(function () { const generatedCronjobUrl = $("#cronjob-generated-api-url").text(); const inputCronjobUrl = $("#cronjob-api-url").val(); const cronjobApiUrl = generatedCronjobUrl || inputCronjobUrl; const settings = { enabled: $("#cronjob-enabled").is(":checked"), api_url: cronjobApiUrl, crawling_method: $("input[name='crawling_method']:checked").val(), selected_pages_from: $("#selected-pages-from").val(), selected_pages_to: $("#selected-pages-to").val(), schedule: $("#cronjob-schedule").val(), download_images: $("#cronjob-download-images").is(":checked"), force_update: $("#cronjob-force-update").is(":checked") }; setLoadingState($("#save-cronjob-settings"), true); $("#save-cronjob-settings").html("Saving..."); $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_save_cronjob_settings", ...settings }, success: function (response) { setLoadingState($("#save-cronjob-settings"), false); $("#save-cronjob-settings").html("Save Settings"); const data = safeParseResponse(response, "save-cronjob-settings"); if (!data) { showError("Invalid response from server."); return; } if (data.success) { showModal("success", "? " + data.data.message); loadCronjobSettings(); } else { showError(data.data ? data.data.message : "Failed to save settings"); } }, error: function () { setLoadingState($("#save-cronjob-settings"), false); $("#save-cronjob-settings").html("Save Settings"); showError("Failed to save cronjob settings. Please try again."); } }); }); // -- Test cronjob (OPTIMIZED) ------------------------------------------ $("#test-cronjob").click(function (e) { e.preventDefault(); // 1. Gather Settings from UI const generatedCronjobUrl = $("#cronjob-generated-api-url").text(); const inputCronjobUrl = $("#cronjob-api-url").val(); const cronjobApiUrl = generatedCronjobUrl || inputCronjobUrl; if (!cronjobApiUrl) { showError("Please enter a Cronjob API URL first."); return; } const method = $("input[name='crawling_method']:checked").val(); const crawlImage = $("#cronjob-download-images").is(":checked"); const forceUpdate = $("#cronjob-force-update").is(":checked"); // 2. UI Setup showSection(moviesListDiv); setLoadingState($(this), true); $(this).html(` Checking API...`); // 3. First, we need to check the API to get Total Pages (just like the manual Check button) $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_crawler_api", api: cronjobApiUrl }, context: this, // keep button context success: function (response) { const data = safeParseResponse(response, "test-cronjob-check"); if (!data || data.code > 1) { showError("Test failed: Could not connect to API or invalid response."); setLoadingState($(this), false); $(this).html("Test Now"); return; } // 4. Build Page List based on Cronjob Method let pagesList = []; const total = data.last_page; if (method === "recent") { // Recent: Use latest_list_page logic if returned by server, else default to 1-5 pagesList = data.latest_list_page || Array.from({length: Math.min(total, 5)}, (_, i) => i + 1); } else if (method === "selected") { const pFrom = parseInt($("#selected-pages-from").val(), 10); const pTo = parseInt($("#selected-pages-to").val(), 10); if (pFrom > 0 && pTo >= pFrom) { for (let i = pFrom; i <= Math.min(pTo, total); i++) pagesList.push(i); } else { showError("Invalid page range in selected method."); setLoadingState($(this), false); $(this).html("Test Now"); return; } } else { // Full: Crawl ALL pages (Warning: might be huge) pagesList = Array.from({length: total}, (_, i) => i + 1); } if (pagesList.length === 0) { showError("No pages found to crawl."); setLoadingState($(this), false); $(this).html("Test Now"); return; } // 5. Start the JS-based Batch Crawler showModal("success", `? Test Started! Found ${total} pages. Processing ${pagesList.length} pages based on settings.`); setLoadingState($(this), false); $(this).html("Running..."); resetCrawlStats(); updateButtonStates(false); // Lock buttons const opts = { forceUpdate: forceUpdate, crawlImage: crawlImage }; // Use the optimized recursive function crawl_movies_page(pagesList, cronjobApiUrl, opts); }, error: function () { showError("Failed to check API for Cronjob Test."); setLoadingState($(this), false); $(this).html("Test Now"); } }); }); // -- Stop cronjob ------------------------------------------------------ $("#stop-cronjob").click(function () { if (!confirm("Are you sure you want to stop all scheduled cronjobs? This will immediately halt all auto-crawling.")) return; setLoadingState($(this), true); $(this).html("Stopping..."); $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_stop_cronjob" }, success: function (response) { setLoadingState($("#stop-cronjob"), false); $("#stop-cronjob").html("Stop Now"); const data = safeParseResponse(response, "stop-cronjob"); if (!data) { showError("Invalid response from server."); return; } if (data.success) { showModal("success", "? " + data.data.message); loadCronjobSettings(); } else { showError(data.data ? data.data.message : "Failed to stop cronjob"); } }, error: function () { setLoadingState($("#stop-cronjob"), false); $("#stop-cronjob").html("Stop Now"); showError("Failed to stop cronjob. Please try again."); } }); }); // -- Clear cronjob lock ------------------------------------------------ $("#clear-cronjob-lock").click(function () { setLoadingState($(this), true); $(this).html("Clearing..."); $.ajax({ type: "POST", url: ajaxurl, data: { action: "avdbapi_clear_cronjob_lock" }, success: function (response) { setLoadingState($("#clear-cronjob-lock"), false); $("#clear-cronjob-lock").html("Clear Lock"); const data = safeParseResponse(response, "clear-cronjob-lock"); if (!data) { showError("Invalid response from server."); return; } if (data.success) { showModal("success", "? " + data.data.message); loadCronjobSettings(); } else { showError(data.data ? data.data.message : "Failed to clear lock"); } }, error: function () { setLoadingState($("#clear-cronjob-lock"), false); $("#clear-cronjob-lock").html("Clear Lock"); showError("Failed to clear lock. Please try again."); } }); }); }); // end $(function) })(jQuery);