Bilibili-Evolved/src/components/feeds/api/index.ts
2022-04-11 13:31:06 +08:00

195 lines
7.3 KiB
TypeScript

import { getUID, pascalCase } from '@/core/utils'
import { getJsonWithCredentials } from '@/core/ajax'
import { formatCount, formatDuration } from '@/core/utils/formatters'
import { watchlaterList } from '@/components/video/watchlater'
import { getData, registerData } from '@/plugins/data'
import { descendingStringSort } from '@/core/utils/sort'
import { VideoCard } from '../video-card'
import { FeedsCard, FeedsCardType } from './types'
export * from './types'
export * from './manager'
/**
* 搜索视频卡片中重复的 aid, 合并为联合投稿
* @param cards 视频卡片
*/
export const groupVideoFeeds = (cards: VideoCard[]) => {
const groups = lodash.groupBy(cards, c => c.aid)
const cardToCooperationItem = (card: VideoCard) => (
{
id: card.upID,
name: card.upName,
faceUrl: card.upFaceUrl,
}
)
const results = Object.values(groups).map(groupCards => {
if (groupCards.length === 1) {
return groupCards[0]
}
const [firstCard, ...restCards] = groupCards
firstCard.cooperation = [
cardToCooperationItem(firstCard),
...restCards.map(cardToCooperationItem),
]
console.log([...firstCard.cooperation])
return firstCard
}).sort(descendingStringSort(it => it.id))
return results
}
/** 判断动态卡片是否包含预约功能.
* > 在预约功能刚上线时, 无论使用什么参数请求动态流, 预约过的东西都会插入到其中.
* > 比如传参番剧类型结果依然混入的预约视频, 导致数据混乱. 不知道之后有没有修复, 目前是使用此方法过滤掉预约类卡片.
*/
export const isPreOrderedVideo = (card: any) => lodash.get(card, 'extra.is_reserve_recall', 0) === 1
export interface FeedsContentFilter {
filter: <T> (items: T[]) => T[]
}
const contentFiltersKey = 'feeds.contentFilters'
registerData(contentFiltersKey, [] as FeedsContentFilter[])
/** 对动态内容进行过滤 */
export const applyContentFilter = <T> (items: T[]) => {
const [contentFilters] = getData(contentFiltersKey) as [FeedsContentFilter[]]
const result = contentFilters.reduce((acc, it) => (acc = it.filter(acc)), items)
return result
}
/** 对异步获取动态内容的函数进行包装, 将返回值套用 `applyContentFilter` */
export const withContentFilter = <Args extends any[], Item> (
func: (...args: Args) => Promise<Item[]>,
) => (...args: Args) => func(...args).then(items => applyContentFilter(items))
/**
* 获取视频或番剧动态
* @param type 指定视频 (video) 或番剧 (bangumi) 类型, 默认是视频
*/
export const getVideoFeeds = withContentFilter(
async (type: 'video' | 'bangumi' = 'video'): Promise<VideoCard[]> => {
if (!getUID()) {
return []
}
const json = await 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)
}
const dataCards = json.data.cards as any[]
const dataCardsWithoutPreOrder = dataCards.filter(it => !isPreOrderedVideo(JSON.parse(it.card)))
if (type === 'video') {
return groupVideoFeeds(dataCards.map(
(c: any): VideoCard => {
const card = JSON.parse(c.card)
const topics = lodash.get(c, 'display.topic_info.topic_details', []).map(
(it: any) => ({
id: it.topic_id,
name: it.topic_name,
}),
)
return {
id: c.desc.dynamic_id_str,
aid: card.aid,
bvid: c.desc.bvid || card.bvid,
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: watchlaterList.includes(card.aid),
}
},
))
} if (type === 'bangumi') {
return dataCardsWithoutPreOrder.map(
(c: any): VideoCard => {
const card = JSON.parse(c.card)
return {
id: c.desc.dynamic_id_str,
aid: card.aid,
bvid: c.desc.bvid || card.bvid,
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,
}
},
)
}
return []
},
)
// let mockupCalled = false
/**
* 获取动态
* @param type 动态类型, 或传入类型ID列表返回最新动态
* @param afterID 返回指定ID之前的动态历史, 省略则返回最新的动态
*/
export const getFeeds = async (type: FeedsCardType | string, afterID?: string | number) => {
if (typeof type === 'string') {
return getJsonWithCredentials(`https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${type}`)
}
const id = type.id.toString()
let api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${id}`
if (afterID) {
api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${afterID}&type=${id}`
}
return getJsonWithCredentials(api)
}
/**
* 向动态卡片的菜单中添加菜单项
* @param card 动态卡片
* @param config 菜单项配置
*/
export const addMenuItem = (card: FeedsCard, config: {
className: string
text: string
action: (e: MouseEvent) => void
}) => {
const morePanel = dq(card.element, '.more-panel, .bili-dyn-more__menu') as HTMLElement
const isV2 = morePanel.classList.contains('bili-dyn-more__menu')
const { className, text, action } = config
if (!morePanel || dq(morePanel, `.${className}`)) {
return
}
const menuItem = document.createElement(isV2 ? 'div' : 'p')
if (isV2) {
menuItem.classList.add('bili-dyn-more__menu__item', className)
menuItem.style.height = '25px'
menuItem.style.padding = '2px 0'
menuItem.style.textAlign = 'center'
menuItem.dataset.module = 'more'
menuItem.dataset.type = lodash.snakeCase(`ThreePoint${pascalCase(className)}`).toUpperCase()
menuItem.dataset.params = '{}'
} else {
menuItem.classList.add('child-button', 'c-pointer', className)
}
menuItem.textContent = text
const vueScopeAttributes = [...new Set([...morePanel.children].map((element: HTMLElement) => element.getAttributeNames().filter(it => it.startsWith('data-v-'))).flat())]
vueScopeAttributes.forEach(attr => menuItem.setAttribute(attr, ''))
menuItem.addEventListener('click', e => {
action(e)
card.element.click()
})
morePanel.appendChild(menuItem)
}