2020-06-06 09:33:59 +08:00

299 lines
8.9 KiB
TypeScript

import Parser = require("@yang991178/rss-parser")
import * as db from "../db"
import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils"
import { RSSItem, insertItems } from "./item"
import { SourceGroup } from "./group"
import { saveSettings } from "./app"
export class RSSSource {
sid: number
url: string
iconurl: string
name: string
description: string
useProxy: boolean
constructor(url: string, name: string = null) {
this.url = url
this.name = name
this.useProxy = false
}
async fetchMetaData(parser: Parser) {
let feed = await parser.parseURL(this.url)
if (!this.name && feed.title) this.name = feed.title.trim()
this.description = feed.description
let domain = this.url.split("/").slice(0, 3).join("/")
let f: string = null
try {
f = await faviconPromise(domain)
if (f === null) f = domain + "/favicon.ico"
let result = await fetch(f)
if (result.status == 200 && result.headers.has("Content-Type")
&& result.headers.get("Content-Type").startsWith("image")) {
this.iconurl = f
}
} finally {
return feed
}
}
private static checkItem(source: RSSSource, item: Parser.Item, db: Nedb<RSSItem>): Promise<RSSItem> {
return new Promise<RSSItem>((resolve, reject) => {
let i = new RSSItem(item, source)
db.findOne({
source: i.source,
title: i.title,
date: i.date
},
(err, doc) => {
if (err) {
reject(err)
} else if (doc === null) {
resolve(i)
} else {
resolve(null)
}
})
})
}
static checkItems(source: RSSSource, items: Parser.Item[], db: Nedb<RSSItem>): Promise<RSSItem[]> {
return new Promise<RSSItem[]>((resolve, reject) => {
let p = new Array<Promise<RSSItem>>()
for (let item of items) {
p.push(this.checkItem(source, item, db))
}
Promise.all(p).then(values => {
resolve(values.filter(v => v != null))
}).catch(e => { reject(e) })
})
}
static async fetchItems(source: RSSSource, parser: Parser, db: Nedb<RSSItem>) {
let feed = await parser.parseURL(source.url)
return await this.checkItems(source, feed.items, db)
}
}
export type SourceState = {
[sid: number]: RSSSource
}
export const INIT_SOURCES = "INIT_SOURCES"
export const ADD_SOURCE = "ADD_SOURCE"
export const UPDATE_SOURCE = "UPDATE_SOURCE"
export const DELETE_SOURCE = "DELETE_SOURCE"
interface InitSourcesAction {
type: typeof INIT_SOURCES
status: ActionStatus
sources?: RSSSource[]
err?
}
interface AddSourceAction {
type: typeof ADD_SOURCE
status: ActionStatus
batch: boolean
source?: RSSSource
err?
}
interface UpdateSourceAction {
type: typeof UPDATE_SOURCE
source: RSSSource
}
interface DeleteSourceAction {
type: typeof DELETE_SOURCE,
source: RSSSource
}
export type SourceActionTypes = InitSourcesAction | AddSourceAction | UpdateSourceAction | DeleteSourceAction
export function initSourcesRequest(): SourceActionTypes {
return {
type: INIT_SOURCES,
status: ActionStatus.Request
}
}
export function initSourcesSuccess(sources: RSSSource[]): SourceActionTypes {
return {
type: INIT_SOURCES,
status: ActionStatus.Success,
sources: sources
}
}
export function initSourcesFailure(err): SourceActionTypes {
return {
type: INIT_SOURCES,
status: ActionStatus.Failure,
err: err
}
}
export function initSources(): AppThunk<Promise<void>> {
return (dispatch) => {
dispatch(initSourcesRequest())
return new Promise<void>((resolve, reject) => {
db.sdb.find({}).sort({ sid: 1 }).exec((err, docs) => {
if (err) {
dispatch(initSourcesFailure(err))
reject(err)
} else {
dispatch(initSourcesSuccess(docs))
resolve()
}
})
})
}
}
export function addSourceRequest(batch: boolean): SourceActionTypes {
return {
type: ADD_SOURCE,
batch: batch,
status: ActionStatus.Request
}
}
export function addSourceSuccess(source: RSSSource, batch: boolean): SourceActionTypes {
return {
type: ADD_SOURCE,
batch: batch,
status: ActionStatus.Success,
source: source
}
}
export function addSourceFailure(err, batch: boolean): SourceActionTypes {
return {
type: ADD_SOURCE,
batch: batch,
status: ActionStatus.Failure,
err: err
}
}
export function addSource(url: string, name: string = null, batch = false): AppThunk<Promise<number>> {
return (dispatch, getState) => {
let app = getState().app
if (app.sourceInit && !app.fetchingItems) {
dispatch(addSourceRequest(batch))
let source = new RSSSource(url, name)
return source.fetchMetaData(rssParser)
.then(feed => {
let sids = Object.values(getState().sources).map(s => s.sid)
source.sid = Math.max(...sids, -1) + 1
return new Promise<number>((resolve, reject) => {
db.sdb.insert(source, (err) => {
if (err) {
reject(err)
} else {
dispatch(addSourceSuccess(source, batch))
RSSSource.checkItems(source, feed.items, db.idb)
.then(items => insertItems(items))
.then(items => {
//dispatch(fetchItemsSuccess(items))
SourceGroup.save(getState().groups)
resolve(source.sid)
})
}
})
})
})
.catch(e => {
console.log(e)
dispatch(addSourceFailure(e, batch))
return new Promise((_, reject) => { reject(e) })
})
}
return new Promise((_, reject) => { reject("Sources not initialized or fetching items.") })
}
}
export function updateSourceDone(source: RSSSource): SourceActionTypes {
return {
type: UPDATE_SOURCE,
source: source
}
}
export function updateSource(source: RSSSource): AppThunk {
return (dispatch) => {
db.sdb.update({ sid: source.sid }, { $set: { ...source }}, {}, err => {
if (!err) {
dispatch(updateSourceDone(source))
}
})
}
}
export function deleteSourceDone(source: RSSSource): SourceActionTypes {
return {
type: DELETE_SOURCE,
source: source
}
}
export function deleteSource(source: RSSSource): AppThunk {
return (dispatch, getState) => {
dispatch(saveSettings())
db.idb.remove({ source: source.sid }, { multi: true }, (err) => {
if (err) {
console.log(err)
dispatch(saveSettings())
} else {
db.sdb.remove({ sid: source.sid }, {}, (err) => {
if (err) {
console.log(err)
dispatch(saveSettings())
} else {
dispatch(deleteSourceDone(source))
SourceGroup.save(getState().groups)
dispatch(saveSettings())
}
})
}
})
}
}
export function sourceReducer(
state: SourceState = {},
action: SourceActionTypes
): SourceState {
switch (action.type) {
case INIT_SOURCES:
switch (action.status) {
case ActionStatus.Success: {
let newState: SourceState = {}
for (let source of action.sources) {
newState[source.sid] = source
}
return newState
}
default: return state
}
case ADD_SOURCE:
switch (action.status) {
case ActionStatus.Success: return {
...state,
[action.source.sid]: action.source
}
default: return state
}
case UPDATE_SOURCE: return {
...state,
[action.source.sid]: action.source
}
case DELETE_SOURCE: {
delete state[action.source.sid]
return { ...state }
}
default: return state
}
}