diff --git a/src/libpoketube/init/pages-404-and-main.js b/src/libpoketube/init/pages-404-and-main.js index bfd150eb..7ea81846 100644 --- a/src/libpoketube/init/pages-404-and-main.js +++ b/src/libpoketube/init/pages-404-and-main.js @@ -148,87 +148,100 @@ function getJson(str) { module.exports = function (app, config, renderTemplate) { app.get("/app", async function (req, res) { - const { fetch } = await import("undici"); + const { fetch } = await import("undici"); - let tab = ""; - if (req.query.tab) { - tab = `/?type=${capitalizeFirstLetter(req.query.tab)}`; - } - - const invtrend = await fetch(`${config.invapi}/trending${tab}`, { - headers: { "User-Agent": config.useragent }, - }); - const t = getJson(await invtrend.text()); - - const invpopular = await fetch( - `https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1/popular`, - { - headers: { "User-Agent": config.useragent }, - } - ); - const p = getJson(await invpopular.text()); -let j = null; - -try { - // Accept multiple query aliases; trim to avoid spaces-only values. - const query = - (typeof req.query.mobilesearch === "string" && req.query.mobilesearch.trim()) ?? - (typeof req.query.query === "string" && req.query.query.trim()) ?? - (typeof req.query.q === "string" && req.query.q.trim()) ?? - ""; - - // Use nullish coalescing so "0" is NOT overwritten. - const continuation = (req.query.continuation ?? "1").toString(); - - if (query) { - const searchUrl = `${config.invapi}/search?q=${encodeURIComponent(query)}&page=${encodeURIComponent(continuation)}`; - - const res = await fetch(searchUrl, { - headers: { "User-Agent": config.useragent }, - }); - - if (!res.ok) { - // Don’t leave j as null; store error info to inspect upstream. - j = { error: true, status: res.status, statusText: res.statusText }; - console.error("[mobilesearch] HTTP error", j, "URL:", searchUrl); - } else { - // If the endpoint returns JSON, prefer res.json(); otherwise parse text -> getJson. - // Try JSON first; fall back to text parsing. - let data; - const ct = res.headers.get("content-type") || ""; - if (ct.includes("application/json")) { - data = await res.json(); - } else { - const txt = await res.text(); - data = await Promise.resolve(getJson(txt)); - } - - j = (data ?? null); - if (j === null) { - j = { error: true, reason: "Parsed data is null/undefined" }; - console.error("[mobilesearch] getJson produced null/undefined. URL:", searchUrl); - } - } - } else { - j = { error: true, reason: "Missing 'mobilesearch' (or q/query) parameter" }; - console.warn("[mobilesearch] Missing query parameter"); + let tab = ""; + if (req.query.tab) { + tab = `/?type=${capitalizeFirstLetter(req.query.tab)}`; } -} catch (err) { - j = { error: true, reason: "Exception", message: String(err && err.message || err) }; - console.error("[mobilesearch] Exception:", err); -} - renderTemplate(res, req, "discover.ejs", { - tab: req.query.tab, - isMobile: req.useragent.isMobile, - p, - mobilesearch: req.query.mobilesearch, - inv: t, - turntomins, - continuation: req.query.continuation, - j, - }); + const invtrend = await fetch(`${config.invapi}/trending${tab}`, { + headers: { "User-Agent": config.useragent }, }); + const t = getJson(await invtrend.text()); + + const invpopular = await fetch( + `https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1/popular`, + { headers: { "User-Agent": config.useragent } } + ); + const p = getJson(await invpopular.text()); + + let j = { results: [], meta: {} }; + + // Small helper to coerce various payload shapes into { results: [] } + const normalizeSearchData = (data) => { + if (!data) return { results: [] }; + if (Array.isArray(data)) return { results: data }; + if (Array.isArray(data.results)) return { results: data.results, meta: data.meta || {} }; + if (Array.isArray(data.items)) return { results: data.items, meta: data.meta || {} }; + if (Array.isArray(data.videos)) return { results: data.videos, meta: data.meta || {} }; + return { results: [], meta: { note: "unrecognized search payload shape" } }; + }; + + try { + // Accept multiple query aliases; trim to avoid spaces-only values. + const query = + (typeof req.query.mobilesearch === "string" && req.query.mobilesearch.trim()) ?? + (typeof req.query.query === "string" && req.query.query.trim()) ?? + (typeof req.query.q === "string" && req.query.q.trim()) ?? + ""; + + // Keep "0" valid + const continuation = (req.query.continuation ?? "1").toString(); + + if (query) { + const searchUrl = `${config.invapi}/search?q=${encodeURIComponent(query)}&page=${encodeURIComponent(continuation)}`; + + const res = await fetch(searchUrl, { + headers: { "User-Agent": config.useragent }, + }); + + if (!res.ok) { + j = { + results: [], + error: true, + meta: { status: res.status, statusText: res.statusText, url: searchUrl }, + }; + console.error("[mobilesearch] HTTP error", j.meta); + } else { + const ct = res.headers.get("content-type") || ""; + let data; + if (ct.includes("application/json")) { + data = await res.json(); + } else { + const txt = await res.text(); + data = await Promise.resolve(getJson(txt)); + } + j = normalizeSearchData(data); + } + } else { + j = { results: [], error: true, meta: { reason: "missing query" } }; + console.warn("[mobilesearch] Missing query parameter (mobilesearch/q/query)"); + } + + // expose continuation back to the template even if not provided + j.meta = { ...(j.meta || {}), continuation }; + } catch (err) { + j = { + results: [], + error: true, + meta: { reason: "exception", message: String(err && err.message || err) }, + }; + console.error("[mobilesearch] Exception:", err); + } + + renderTemplate(res, req, "discover.ejs", { + tab: req.query.tab, + isMobile: req.useragent.isMobile, + p, + mobilesearch: req.query.mobilesearch, + inv: t, + turntomins, + continuation: req.query.continuation, + j, + }); +}); + app.get("/:v*?", async function (req, res) { const uaos = req.useragent.os;