(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:
${crawlStats.skippedReasons.missingFields > 0 ? `- Missing required fields: ${crawlStats.skippedReasons.missingFields}
` : ""}
${crawlStats.skippedReasons.missingEpisodes > 0 ? `- Missing episodes data: ${crawlStats.skippedReasons.missingEpisodes}
` : ""}
${crawlStats.skippedReasons.missingLinkEmbed > 0 ? `- Missing video links: ${crawlStats.skippedReasons.missingLinkEmbed}
` : ""}
${crawlStats.skippedReasons.alreadyExists > 0 ? `- Already exists: ${crawlStats.skippedReasons.alreadyExists}
` : ""}
${crawlStats.skippedReasons.jsonError > 0 ? `- JSON / server output errors: ${crawlStats.skippedReasons.jsonError}
` : ""}
${crawlStats.skippedReasons.other > 0 ? `- Other errors: ${crawlStats.skippedReasons.other}
` : ""}
` : ""}
`;
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);