feat: add some sources

This commit is contained in:
Ou 2024-10-21 01:38:58 +08:00
parent 3e425cbfce
commit f24a8834ea
26 changed files with 247 additions and 60 deletions

View File

@ -58,6 +58,7 @@
"react-dom": "^18.3.1",
"react-use": "^17.5.1",
"sonner": "^1.5.0",
"uncrypto": "^0.1.3",
"zod": "^3.23.8"
},
"devDependencies": {

3
pnpm-lock.yaml generated
View File

@ -113,6 +113,9 @@ importers:
sonner:
specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
uncrypto:
specifier: ^0.1.3
version: 0.1.3
zod:
specifier: ^3.23.8
version: 3.23.8

BIN
public/icons/cls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -20,7 +20,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
const cacheTable = useCache()
const now = Date.now()
if (cacheTable) {
if (process.env.NODE_ENV === "production" && cacheTable) {
if (process.env.INIT_TABLE !== "false") await cacheTable.init()
const cache = await cacheTable.get(id)
if (cache) {

View File

@ -2,7 +2,8 @@ import type { NewsItem } from "@shared/types"
import { load } from "cheerio"
const quick = defineSource(async () => {
const url = "https://www.36kr.com/newsflashes"
const baseURL = "https://www.36kr.com"
const url = `${baseURL}/newsflashes`
const response = await $fetch(url) as any
const $ = load(response)
const news: NewsItem[] = []
@ -15,7 +16,7 @@ const quick = defineSource(async () => {
const relativeDate = $el.find(".time")
if (url && title && relativeDate) {
news.push({
url: `https://www.36kr.com${url}`,
url: `${baseURL}${url}`,
title,
id: url,
extra: {

View File

@ -12,7 +12,6 @@ interface Res {
export default defineSource(async () => {
const res = await Promise.all(["zhongguo", "guandian", "gj"].map(k => $fetch(`https://china.cankaoxiaoxi.com/json/channel/${k}/list.json`) as Promise<Res>))
if (!res?.[0]?.list?.length) throw new Error("Cannot fetch data")
return res.map(k => k.list).flat().map(k => ({
id: k.data.id,
title: k.data.title,
@ -20,5 +19,5 @@ export default defineSource(async () => {
date: tranformToUTC(k.data.publishTime),
},
url: k.data.url,
})).sort((m, n) => m.extra.date < n.extra.date ? 1 : -1).slice(0, 30)
})).sort((m, n) => m.extra.date < n.extra.date ? 1 : -1)
})

View File

@ -0,0 +1,68 @@
import { getSearchParams } from "./utils"
interface Item {
id: number
title?: string
brief: string
shareurl: string
// need *1000
ctime: number
// 1
is_ad: number
}
interface TelegraphRes {
data: {
roll_data: Item[]
}
}
interface Depthes {
data: {
top_article: Item[]
depth_list: Item[]
}
}
const depth = defineSource(async () => {
const apiUrl = `https://www.cls.cn/v3/depth/home/assembled/1000`
const res: Depthes = await $fetch(apiUrl, {
query: await getSearchParams(),
})
return res.data.depth_list.sort((m, n) => n.ctime - m.ctime).map((k) => {
return {
id: k.id,
title: k.title || k.brief,
mobileUrl: k.shareurl,
extra: {
date: k.ctime * 1000,
},
url: `https://www.cls.cn/detail/${k.id}`,
}
})
})
// hot 失效
const telegraph = defineSource(async () => {
const apiUrl = `https://www.cls.cn/nodeapi/updateTelegraphList`
const res: TelegraphRes = await $fetch(apiUrl, {
query: await getSearchParams({ }),
})
return res.data.roll_data.filter(k => !k.is_ad).map((k) => {
return {
id: k.id,
title: k.title || k.brief,
mobileUrl: k.shareurl,
extra: {
date: k.ctime * 1000,
},
url: `https://www.cls.cn/detail/${k.id}`,
}
})
})
export default defineSource({
"cls": telegraph,
"cls-telegraph": telegraph,
"cls-depth": depth,
})

View File

@ -0,0 +1,13 @@
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/cls/utils.ts
const params = {
appName: "CailianpressWeb",
os: "web",
sv: "7.7.5",
}
export async function getSearchParams(moreParams?: any) {
const searchParams = new URLSearchParams({ ...params, ...moreParams })
searchParams.sort()
searchParams.append("sign", await md5(await myCrypto(searchParams.toString(), "SHA-1")))
return searchParams
}

View File

@ -23,9 +23,9 @@ interface Res {
export default defineSource(async () => {
const url = "https://api.coolapk.com/v6/page/dataList?url=%2Ffeed%2FstatList%3FcacheExpires%3D300%26statType%3Dday%26sortField%3Ddetailnum%26title%3D%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&title=%E4%BB%8A%E6%97%A5%E7%83%AD%E9%97%A8&subTitle=&page=1"
const r: Res = await $fetch(url, {
headers: genHeaders(),
headers: await genHeaders(),
})
if (!r.data || r.data.length === 0) throw new Error("Failed to fetch")
if (!r.data.length) throw new Error("Failed to fetch")
return r.data.filter(k => k.id).map(i => ({
id: i.id,
title: i.editor_title || load(i.message).text().split("\n")[0],

View File

@ -1,6 +1,5 @@
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/coolapk/utils.ts
import { Buffer } from "node:buffer"
import md5 from "md5"
function getRandomDEVICE_ID() {
const r = [10, 6, 6, 6, 14]
@ -8,22 +7,22 @@ function getRandomDEVICE_ID() {
return id.join("-")
}
function get_app_token() {
async function get_app_token() {
const DEVICE_ID = getRandomDEVICE_ID()
const now = Math.round(Date.now() / 1000)
const hex_now = `0x${now.toString(16)}`
const md5_now = md5(now.toString())
const md5_now = await md5(now.toString())
const s = `token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?${md5_now}$${DEVICE_ID}&com.coolapk.market`
const md5_s = md5(Buffer.from(s).toString("base64"))
const md5_s = await md5(Buffer.from(s).toString("base64"))
const token = md5_s + DEVICE_ID + hex_now
return token
}
export function genHeaders() {
export async function genHeaders() {
return {
"X-Requested-With": "XMLHttpRequest",
"X-App-Id": "com.coolapk.market",
"X-App-Token": get_app_token(),
"X-App-Token": await get_app_token(),
"X-Sdk-Int": "29",
"X-Sdk-Locale": "zh-CN",
"X-App-Version": "11.0",

View File

@ -1,6 +1,6 @@
interface Res {
data?: {
word_list?: {
data: {
word_list: {
sentence_id: string
word: string
event_time: string
@ -33,16 +33,11 @@ export default defineSource(async () => {
Cookie: `passport_csrf_token=${cookie}`,
},
})
if (!res?.data?.word_list || res.data.word_list.length === 0) throw new Error("Cannot fetch data")
return res.data.word_list
.slice(0, 30)
.map((k) => {
return {
id: k.sentence_id,
title: k.word,
extra: {
info: k.hot_value,
},
url: `https://www.douyin.com/hot/${k.sentence_id}`,
}
})

View File

@ -1,4 +1,4 @@
import type { SourceID } from "@shared/types"
import type { DisabledSourceID, SourceID } from "@shared/types"
import weibo from "./weibo"
import zaobao from "./zaobao"
import v2ex from "./v2ex"
@ -6,11 +6,12 @@ import ithome from "./ithome"
import zhihu from "./zhihu"
import cankaoxiaoxi from "./cankaoxiaoxi"
import coolapk from "./coolapk"
import sputniknewscn from "./sputniknewscn"
import kr36 from "./36kr"
import wallstreetcn from "./wallstreetcn"
import douyin from "./douyin"
import toutiao from "./toutiao"
import cls from "./cls"
import sputniknewscn from "./sputniknewscn"
import type { SourceGetter } from "#/types"
export const sourcesGetters = {
@ -22,8 +23,9 @@ export const sourcesGetters = {
coolapk,
cankaoxiaoxi,
sputniknewscn,
wallstreetcn,
...wallstreetcn,
douyin,
...cls,
toutiao,
...kr36,
} as Record<SourceID, SourceGetter>
} as Record<SourceID, SourceGetter> & Partial<Record<DisabledSourceID, SourceGetter>>

View File

@ -28,5 +28,4 @@ export default defineSource(async () => {
}
})
return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1)
.slice(0, 30)
})

View File

@ -1,9 +1,8 @@
import * as cheerio from "cheerio"
import type { NewsItem } from "@shared/types"
import { $fetch } from "ofetch"
export default defineSource(async () => {
const response = await $fetch("https://sputniknews.cn/services/widget/lenta/")
const response: any = await $fetch("https://sputniknews.cn/services/widget/lenta/")
const $ = cheerio.load(response)
const $items = $(".lenta__item")
const news: NewsItem[] = []
@ -24,5 +23,5 @@ export default defineSource(async () => {
})
}
})
return news.slice(0, 30)
return news
})

View File

@ -1,5 +1,5 @@
interface Res {
data?: {
data: {
ClusterIdStr: string
Title: string
HotValue: string
@ -12,16 +12,11 @@ interface Res {
export default defineSource(async () => {
const url = "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc"
const res: Res = await $fetch(url)
if (!res.data || res.data.length === 0) throw new Error("Cannot fetch data")
return res.data
.slice(0, 30)
.map((k) => {
return {
id: k.ClusterIdStr,
title: k.Title,
extra: {
info: k.HotValue,
},
url: `https://www.toutiao.com/trending/${k.ClusterIdStr}/`,
}
})

View File

@ -18,7 +18,6 @@ interface Res {
const share = defineSource(async () => {
const res = await Promise.all(["create", "ideas", "programmer", "share"].map(k => $fetch(`https://www.v2ex.com/feed/${k}.json`) as Promise< Res>))
if (!res?.[0]?.items?.length) throw new Error("Cannot fetch data")
return res.map(k => k.items).flat().map(k => ({
id: k.id,
title: k.title,

View File

@ -1,33 +1,87 @@
interface Res {
interface Item {
uri: string
id: number
title?: string
// ad
resource_type?: string
content_text: string
content_short: string
display_time: number
type?: string
}
interface LiveRes {
data: {
items: Item[]
}
}
interface NewsRes {
data: {
items: {
uri: string
id: number
title?: string
content_text: string
display_time: number
resource: Item
}[]
}
}
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/wallstreetcn/live.ts
export default defineSource(async () => {
const category = "global"
const apiRootUrl = "https://api-one.wallstcn.com"
const apiUrl = `${apiRootUrl}/apiv1/content/lives?channel=${category}-channel&limit=30`
interface HotRes {
data: {
day_items: Item[]
}
}
const res: Res = await $fetch(apiUrl)
if (!res?.data?.items || res.data.items.length === 0) throw new Error("Cannot fetch data")
// https://github.com/DIYgod/RSSHub/blob/master/lib/routes/wallstreetcn/live.ts
const live = defineSource(async () => {
const apiUrl = `https://api-one.wallstcn.com/apiv1/content/lives?channel=global-channel&limit=30`
const res: LiveRes = await $fetch(apiUrl)
return res.data.items
.slice(0, 30)
.map((k) => {
return {
id: k.id,
title: k.title || k.content_text,
extra: {
date: new Date(k.display_time * 1000).getTime(),
date: k.display_time * 1000,
},
url: k.uri,
}
})
})
const news = defineSource(async () => {
const apiUrl = `https://api-one.wallstcn.com/apiv1/content/information-flow?channel=global-channel&accept=article&limit=30`
const res: NewsRes = await $fetch(apiUrl)
return res.data.items
.filter(k => k.resource.resource_type !== "ad" && k.resource.type !== "live")
.map(({ resource: h }) => {
return {
id: h.id,
title: h.title || h.content_short,
extra: {
date: h.display_time * 1000,
},
url: h.uri,
}
})
})
const hot = defineSource(async () => {
const apiUrl = `https://api-one.wallstcn.com/apiv1/content/articles/hot?period=all`
const res: HotRes = await $fetch(apiUrl)
return res.data.day_items
.map((h) => {
return {
id: h.id,
title: h.title!,
url: h.uri,
}
})
})
export default defineSource({
"wallstreetcn": live,
"wallstreetcn-quick": live,
"wallstreetcn-news": news,
"wallstreetcn-hot": hot,
})

View File

@ -28,10 +28,8 @@ interface Res {
export default defineSource(async () => {
const url = "https://weibo.com/ajax/side/hotSearch"
const res: Res = await $fetch(url)
if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data")
return res.data.realtime
.filter(k => !k.is_ad)
.slice(0, 30)
.map((k) => {
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
return {

View File

@ -31,5 +31,4 @@ export default defineSource(async () => {
}
})
return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1)
.slice(0, 30)
})

View File

@ -22,7 +22,6 @@ interface Res {
export default defineSource(async () => {
const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=20&desktop=true"
const res: Res = await $fetch(url)
if (!res.data || res.data.length === 0) throw new Error("Cannot fetch data")
return res.data
.slice(0, 30)
.map((k) => {

24
server/utils/crypto.ts Normal file
View File

@ -0,0 +1,24 @@
import _md5 from "md5"
import { subtle as _ } from "uncrypto"
type T = typeof crypto.subtle
const subtle: T = _
export async function md5(s: string) {
try {
// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/
// cloudflare worker support md5
return await myCrypto(s, "MD5")
} catch {
return _md5(s)
}
}
type Algorithm = "MD5" | "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"
export async function myCrypto(s: string, algorithm: Algorithm) {
const sUint8 = new TextEncoder().encode(s)
const hashBuffer = await subtle.digest(algorithm, sUint8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("")
return hashHex
}

View File

@ -1,8 +1,8 @@
import type { SourceID } from "@shared/types"
import type { AllSourceID, SourceID } from "@shared/types"
import defu from "defu"
import type { FallbackResponse, RSSHubOption, RSSHubInfo as RSSHubResponse, SourceGetter, SourceOption } from "#/types"
type X = SourceGetter | Partial<Record<SourceID, SourceGetter>>
type X = SourceGetter | Partial<Record<AllSourceID, SourceGetter>>
export function defineSource<T extends X>(source: T): T {
return source
}

View File

@ -19,7 +19,7 @@ const originMetadata: Metadata = {
},
finance: {
name: "财经",
sources: ["wallstreetcn", "36kr-quick"],
sources: ["cls-telegraph", "cls-depth", "wallstreetcn", "wallstreetcn-hot", "wallstreetcn-news"],
},
focus: {
name: "关注",

View File

@ -52,16 +52,30 @@ export const originSources = {
},
"wallstreetcn": {
name: "华尔街见闻",
interval: Time.Fast,
type: "realtime",
color: "blue",
home: "https://wallstreetcn.com/",
title: "快讯",
sub: {
quick: {
type: "realtime",
interval: Time.Fast,
title: "实时快讯",
},
news: {
title: "最新资讯",
interval: Time.Common,
},
hot: {
title: "最热文章",
type: "hottest",
interval: Time.Common,
},
},
},
"36kr": {
name: "36氪",
type: "realtime",
color: "blue",
disable: true,
home: "https://36kr.com",
sub: {
quick: {
@ -115,6 +129,22 @@ export const originSources = {
interval: Time.Common,
home: "https://china.cankaoxiaoxi.com",
},
"cls": {
name: "财联社",
color: "red",
home: "https://www.cls.cn",
sub: {
telegraph: {
title: "电报",
interval: Time.Fast,
type: "realtime",
},
depth: {
title: "深度头条",
interval: Time.Common,
},
},
},
} as const satisfies Record<string, OriginSource>
export const sources = genSources()

View File

@ -15,6 +15,15 @@ export type SourceID = {
}[keyof SubSource] | Key : Key;
}[MainSourceID]
export type AllSourceID = {
[Key in MainSourceID]: ConstSources[Key] extends { sub?: infer SubSource } ? keyof {
// @ts-expect-error >_<
[SubKey in keyof SubSource as `${Key}-${SubKey}`]: never
} | Key : Key
}[MainSourceID]
export type DisabledSourceID = Exclude<SourceID, MainSourceID>
export type ColumnID = (typeof columnIds)[number]
export type Metadata = Record<ColumnID, Column>

View File

@ -74,6 +74,7 @@ export default defineConfig({
experimental: {
database: true,
},
sourceMap: false,
database: {
default: {
connector: isCF ? "cloudflare-d1" : "sqlite",