mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 11:19:14 +08:00
pref: server end
This commit is contained in:
parent
9389f23ad7
commit
8ca82048de
@ -1,5 +1,4 @@
|
|||||||
import { TTL } from "@shared/consts"
|
import type { CacheInfo, NewsItem } from "@shared/types"
|
||||||
import type { CacheInfo } from "@shared/types"
|
|
||||||
import type { Database } from "db0"
|
import type { Database } from "db0"
|
||||||
|
|
||||||
export class Cache {
|
export class Cache {
|
||||||
@ -13,26 +12,25 @@ export class Cache {
|
|||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS cache (
|
CREATE TABLE IF NOT EXISTS cache (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
data TEXT,
|
|
||||||
updated INTEGER,
|
updated INTEGER,
|
||||||
expires INTEGER
|
data TEXT
|
||||||
);
|
);
|
||||||
`).run()
|
`).run()
|
||||||
console.log(`init: `, performance.now() - last)
|
console.log(`init: `, performance.now() - last)
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: any) {
|
async set(key: string, value: NewsItem[]) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const last = performance.now()
|
const last = performance.now()
|
||||||
await this.db.prepare(
|
await this.db.prepare(
|
||||||
`INSERT OR REPLACE INTO cache (id, data, updated, expires) VALUES (?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO cache (id, data, updated) VALUES (?, ?, ?)`,
|
||||||
).run(key, JSON.stringify(value), now, now + TTL)
|
).run(key, JSON.stringify(value), now)
|
||||||
console.log(`set ${key}: `, performance.now() - last)
|
console.log(`set ${key}: `, performance.now() - last)
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key: string): Promise<CacheInfo> {
|
async get(key: string): Promise<CacheInfo> {
|
||||||
const last = performance.now()
|
const last = performance.now()
|
||||||
const row: any = await this.db.prepare(`SELECT id, data, updated, expires FROM cache WHERE id = ?`).get(key)
|
const row: any = await this.db.prepare(`SELECT id, data, updated FROM cache WHERE id = ?`).get(key)
|
||||||
const r = row
|
const r = row
|
||||||
? {
|
? {
|
||||||
...row,
|
...row,
|
||||||
|
@ -1,50 +1,70 @@
|
|||||||
import { fallback, sources } from "#/sources"
|
import { Interval, TTL } from "@shared/consts"
|
||||||
|
import type { SourceResponse } from "@shared/types"
|
||||||
|
import { sources } from "@shared/data"
|
||||||
|
import { fallback, sourcesFn } from "#/sources"
|
||||||
import { Cache } from "#/cache"
|
import { Cache } from "#/cache"
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event): Promise<SourceResponse> => {
|
||||||
try {
|
try {
|
||||||
const id = getRouterParam(event, "id") as keyof typeof sources
|
const id = getRouterParam(event, "id") as keyof typeof sourcesFn
|
||||||
const query = getQuery(event)
|
const query = getQuery(event)
|
||||||
const latest = query.latest !== undefined && query.latest !== "false"
|
const latest = query.latest !== undefined && query.latest !== "false"
|
||||||
|
|
||||||
if (!id) throw new Error("Invalid source id")
|
if (!id || !sources[id]) throw new Error("Invalid source id")
|
||||||
const db = useDatabase()
|
const db = useDatabase()
|
||||||
const cacheStore = db ? new Cache(db) : undefined
|
const cacheStore = db ? new Cache(db) : undefined
|
||||||
|
const now = Date.now()
|
||||||
if (cacheStore) {
|
if (cacheStore) {
|
||||||
// await cacheStore.init()
|
// await cacheStore.init()
|
||||||
const cache = await cacheStore.get(id)
|
const cache = await cacheStore.get(id)
|
||||||
if (cache) {
|
if (cache) {
|
||||||
if (!latest && cache.expires > Date.now()) {
|
if (!latest && now - cache.updated < TTL) {
|
||||||
return {
|
return {
|
||||||
status: "cache",
|
status: "cache",
|
||||||
data: cache.data,
|
data: {
|
||||||
|
updatedTime: cache.updated,
|
||||||
|
items: cache.data,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else if (latest && Date.now() - cache.updated < 60 * 1000) {
|
} else if (latest) {
|
||||||
|
let interval = Interval
|
||||||
|
if ("interval" in sources[id]) interval = sources[id].interval as number
|
||||||
|
if (now - cache.updated < interval) {
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
data: cache.data,
|
data: {
|
||||||
|
updatedTime: now,
|
||||||
|
items: cache.data,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sources[id]) {
|
if (sourcesFn[id]) {
|
||||||
const last = performance.now()
|
const last = performance.now()
|
||||||
const data = await fallback(id)
|
const data = await sourcesFn[id]()
|
||||||
console.log(`fetch: ${id}`, performance.now() - last)
|
console.log(`fetch: ${id}`, performance.now() - last)
|
||||||
if (cacheStore) await cacheStore.set(id, data)
|
if (cacheStore) event.waitUntil(cacheStore.set(id, data))
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
data,
|
data: {
|
||||||
|
updatedTime: now,
|
||||||
|
items: data,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const last = performance.now()
|
const last = performance.now()
|
||||||
const data = await (await sources[id])()
|
const data = await fallback(id)
|
||||||
console.log(`fetch: ${id}`, performance.now() - last)
|
console.log(`fetch: ${id}`, performance.now() - last)
|
||||||
if (cacheStore) await cacheStore.set(id, data)
|
if (cacheStore) event.waitUntil(cacheStore.set(id, data))
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
data,
|
data: {
|
||||||
|
updatedTime: now,
|
||||||
|
items: data,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { SourceInfo } from "@shared/types"
|
import type { NewsItem } from "@shared/types"
|
||||||
|
|
||||||
export interface Res {
|
export interface Res {
|
||||||
code: number
|
code: number
|
||||||
@ -17,13 +17,11 @@ export interface Res {
|
|||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fallback(id: string): Promise<SourceInfo> {
|
export async function fallback(id: string): Promise<NewsItem[]> {
|
||||||
const url = `https://smzdk.top/api/${id}/new`
|
const url = `https://smzdk.top/api/${id}/new`
|
||||||
const res: Res = await $fetch(url)
|
const res: Res = await $fetch(url)
|
||||||
if (res.code !== 200 || !res.data) throw new Error(res.message)
|
if (res.code !== 200 || !res.data) throw new Error(res.message)
|
||||||
return {
|
return res.data.map(item => ({
|
||||||
updatedTime: res.updateTime,
|
|
||||||
items: res.data.map(item => ({
|
|
||||||
extra: {
|
extra: {
|
||||||
date: item.time,
|
date: item.time,
|
||||||
},
|
},
|
||||||
@ -31,6 +29,5 @@ export async function fallback(id: string): Promise<SourceInfo> {
|
|||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
mobileUrl: item.mobileUrl,
|
mobileUrl: item.mobileUrl,
|
||||||
})),
|
}))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { SourceID, SourceInfo } from "@shared/types"
|
import type { NewsItem, SourceID } from "@shared/types"
|
||||||
import peopledaily from "./peopledaily"
|
import peopledaily from "./peopledaily"
|
||||||
import weibo from "./weibo"
|
import weibo from "./weibo"
|
||||||
import zaobao from "./zaobao"
|
import zaobao from "./zaobao"
|
||||||
@ -8,11 +8,11 @@ import wallstreetcn from "./wallstreetcn"
|
|||||||
|
|
||||||
export { fallback } from "./fallback"
|
export { fallback } from "./fallback"
|
||||||
|
|
||||||
export const sources = {
|
export const sourcesFn = {
|
||||||
peopledaily,
|
peopledaily,
|
||||||
weibo,
|
weibo,
|
||||||
zaobao,
|
zaobao,
|
||||||
wallstreetcn,
|
wallstreetcn,
|
||||||
"36kr-quick": krQ,
|
"36kr-quick": krQ,
|
||||||
// "36kr": kr,
|
// "36kr": kr,
|
||||||
} as Record<SourceID, () => Promise<SourceInfo>>
|
} as Record<SourceID, () => Promise<NewsItem[]>>
|
||||||
|
@ -1,28 +1,22 @@
|
|||||||
import { RSSHubBase } from "@shared/consts"
|
import { RSSHubBase } from "@shared/consts"
|
||||||
import type { NewsItem, RSSHubInfo, SourceInfo } from "@shared/types"
|
import type { NewsItem, RSSHubInfo } from "@shared/types"
|
||||||
|
|
||||||
export function defineSource(source: () => Promise<NewsItem[]>): () => Promise<SourceInfo> {
|
export function defineSource(source: () => Promise<NewsItem[]>): () => Promise<NewsItem[]> {
|
||||||
return async () => ({
|
return source
|
||||||
updatedTime: Date.now(),
|
|
||||||
items: await source(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineRSSSource(url: string): () => Promise<SourceInfo> {
|
export function defineRSSSource(url: string): () => Promise<NewsItem[]> {
|
||||||
return async () => {
|
return async () => {
|
||||||
const data = await rss2json(url)
|
const data = await rss2json(url)
|
||||||
if (!data?.items.length) throw new Error("Cannot fetch data")
|
if (!data?.items.length) throw new Error("Cannot fetch data")
|
||||||
return {
|
return data.items.slice(0, 20).map(item => ({
|
||||||
updatedTime: data.updatedTime ?? Date.now(),
|
|
||||||
items: data.items.slice(0, 20).map(item => ({
|
|
||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.link,
|
url: item.link,
|
||||||
id: item.link,
|
id: item.link,
|
||||||
extra: {
|
extra: {
|
||||||
date: item.created,
|
date: item.created,
|
||||||
},
|
},
|
||||||
})),
|
}))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +26,7 @@ interface Option {
|
|||||||
// default: 20
|
// default: 20
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
export function defineRSSHubSource(route: string, option?: Option): () => Promise<SourceInfo> {
|
export function defineRSSHubSource(route: string, option?: Option): () => Promise<NewsItem[]> {
|
||||||
return async () => {
|
return async () => {
|
||||||
const url = new URL(route, RSSHubBase)
|
const url = new URL(route, RSSHubBase)
|
||||||
url.searchParams.set("format", "json")
|
url.searchParams.set("format", "json")
|
||||||
@ -45,16 +39,13 @@ export function defineRSSHubSource(route: string, option?: Option): () => Promis
|
|||||||
url.searchParams.set(key, value.toString())
|
url.searchParams.set(key, value.toString())
|
||||||
})
|
})
|
||||||
const data: RSSHubInfo = await $fetch(url)
|
const data: RSSHubInfo = await $fetch(url)
|
||||||
return {
|
return data.items.slice(0, 20).map(item => ({
|
||||||
updatedTime: Date.now(),
|
|
||||||
items: data.items.slice(0, 20).map(item => ({
|
|
||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
id: item.id ?? item.url,
|
id: item.id ?? item.url,
|
||||||
extra: {
|
extra: {
|
||||||
date: item.date_published,
|
date: item.date_published,
|
||||||
},
|
},
|
||||||
})),
|
}))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
export const TTL = 15 * 60 * 1000
|
|
||||||
/**
|
/**
|
||||||
* 默认刷新间隔,否则复用缓存
|
* 缓存过期时间
|
||||||
*/
|
*/
|
||||||
export const Interval = 30 * 60 * 1000
|
export const TTL = 30 * 60 * 1000
|
||||||
|
/**
|
||||||
|
* 默认刷新间隔
|
||||||
|
*/
|
||||||
|
export const Interval = 10 * 60 * 1000
|
||||||
|
|
||||||
export const RSSHubBase = "https://rsshub.rssforever.com"
|
export const RSSHubBase = "https://rsshub.rssforever.com"
|
||||||
// export const RSSHubBase = "https://rsshub.pseudoyu.com"
|
// export const RSSHubBase = "https://rsshub.pseudoyu.com"
|
||||||
|
@ -5,8 +5,8 @@ export const sectionIds = ["focus", "social", "china", "world", "digital"] as co
|
|||||||
export const sources = {
|
export const sources = {
|
||||||
"wallstreetcn": {
|
"wallstreetcn": {
|
||||||
name: "华尔街见闻",
|
name: "华尔街见闻",
|
||||||
interval: 10,
|
|
||||||
home: "https://wallstreetcn.com/",
|
home: "https://wallstreetcn.com/",
|
||||||
|
interval: 3 * 60 * 1000,
|
||||||
type: "快讯",
|
type: "快讯",
|
||||||
},
|
},
|
||||||
// "36kr": {
|
// "36kr": {
|
||||||
@ -18,12 +18,12 @@ export const sources = {
|
|||||||
"36kr-quick": {
|
"36kr-quick": {
|
||||||
name: "36氪",
|
name: "36氪",
|
||||||
type: "快讯",
|
type: "快讯",
|
||||||
interval: 3,
|
interval: 3 * 60 * 1000,
|
||||||
home: "https://36kr.com",
|
home: "https://36kr.com",
|
||||||
},
|
},
|
||||||
"douyin": {
|
"douyin": {
|
||||||
name: "抖音",
|
name: "抖音",
|
||||||
interval: 1,
|
interval: 3 * 60 * 1000,
|
||||||
home: "https://www.douyin.com",
|
home: "https://www.douyin.com",
|
||||||
},
|
},
|
||||||
"hupu": {
|
"hupu": {
|
||||||
@ -32,24 +32,21 @@ export const sources = {
|
|||||||
},
|
},
|
||||||
"zhihu": {
|
"zhihu": {
|
||||||
name: "知乎",
|
name: "知乎",
|
||||||
interval: 10,
|
|
||||||
home: "https://www.zhihu.com",
|
home: "https://www.zhihu.com",
|
||||||
},
|
},
|
||||||
"weibo": {
|
"weibo": {
|
||||||
name: "微博",
|
name: "微博",
|
||||||
type: "实时热搜",
|
type: "实时热搜",
|
||||||
interval: 1,
|
interval: 1 * 60 * 1000,
|
||||||
home: "https://weibo.com",
|
home: "https://weibo.com",
|
||||||
},
|
},
|
||||||
"tieba": {
|
"tieba": {
|
||||||
name: "百度贴吧",
|
name: "百度贴吧",
|
||||||
interval: 2,
|
|
||||||
home: "https://tieba.baidu.com",
|
home: "https://tieba.baidu.com",
|
||||||
},
|
},
|
||||||
"zaobao": {
|
"zaobao": {
|
||||||
name: "联合早报",
|
name: "联合早报",
|
||||||
type: "实时新闻",
|
type: "实时新闻",
|
||||||
interval: 10,
|
|
||||||
home: "https://www.zaobao.com",
|
home: "https://www.zaobao.com",
|
||||||
},
|
},
|
||||||
"thepaper": {
|
"thepaper": {
|
||||||
@ -58,7 +55,6 @@ export const sources = {
|
|||||||
},
|
},
|
||||||
"toutiao": {
|
"toutiao": {
|
||||||
name: "今日头条",
|
name: "今日头条",
|
||||||
interval: 2,
|
|
||||||
home: "https://www.toutiao.com",
|
home: "https://www.toutiao.com",
|
||||||
},
|
},
|
||||||
"cankaoxiaoxi": {
|
"cankaoxiaoxi": {
|
||||||
@ -73,13 +69,9 @@ export const sources = {
|
|||||||
name: string
|
name: string
|
||||||
type?: string
|
type?: string
|
||||||
/**
|
/**
|
||||||
* 分钟,刷新的间隔时间,复用缓存
|
* 刷新的间隔时间,复用缓存
|
||||||
*/
|
*/
|
||||||
interval?: number
|
interval?: number
|
||||||
/**
|
|
||||||
* 每天刷新一次
|
|
||||||
*/
|
|
||||||
once?: number
|
|
||||||
home: string
|
home: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export interface SourceInfo {
|
|||||||
items: NewsItem[]
|
items: NewsItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OResponse = {
|
export type SourceResponse = {
|
||||||
status: "success" | "cache"
|
status: "success" | "cache"
|
||||||
data: SourceInfo
|
data: SourceInfo
|
||||||
} | {
|
} | {
|
||||||
@ -48,9 +48,8 @@ export interface RSSItem {
|
|||||||
|
|
||||||
export interface CacheInfo {
|
export interface CacheInfo {
|
||||||
id: SourceID
|
id: SourceID
|
||||||
data: SourceInfo
|
data: NewsItem[]
|
||||||
updated: number
|
updated: number
|
||||||
expires: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RSSHubInfo {
|
export interface RSSHubInfo {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { NewsItem, SourceID, SourceInfo } from "@shared/types"
|
import type { NewsItem, SourceID, SourceInfo, SourceResponse } from "@shared/types"
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
||||||
import type { UseQueryResult } from "@tanstack/react-query"
|
import type { UseQueryResult } from "@tanstack/react-query"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
@ -71,11 +71,11 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
|||||||
if (Date.now() - _refetchTime < 1000) {
|
if (Date.now() - _refetchTime < 1000) {
|
||||||
url = `/api/${_id}?latest`
|
url = `/api/${_id}?latest`
|
||||||
}
|
}
|
||||||
const response = await fetch(url).then(res => res.json())
|
const response: SourceResponse = await fetch(url).then(res => res.json())
|
||||||
if (response.status === "error") {
|
if (response.status === "error") {
|
||||||
throw new Error(response.message)
|
throw new Error(response.message)
|
||||||
} else {
|
} else {
|
||||||
return response.data as SourceInfo
|
return response.data
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// refetch 时显示原有的数据
|
// refetch 时显示原有的数据
|
||||||
|
Loading…
x
Reference in New Issue
Block a user