Bilibili-Evolved/src/activity/feeds-apis.ts
2020-03-02 19:59:50 +08:00

320 lines
8.8 KiB
TypeScript

import { VideoCardInfo } from '../style/simplify-home/video-card-info'
export interface FeedsCardType {
id: number
name: string
}
export const feedsCardTypes = {
repost: {
id: 1,
name: '转发',
} as FeedsCardType,
textWithImages: {
id: 2,
name: '图文'
} as FeedsCardType,
text: {
id: 4,
name: '文字',
} as FeedsCardType,
video: {
id: 8,
name: '视频',
} as FeedsCardType,
miniVideo: {
id: 16,
name: '小视频',
} as FeedsCardType,
column: {
id: 64,
name: '专栏',
} as FeedsCardType,
audio: {
id: 256,
name: '音频',
} as FeedsCardType,
bangumi: {
id: 512,
name: '番剧',
} as FeedsCardType,
share: {
id: 2048,
name: '分享',
} as FeedsCardType,
manga: {
id: 2049,
name: '漫画',
} as FeedsCardType,
film: {
id: 4098,
name: '电影',
} as FeedsCardType,
tv: {
id: 4099,
name: 'TV剧',
} as FeedsCardType,
chinese: {
id: 4100,
name: '国创',
} as FeedsCardType,
documentary: {
id: 4101,
name: '纪录片',
} as FeedsCardType,
mediaList: {
id: 4300,
name: '收藏夹',
} as FeedsCardType,
}
export interface FeedsCard {
id: string
username: string
text: string
reposts: number
comments: number
likes: number
element: HTMLElement
type: FeedsCardType
getText: () => Promise<string>
}
const getFeedsCardType = (element: HTMLElement) => {
if (element.querySelector('.repost')) {
return feedsCardTypes.repost
}
if (element.querySelector('.imagesbox')) {
return feedsCardTypes.textWithImages
}
if (element.querySelector('.video-container')) {
return feedsCardTypes.video
}
if (element.querySelector('.bangumi-container')) {
return feedsCardTypes.bangumi
}
if (element.querySelector('.article-container')) {
return feedsCardTypes.column
}
if (element.querySelector('.music-container')) {
return feedsCardTypes.audio
}
if (element.querySelector('.h5share-container')) {
return feedsCardTypes.share
}
if (element.querySelector('.vc-ctnr')) {
return feedsCardTypes.miniVideo
}
return feedsCardTypes.text
}
class FeedsCardsManager extends EventTarget {
watching = false
cards: FeedsCard[] = []
addEventListener(
type: 'addCard' | 'removeCard',
listener: (event: CustomEvent<FeedsCard>) => void,
options?: boolean | AddEventListenerOptions | undefined
): void {
super.addEventListener(type, listener, options)
}
removeEventListener(
type: 'addCard' | 'removeCard',
callback: (event: CustomEvent<FeedsCard>) => void,
options?: boolean | AddEventListenerOptions | undefined
): void {
super.removeEventListener(type, callback, options)
}
async addCard(node: Node) {
if (node instanceof HTMLElement && node.classList.contains('card')) {
if (node.querySelector('.skeleton') !== null) {
const obs = Observer.childList(node, () => {
if (node.querySelector('.skeleton') === null) {
obs.stop()
this.addCard(node)
}
})
} else {
const card = await this.parseCard(node)
this.cards.push(card)
this.cards.sort((a, b) => {
if (a.id === b.id) {
return 0
}
return a.id > b.id ? -1 : 1
})
const event = new CustomEvent('addCard', { detail: card })
this.dispatchEvent(event)
}
}
}
async removeCard(node: Node) {
if (node instanceof HTMLElement && node.classList.contains('card')) {
const id = (await this.parseCard(node)).id
const index = this.cards.findIndex(c => c.id === id)
const card = this.cards[index]
this.cards.splice(index, 1)
const event = new CustomEvent('removeCard', { detail: card })
this.dispatchEvent(event)
}
}
async parseCard(element: HTMLElement): Promise<FeedsCard> {
const getSimpleText = async (selector: string) => {
const subElement = await SpinQuery.condition(
() => element.querySelector(selector),
it => it !== null
) as HTMLElement
if (subElement === null) {
console.warn(element, selector)
return ''
}
const subElementText = subElement.innerText.trim()
return subElementText
}
const getComplexText = async (type: FeedsCardType) => {
if (type === feedsCardTypes.bangumi) {
return ''
}
const el = await SpinQuery.condition(() => element, (it: any) => Boolean(it.__vue__))
if (el === null) {
console.warn(el)
return ''
}
if (type === feedsCardTypes.repost) {
const original = el.__vue__.originCardData.pureText
return el.__vue__.card.item.content + '\n' + original || ''
}
return el.__vue__.originCardData.pureText || ''
}
const getNumber = async (selector: string) => {
const result = parseInt(await getSimpleText(selector))
if (isNaN(result)) {
return 0
}
return result
}
const card = {
id: element.getAttribute('data-did') as string,
username: await getSimpleText('.main-content .user-name'),
text: '',
reposts: await getNumber('.button-bar .single-button:nth-child(1) .text-offset'),
comments: await getNumber('.button-bar .single-button:nth-child(2) .text-offset'),
likes: await getNumber('.button-bar .single-button:nth-child(3) .text-offset'),
element,
type: getFeedsCardType(element),
async getText() {
const result = await getComplexText(this.type)
this.text = result
// console.log(result)
return result
}
}
await card.getText()
element.setAttribute('data-type', card.type.id.toString())
// if (card.text === '') {
// console.warn('card text parsing failed!', card)
// }
return card
}
async startWatching() {
const cardsList = await SpinQuery.select('.card-list .content') as HTMLDivElement
if (!cardsList) {
return false
}
if (this.watching) {
return true
}
this.watching = true
const cards = [...cardsList.querySelectorAll('.content>.card')]
cards.forEach(it => this.addCard(it))
Observer.childList(cardsList, records => {
records.forEach(record => {
record.addedNodes.forEach(node => this.addCard(node))
record.removedNodes.forEach(node => this.removeCard(node))
})
})
return true
}
}
export const feedsCardsManager = new FeedsCardsManager()
export const getVideoFeeds = async (type: 'video' | 'bangumi' = 'video'): Promise<VideoCardInfo[]> => {
if (!getUID()) {
return []
}
const json = await Ajax.getJsonWithCredentials(
`https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${type === 'video' ? 8 : 512}`
)
if (json.code !== 0) {
throw new Error(json.message)
}
if (type === 'video') {
return json.data.cards.filter((c: any) => {
// 合作视频仅取UP主的
return c.desc.orig_dy_id === 0
}).map(
(c: any): VideoCardInfo => {
const card = JSON.parse(c.card)
const topics = _.get(c, 'display.topic_info.topic_details', []).map(
(it: any) => {
return {
id: it.topic_id,
name: it.topic_name
}
}
)
return {
id: c.desc.dynamic_id_str,
aid: card.aid,
title: card.title,
upID: c.desc.user_profile.info.uid,
upName: c.desc.user_profile.info.uname,
upFaceUrl: c.desc.user_profile.info.face,
coverUrl: card.pic,
description: card.desc,
timestamp: c.timestamp,
time: new Date(c.timestamp * 1000),
topics,
dynamic: card.dynamic,
like: formatCount(c.desc.like),
duration: card.duration,
durationText: formatDuration(card.duration, 0),
playCount: formatCount(card.stat.view),
danmakuCount: formatCount(card.stat.danmaku),
watchlater: store.state.watchlaterList.includes(card.aid)
}
}
)
} else if (type === 'bangumi') {
return json.data.cards.map(
(c: any): VideoCardInfo => {
const card = JSON.parse(c.card)
return {
id: c.desc.dynamic_id_str,
aid: card.aid,
epID: card.episode_id,
title: card.new_desc,
upName: card.apiSeasonInfo.title,
upFaceUrl: card.apiSeasonInfo.cover,
coverUrl: card.cover,
description: '',
timestamp: c.timestamp,
time: new Date(c.timestamp * 1000),
like: formatCount(c.desc.like),
durationText: '',
playCount: formatCount(card.play_count),
danmakuCount: formatCount(card.bullet_count),
watchlater: false,
}
}
)
} else {
return []
}
}
export default {
export: {
feedsCardsManager,
feedsCardTypes,
getVideoFeeds,
},
}