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

View File

@ -1,16 +1,22 @@
import { Interval, TTL } from "@shared/consts"
import type { SourceResponse } from "@shared/types"
import { sources } from "@shared/data"
import type { SourceID, SourceResponse } from "@shared/types"
import { sources } from "@shared/sources"
import { sourcesFn } from "#/sources"
import { Cache } from "#/cache"
export default defineEventHandler(async (event): Promise<SourceResponse> => {
try {
const id = getRouterParam(event, "id") as keyof typeof sourcesFn
let id = getRouterParam(event, "id") as SourceID
const query = getQuery(event)
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 cacheStore = db ? new Cache(db) : undefined
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)
})

View File

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

View File

@ -1,99 +1,30 @@
import type { Metadata } from "./types"
export const sectionIds = ["focus", "social", "china", "world", "digital"] 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 sectionIds = ["focus", "social", "china", "world", "tech", "code"] as const
export const metadata: Metadata = {
focus: {
name: "关注",
sourceList: [],
sources: [],
},
social: {
name: "实时",
sourceList: ["douyin", "weibo", "36kr-quick", "wallstreetcn", "zaobao"],
sources: ["douyin", "weibo", "wallstreetcn", "ithome", "36kr"],
},
china: {
name: "国内",
sourceList: ["peopledaily", "toutiao"],
sources: ["peopledaily", "toutiao"],
},
world: {
name: "国外",
sourceList: [],
sources: ["aljazeeracn", "sputniknewscn", "zaobao"],
},
digital: {
name: "数码",
sourceList: [],
code: {
name: "代码",
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 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 {
name: string
sourceList: SourceID[]
sources: SourceID[]
}
export interface NewsItem {
@ -47,7 +78,7 @@ export interface RSSItem {
}
export interface CacheInfo {
id: SourceID
id: MainSourceID
data: NewsItem[]
updated: number
}

View File

@ -22,3 +22,7 @@ export function relativeTime(timestamp: string | number) {
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 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"
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
@ -30,7 +31,7 @@ export const currentSectionAtom = atom((get) => {
return {
id,
...metadata[id],
sourceList: get(focusSourcesAtom),
sources: get(focusSourcesAtom),
}
}
return {

View File

@ -24,7 +24,7 @@ function RefreshButton() {
const currentSection = useAtomValue(currentSectionAtom)
const setRefetchSource = useSetAtom(refetchSourcesAtom)
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 => ({
...prev,
...obj,
@ -33,7 +33,7 @@ function RefreshButton() {
const isFetching = useIsFetching({
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 { useAtom } from "jotai"
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 { ofetch } from "ofetch"
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
@ -111,8 +111,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
{sources[id].name}
</span>
</div>
{/* @ts-expect-error -_- */}
<span className="text-xs">{sources[id]?.type}</span>
<span className="text-xs">{sources[id]?.title}</span>
</div>
<OverlayScrollbarsComponent
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} />
))
}