rsshub
Version:
Make RSS Great Again!
230 lines (229 loc) • 8.3 kB
JavaScript
import { t as rofetch } from "./ofetch-C3ts-Hud.mjs";
import { load } from "cheerio";
import { decodeHTML } from "entities";
//#region lib/routes/naver/search.ts
const route = {
path: "/search/:category/:keyword",
categories: ["other"],
example: "/naver/search/all/송소희",
parameters: {
category: {
description: "검색 카테고리. 기본값: all (통합검색)",
default: "all",
options: [
{
value: "all",
label: "통합검색"
},
{
value: "blog",
label: "블로그"
},
{
value: "cafe",
label: "카페"
},
{
value: "news",
label: "뉴스"
},
{
value: "video",
label: "동영상"
}
]
},
keyword: "검색 키워드"
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false
},
radar: [{
source: ["m.search.naver.com/search.naver"],
target: "/search/:category/:keyword"
}],
name: "검색",
maintainers: ["slowmande"],
handler
};
const CATEGORY_CONFIG = {
all: {
url: (keyword) => `https://m.search.naver.com/search.naver?ssc=tab.m.all&where=m&sm=mtb_opt&query=${encodeURIComponent(keyword)}&nso=so%3Add&nso_open=1`,
templateIds: [
"webItem",
"ugcItem",
"newsItem",
"videoItem"
]
},
blog: {
url: (keyword) => `https://m.search.naver.com/search.naver?ssc=tab.m_blog.all&sm=mtb_jum&query=${encodeURIComponent(keyword)}&nso=so%3Add`,
templateIds: ["ugcItem"]
},
cafe: {
url: (keyword) => `https://m.search.naver.com/search.naver?ssc=tab.m_cafe.all&sm=mtb_jum&query=${encodeURIComponent(keyword)}&nso=so%3Add`,
templateIds: [
"webItem",
"ugcItem",
"newsItem",
"videoItem"
]
},
news: {
url: (keyword) => `https://m.search.naver.com/search.naver?ssc=tab.m_news.all&where=m_news&sm=mtb_jum&query=${encodeURIComponent(keyword)}&nso=so%3Add`,
templateIds: ["newsItem"]
},
video: {
url: (keyword) => `https://m.search.naver.com/search.naver?ssc=tab.m_video.all&where=m_video&sm=mtb_jum&query=${encodeURIComponent(keyword)}&nso=so%3Add`,
templateIds: ["videoItem"]
}
};
const CATEGORY_NAMES = {
all: "통합검색",
blog: "블로그",
cafe: "카페",
news: "뉴스",
video: "동영상"
};
async function handler(ctx) {
const keyword = ctx.req.param("keyword");
const category = ctx.req.param("category") || "all";
const config = CATEGORY_CONFIG[category] || CATEGORY_CONFIG.all;
const url = config.url(keyword);
const response = await rofetch(url);
const items = category === "cafe" ? extractCafeItems(response) : config.templateIds.flatMap((tid) => extractItems(response, tid));
return {
title: `${keyword} - 네이버 ${CATEGORY_NAMES[category] || CATEGORY_NAMES.all}`,
description: `${keyword}의 네이버 ${CATEGORY_NAMES[category] || CATEGORY_NAMES.all} 검색 결과입니다.`,
link: url,
item: items
};
}
function extractItems(response, templateId) {
return response.split(`"templateId":"${templateId}"`).slice(0, -1).map((segment) => templateId === "videoItem" ? extractVideoItem(segment) : extractGenericItem(segment, templateId)).filter(Boolean);
}
function extractVideoItem(segment) {
const htmlMatch = segment.match(/"html":"((?:[^"\\]|\\.)*)"/);
const title = htmlMatch ? cleanText(htmlMatch[1]) : "";
const authorMatch = segment.match(/"authorHtml":"((?:[^"\\]|\\.)*)"/);
const author = authorMatch ? cleanText(authorMatch[1]) : "";
const links = segment.matchAll(/"href":"((?:[^"\\]|\\.)*)"/g).toArray().map((m) => m[1]);
const mediaDomains = /^(?:https?:\/\/)?(?:m\.youtube\.com|youtu\.be|www\.youtube\.com|www\.tiktok\.com|tv\.naver\.com|m\.blog\.naver\.com|m\.cafe\.naver\.com)\//;
const link = links.find((l) => mediaDomains.test(l)) || links[0] || "";
const timeText = segment.match(/"createdAt":"((?:[^"\\]|\\.)*)"/)?.[1] || "";
const viewMatch = segment.match(/"viewCount":"((?:[^"\\]|\\.)*)"/);
const viewCount = viewMatch ? cleanText(viewMatch[1]) : "";
const durationMatch = segment.match(/"playDuration":(\d+)/);
const duration = durationMatch ? `${durationMatch[1]}초` : "";
const hasVideoFields = segment.includes("\"videoPlayerType\"") || segment.includes("\"playDuration\"");
if (!title || !link || !timeText || !hasVideoFields) return null;
return buildVideoResult(title, link, viewCount, duration, author, parseKoreanRelativeTime(timeText));
}
function buildVideoResult(title, link, viewCount, duration, author, pubDate) {
return {
title,
link,
description: buildVideoDescription(viewCount, duration),
author: author || void 0,
...pubDate && { pubDate }
};
}
function buildVideoDescription(viewCount, duration) {
const parts = [];
if (viewCount) parts.push(`조회수: ${viewCount}`);
if (duration && duration !== "0초") parts.push(`재생시간: ${duration}`);
return parts.map((p) => `<p>${p}</p>`).join("");
}
function extractLink(segment, templateId) {
if (templateId === "webItem") return segment.match(/"href":"((?:[^"\\]|\\.)*)"/)?.[1] || "";
const hrefMatches = segment.matchAll(/"titleHref":"((?:[^"\\]|\\.)*)"/g).toArray();
if (hrefMatches.length > 0) return hrefMatches.at(-1)[1];
return segment.match(/"href":"((?:[^"\\]|\\.)*)"/)?.[1] || "";
}
function extractGenericItem(segment, templateId) {
const titles = segment.matchAll(/"title":"((?:[^"\\]|\\.)*)"/g).toArray().map((m) => cleanText(m[1]));
const title = titles.at(-1) || "";
const sourceName = titles.at(-2) || "";
const link = extractLink(segment, templateId);
let bodyText = "";
const bodyMatch = segment.match(/"bodyText":"((?:[^"\\]|\\.)*)"/) || segment.match(/"content":"((?:[^"\\]|\\.)*)"/);
if (bodyMatch) bodyText = cleanText(bodyMatch[1]);
if (!title || !link) return null;
if ([
"더보기",
"관련도순",
"최신순"
].includes(title)) return null;
return buildItemFromTemplate(segment, templateId, title, link, bodyText, sourceName);
}
function buildItemFromTemplate(segment, templateId, title, link, bodyText, sourceName) {
if (templateId === "ugcItem") {
const dateMatch = segment.match(/"createdDate":"([^"]*)"/);
if (dateMatch?.[1]) return {
title,
link,
description: bodyText,
author: sourceName || void 0,
pubDate: new Date(dateMatch[1])
};
return null;
}
let timeText = "";
if (templateId === "webItem") timeText = segment.match(/\[\{"text":"([^"]*)"\}/)?.[1] || "";
else timeText = segment.match(/"text":"([^"]*)"/)?.[1] || "";
const pubDate = parseKoreanRelativeTime(timeText);
return {
title,
link,
description: bodyText,
author: sourceName || void 0,
...pubDate && { pubDate }
};
}
function extractCafeItems(html) {
const $ = load(html);
return $("li.bx").toArray().map((el) => {
const $el = $(el);
const titleEl = $el.find(".title_link");
const title = titleEl.text().trim();
const link = titleEl.attr("href") || "";
const author = $el.find(".name").first().text().trim();
const timeText = $el.find(".sub").first().text().trim();
const descEl = $el.find(".dsc_link");
const description = descEl.length ? descEl.text().trim() : "";
if (!title || !link) return null;
const pubDate = parseKoreanRelativeTime(timeText);
return {
title,
link,
description,
author: author || void 0,
...pubDate && { pubDate }
};
}).filter((item) => item !== null);
}
function cleanText(text) {
return decodeHTML(text).replaceAll("<mark>", "").replaceAll("</mark>", "");
}
function parseKoreanRelativeTime(timeText) {
const now = /* @__PURE__ */ new Date();
if (!timeText) return;
const absDate = new Date(timeText);
if (!Number.isNaN(absDate.getTime())) return absDate;
const match = timeText.match(/(\d+)\s*(시간|[분일주]) 전|(\d+)분 이내|(\d+)시간 이내|방금/);
if (!match) return;
if (match[0] === "방금") return;
const num = Number(match[1] || match[3] || match[4] || "0");
const unit = match[2] || "";
if (unit.includes("분")) return /* @__PURE__ */ new Date(now.getTime() - num * 60 * 1e3);
if (unit.includes("시간")) return /* @__PURE__ */ new Date(now.getTime() - num * 60 * 60 * 1e3);
if (unit.includes("일")) return /* @__PURE__ */ new Date(now.getTime() - num * 24 * 60 * 60 * 1e3);
if (unit.includes("주")) return /* @__PURE__ */ new Date(now.getTime() - num * 7 * 24 * 60 * 60 * 1e3);
}
//#endregion
export { route };