pref: refactor source structure

This commit is contained in:
Ou 2024-10-08 20:49:19 +08:00
parent ad2ff17a0d
commit 060bb92de2
13 changed files with 202 additions and 100 deletions

View File

@ -4,7 +4,7 @@ import { join } from "node:path"
import { Buffer } from "node:buffer" import { Buffer } from "node:buffer"
import { getLogos } from "favicons-scraper" import { getLogos } from "favicons-scraper"
import { consola } from "consola" import { consola } from "consola"
import { sources } from "../shared/data" import { originSources } from "../shared/sources"
const projectDir = fileURLToPath(new URL("..", import.meta.url)) const projectDir = fileURLToPath(new URL("..", import.meta.url))
const iconsDir = join(projectDir, "public", "icons") const iconsDir = join(projectDir, "public", "icons")
@ -25,7 +25,7 @@ async function downloadImage(url: string, outputPath: string, id: string) {
async function main() { async function main() {
await Promise.all( await Promise.all(
Object.entries(sources).map(async ([id, source]) => { Object.entries(originSources).map(async ([id, source]) => {
try { try {
const icon = join(iconsDir, `${id.split("-")[0]}.png`) const icon = join(iconsDir, `${id.split("-")[0]}.png`)
if (fs.existsSync(icon)) { if (fs.existsSync(icon)) {

View File

@ -1,16 +1,22 @@
import { Interval, TTL } from "@shared/consts" import { Interval, TTL } from "@shared/consts"
import type { SourceResponse } from "@shared/types" import type { SourceID, SourceResponse } from "@shared/types"
import { sources } from "@shared/data" import { sources } from "@shared/sources"
import { sourcesFn } from "#/sources" import { sourcesFn } from "#/sources"
import { Cache } from "#/cache" import { Cache } from "#/cache"
export default defineEventHandler(async (event): Promise<SourceResponse> => { export default defineEventHandler(async (event): Promise<SourceResponse> => {
try { try {
const id = getRouterParam(event, "id") as keyof typeof sourcesFn let id = getRouterParam(event, "id") as SourceID
const query = getQuery(event) const query = getQuery(event)
const latest = query.latest !== undefined && query.latest !== "false" const latest = query.latest !== undefined && query.latest !== "false"
const isValid = (id: SourceID) => !id || !sources[id] || !sourcesFn[id]
if (isValid(id)) {
const redirectID = sources[id].redirect
if (redirectID) id = redirectID
if (isValid(id)) throw new Error("Invalid source id")
}
if (!id || !sources[id] || !sourcesFn[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() const now = Date.now()

View File

@ -27,6 +27,6 @@ export default defineSource(async () => {
} }
} }
}) })
return news.sort((m, n) => n.extra!.date > m.extra!.data ? 1 : -1) return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1)
.slice(0, 20) .slice(0, 20)
}) })

View File

@ -12,3 +12,5 @@ export function tranformToUTC(date: string, format?: string, timezone: string =
if (!format) return dayjs.tz(date, timezone).valueOf() if (!format) return dayjs.tz(date, timezone).valueOf()
return dayjs.tz(date, format, timezone).valueOf() return dayjs.tz(date, format, timezone).valueOf()
} }
export const day = dayjs

View File

@ -1,99 +1,30 @@
import type { Metadata } from "./types" import type { Metadata } from "./types"
export const sectionIds = ["focus", "social", "china", "world", "digital"] as const export const sectionIds = ["focus", "social", "china", "world", "tech", "code"] as const
export const sources = {
"wallstreetcn": {
name: "华尔街见闻",
home: "https://wallstreetcn.com/",
interval: 3 * 60 * 1000,
type: "快讯",
},
// "36kr": {
// name: "36氪",
// type: "人气榜",
// interval: 10,
// home: "https://36kr.com",
// },
"36kr-quick": {
name: "36氪",
type: "快讯",
interval: 3 * 60 * 1000,
home: "https://36kr.com",
},
"douyin": {
name: "抖音",
interval: 3 * 60 * 1000,
home: "https://www.douyin.com",
},
"hupu": {
name: "虎扑",
home: "https://hupu.com",
},
"zhihu": {
name: "知乎",
home: "https://www.zhihu.com",
},
"weibo": {
name: "微博",
type: "实时热搜",
interval: 1 * 60 * 1000,
home: "https://weibo.com",
},
"tieba": {
name: "百度贴吧",
home: "https://tieba.baidu.com",
},
"zaobao": {
name: "联合早报",
type: "实时新闻",
home: "https://www.zaobao.com",
},
"thepaper": {
name: "澎湃新闻",
home: "https://www.thepaper.cn",
},
"toutiao": {
name: "今日头条",
home: "https://www.toutiao.com",
},
"cankaoxiaoxi": {
name: "参考消息",
home: "http://www.cankaoxiaoxi.com",
},
"peopledaily": {
name: "人民日报",
home: "http://paper.people.com.cn",
},
} as const satisfies Record<string, {
name: string
type?: string
/**
*
*/
interval?: number
home: string
}>
export const metadata: Metadata = { export const metadata: Metadata = {
focus: { focus: {
name: "关注", name: "关注",
sourceList: [], sources: [],
}, },
social: { social: {
name: "实时", name: "实时",
sourceList: ["douyin", "weibo", "36kr-quick", "wallstreetcn", "zaobao"], sources: ["douyin", "weibo", "wallstreetcn", "ithome", "36kr"],
}, },
china: { china: {
name: "国内", name: "国内",
sourceList: ["peopledaily", "toutiao"], sources: ["peopledaily", "toutiao"],
}, },
world: { world: {
name: "国外", name: "国外",
sourceList: [], sources: ["aljazeeracn", "sputniknewscn", "zaobao"],
}, },
digital: { code: {
name: "数码", name: "代码",
sourceList: [], sources: ["v2ex"],
},
tech: {
name: "科技",
sources: ["ithome"],
}, },
} }

113
shared/sources.ts Normal file
View File

@ -0,0 +1,113 @@
import { typeSafeObjectFromEntries } from "./type.util"
import type { OriginSource, Source, SourceID } from "./types"
export const originSources = {
"v2ex": {
name: "V2EX",
home: "https://v2ex.com/",
},
"wallstreetcn": {
name: "华尔街见闻",
home: "https://wallstreetcn.com/",
title: "快讯",
},
"sputniknewscn": {
name: "俄罗斯卫星通讯社",
home: "https://sputniknews.cn",
},
"aljazeeracn": {
name: "半岛电视台",
interval: 30 * 60 * 1000,
home: "https://chinese.aljazeera.net",
},
"36kr": {
name: "36氪",
home: "https://36kr.com",
sub: {
quick: {
title: "快讯",
},
},
},
"douyin": {
name: "抖音",
home: "https://www.douyin.com",
},
"hupu": {
name: "虎扑",
home: "https://hupu.com",
},
"zhihu": {
name: "知乎",
home: "https://www.zhihu.com",
},
"weibo": {
name: "微博",
title: "实时热搜",
interval: 5 * 60 * 1000,
home: "https://weibo.com",
},
"tieba": {
name: "百度贴吧",
home: "https://tieba.baidu.com",
},
"zaobao": {
name: "联合早报",
home: "https://www.zaobao.com",
},
"thepaper": {
name: "澎湃新闻",
home: "https://www.thepaper.cn",
},
"toutiao": {
name: "今日头条",
home: "https://www.toutiao.com",
},
"cankaoxiaoxi": {
name: "参考消息",
home: "http://www.cankaoxiaoxi.com",
},
"ithome": {
name: "IT之家",
interval: 1000,
home: "https://www.ithome.com",
},
"peopledaily": {
name: "人民日报",
interval: 3 * 60 * 60 * 1000,
home: "http://paper.people.com.cn",
},
} as const satisfies Record<string, OriginSource>
export const sources = genSources()
function genSources() {
const _: [SourceID, Source][] = []
Object.entries(originSources).forEach(([id, source]: [any, OriginSource]) => {
if (source.sub && Object.keys(source.sub).length) {
Object.entries(source.sub).forEach(([subId, subSource], i) => {
if (i === 0) {
_.push([id, {
redirect: `${id}-${subId}`,
name: source.name,
interval: source.interval,
...subSource,
}] as [any, Source])
}
_.push([`${id}-${subId}`, {
name: source.name,
interval: source.interval,
...subSource,
}] as [any, Source])
})
} else {
_.push([id, {
name: source.name,
interval: source.interval,
title: source.title,
}])
}
})
return typeSafeObjectFromEntries(_)
}

15
shared/type.util.ts Normal file
View File

@ -0,0 +1,15 @@
export type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] }
export type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never
export type MaybePromise<T> = Promise<T> | T
export function typeSafeObjectFromEntries<
const T extends ReadonlyArray<readonly [PropertyKey, unknown]>,
>(entries: T): { [K in T[number]as K[0]]: K[1] } {
return Object.fromEntries(entries) as { [K in T[number]as K[0]]: K[1] }
}
export function typeSafeObjectEntries<T extends Record<PropertyKey, unknown>>(obj: T): { [K in keyof T]: [K, T[K]] }[keyof T][] {
return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]
}

View File

@ -1,12 +1,43 @@
import type { sectionIds, sources } from "./data" import type { sectionIds } from "./data"
import type { originSources } from "./sources"
type ConstSources = typeof originSources
type MainSourceID = keyof(ConstSources)
export type SourceID = {
[Key in MainSourceID]: ConstSources[Key] extends { sub?: infer SubType } ? keyof {
// @ts-expect-error >_<
[K in keyof SubType as `${Key}-${K}` ]: never
} | Key : Key;
}[MainSourceID]
export type SourceID = keyof(typeof sources)
export type SectionID = (typeof sectionIds)[number] export type SectionID = (typeof sectionIds)[number]
export type Metadata = Record<SectionID, Section> export type Metadata = Record<SectionID, Section>
export interface OriginSource {
name: string
title?: string
/**
*
*/
interval?: number
home: string
sub?: Record<string, {
title: string
interval?: number
}>
}
export interface Source {
name: string
title?: string
interval?: number
redirect?: SourceID
}
export interface Section { export interface Section {
name: string name: string
sourceList: SourceID[] sources: SourceID[]
} }
export interface NewsItem { export interface NewsItem {
@ -47,7 +78,7 @@ export interface RSSItem {
} }
export interface CacheInfo { export interface CacheInfo {
id: SourceID id: MainSourceID
data: NewsItem[] data: NewsItem[]
updated: number updated: number
} }

View File

@ -22,3 +22,7 @@ export function relativeTime(timestamp: string | number) {
return `${month}${day}` return `${month}${day}`
} }
} }
export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}

View File

@ -1,6 +1,7 @@
import { atom } from "jotai" import { atom } from "jotai"
import type { SectionID, SourceID } from "@shared/types" import type { SectionID, SourceID } from "@shared/types"
import { metadata, sources } from "@shared/data" import { metadata } from "@shared/data"
import { sources } from "@shared/sources"
import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage" import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage"
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => { export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
@ -30,7 +31,7 @@ export const currentSectionAtom = atom((get) => {
return { return {
id, id,
...metadata[id], ...metadata[id],
sourceList: get(focusSourcesAtom), sources: get(focusSourcesAtom),
} }
} }
return { return {

View File

@ -24,7 +24,7 @@ function RefreshButton() {
const currentSection = useAtomValue(currentSectionAtom) const currentSection = useAtomValue(currentSectionAtom)
const setRefetchSource = useSetAtom(refetchSourcesAtom) const setRefetchSource = useSetAtom(refetchSourcesAtom)
const refreshAll = useCallback(() => { const refreshAll = useCallback(() => {
const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()])) const obj = Object.fromEntries(currentSection.sources.map(id => [id, Date.now()]))
setRefetchSource(prev => ({ setRefetchSource(prev => ({
...prev, ...prev,
...obj, ...obj,
@ -33,7 +33,7 @@ function RefreshButton() {
const isFetching = useIsFetching({ const isFetching = useIsFetching({
predicate: (query) => { predicate: (query) => {
return currentSection.sourceList.includes(query.queryKey[0] as SourceID) return currentSection.sources.includes(query.queryKey[0] as SourceID)
}, },
}) })

View File

@ -6,7 +6,7 @@ import clsx from "clsx"
import { useInView } from "react-intersection-observer" import { useInView } from "react-intersection-observer"
import { useAtom } from "jotai" import { useAtom } from "jotai"
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react" import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
import { sources } from "@shared/data" import { sources } from "@shared/sources"
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities" import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
import { ofetch } from "ofetch" import { ofetch } from "ofetch"
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms" import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
@ -111,8 +111,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
{sources[id].name} {sources[id].name}
</span> </span>
</div> </div>
{/* @ts-expect-error -_- */} <span className="text-xs">{sources[id]?.title}</span>
<span className="text-xs">{sources[id]?.type}</span>
</div> </div>
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
defer defer

View File

@ -43,7 +43,7 @@ export function Section({ id }: { id: SectionID }) {
: ( : (
<> <>
{ {
metadata[id].sourceList.map(source => ( metadata[id].sources.map(source => (
<CardWrapper key={source} id={source} /> <CardWrapper key={source} id={source} />
)) ))
} }