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: (items: T[]) => T[] } const contentFiltersKey = 'feeds.contentFilters' registerData(contentFiltersKey, [] as FeedsContentFilter[]) /** 对动态内容进行过滤 */ export const applyContentFilter = (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 = ( func: (...args: Args) => Promise, ) => (...args: Args) => func(...args).then(items => applyContentFilter(items)) /** * 获取视频或番剧动态 * @param type 指定视频 (video) 或番剧 (bangumi) 类型, 默认是视频 */ export const getVideoFeeds = withContentFilter( async (type: 'video' | 'bangumi' = 'video'): Promise => { 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) }