mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge tag 'v2.9.0-preview' into preview-fixes
This commit is contained in:
commit
4d47a6fae4
@ -1,10 +1,10 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
chrome > 100
|
||||
edge > 100
|
||||
firefox > 103
|
||||
safari > 15
|
||||
chrome > 105
|
||||
edge > 105
|
||||
firefox > 127
|
||||
safari > 15.4
|
||||
not and_chr > 0
|
||||
not and_ff > 0
|
||||
not and_qq > 0
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
## 最低配置
|
||||
- 操作系统: Windows 8.1 / macOS Big Sur
|
||||
- 分辨率: 1920x1080, 缩放 125%
|
||||
- 浏览器: Chrome 105+ / Firefox 121+ / Safari 15.4+ (仅理论上能运行, 没测试过)
|
||||
- 浏览器: Chrome 105+ / Firefox 127+ / Safari 15.4+ (仅理论上能运行, 没测试过)
|
||||
- 处理器: Intel Core i5-10500 / AMD Ryzen 5 3600
|
||||
- 内存: 8GB
|
||||
- 脚本管理器: Tampermonkey 5.0 / Violentmonkey 2.18
|
||||
@ -84,7 +84,7 @@
|
||||
## 推荐配置
|
||||
- 操作系统: Windows 10 / macOS Sonoma
|
||||
- 分辨率: 3840x2160, 缩放 200%
|
||||
- 浏览器: Chrome 120+ / Firefox 121+
|
||||
- 浏览器: Chrome 120+ / Firefox 127+
|
||||
- 处理器: Intel Core i7-11700 / AMD Ryzen 7 5800
|
||||
- 内存: 32GB
|
||||
- 脚本管理器: Tampermonkey 5.0 / Violentmonkey 2.18
|
||||
|
||||
33
registry/lib/components/live/remove-mask-panel/index.ts
Normal file
33
registry/lib/components/live/remove-mask-panel/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { liveUrls } from '@/core/utils/urls'
|
||||
|
||||
const id = 'web-player-module-area-mask-panel'
|
||||
const entry = async () => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if ((node as Element).id === id) {
|
||||
node.parentNode?.removeChild(node)
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
export const component = defineComponentMetadata({
|
||||
name: 'removeLiveMaskPanel',
|
||||
displayName: '删除直播马赛克遮罩',
|
||||
author: {
|
||||
name: 'Liki4',
|
||||
link: 'https://github.com/Liki4',
|
||||
},
|
||||
tags: [componentsTags.live, componentsTags.style],
|
||||
description: {
|
||||
'zh-CN': '删除观看直播时某些分区的马赛克遮罩.',
|
||||
},
|
||||
entry,
|
||||
reload: entry,
|
||||
urlInclude: liveUrls,
|
||||
})
|
||||
@ -103,7 +103,10 @@ const favoriteItemMapper = (item: any): FavoritesItemInfo => ({
|
||||
title: item.title,
|
||||
description: item.intro,
|
||||
duration: item.duration,
|
||||
durationText: formatDuration(item.duration),
|
||||
durationText:
|
||||
item.page > 1
|
||||
? `${formatDuration(item.duration)} / ${item.page}P`
|
||||
: formatDuration(item.duration),
|
||||
playCount: item.cnt_info.play,
|
||||
danmakuCount: item.cnt_info.danmaku,
|
||||
upName: item.upper.name,
|
||||
|
||||
@ -60,16 +60,6 @@
|
||||
class="progress"
|
||||
:style="{ width: h.progress * 100 + '%' }"
|
||||
></div>
|
||||
<div v-if="h.progressText" class="floating progress-number">
|
||||
{{ h.progress >= 1 ? '已看完' : h.progressText }}
|
||||
</div>
|
||||
<div
|
||||
v-if="h.liveStatus !== undefined"
|
||||
class="floating duration live-status"
|
||||
:class="{ on: h.liveStatus === 1 }"
|
||||
>
|
||||
{{ h.liveStatus === 1 ? '直播中' : '未开播' }}
|
||||
</div>
|
||||
<div v-if="h.durationText" class="floating duration">{{ h.durationText }}</div>
|
||||
</a>
|
||||
<a class="title" target="_blank" :href="h.url" :title="h.title">{{
|
||||
@ -89,8 +79,25 @@
|
||||
></DpiImage>
|
||||
<div class="up-name">{{ h.upName }}</div>
|
||||
</a>
|
||||
<div v-if="h.timeText" class="time" :title="new Date(h.viewAt).toLocaleString()">
|
||||
{{ h.timeText }}
|
||||
<div class="history-info">
|
||||
<div v-if="h.progressText" class="progress-number">
|
||||
{{ h.progress >= 0.95 ? '已看完' : h.progressText }}
|
||||
</div>
|
||||
<div
|
||||
v-if="h.liveStatus !== undefined"
|
||||
class="duration live-status"
|
||||
:class="{ on: h.liveStatus === 1 }"
|
||||
>
|
||||
{{ h.liveStatus === 1 ? '直播中' : '未开播' }}
|
||||
</div>
|
||||
<span
|
||||
v-if="h.progressText || h.liveStatus !== undefined"
|
||||
class="history-info-separator"
|
||||
>|</span
|
||||
>
|
||||
<div v-if="h.timeText" class="time" :title="new Date(h.viewAt).toLocaleString()">
|
||||
{{ h.timeText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
@ -405,22 +412,6 @@ export default Vue.extend({
|
||||
filter: invert(0.9);
|
||||
}
|
||||
}
|
||||
.duration {
|
||||
left: $padding;
|
||||
bottom: $padding;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.live-status {
|
||||
&.on {
|
||||
background-color: var(--theme-color);
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
}
|
||||
.progress-number {
|
||||
left: $padding;
|
||||
top: $padding;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@ -448,7 +439,7 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
.up,
|
||||
.time {
|
||||
.history-info {
|
||||
font-size: 11px;
|
||||
opacity: 0.75;
|
||||
align-self: center;
|
||||
@ -479,10 +470,23 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
.time {
|
||||
.history-info {
|
||||
@include h-center(4px);
|
||||
font-size: 11px;
|
||||
grid-area: time;
|
||||
padding-right: 6px;
|
||||
&-separator {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
.progress-number,
|
||||
.live-status {
|
||||
@include single-line();
|
||||
}
|
||||
.live-status {
|
||||
&.on {
|
||||
color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { getJsonWithCredentials, bilibiliApi } from '@/core/ajax'
|
||||
import { fixed } from '@/core/utils'
|
||||
import { formatDuration } from '@/core/utils/formatters'
|
||||
|
||||
/** 历史项目类型, 值为 API 中的 `history.business` */
|
||||
@ -107,13 +106,13 @@ const getTimeData = () => {
|
||||
}
|
||||
}
|
||||
const formatTime = (date: Date) => {
|
||||
const { yesterday } = getTimeData()
|
||||
const { yesterday, today } = getTimeData()
|
||||
const timestamp = Number(date)
|
||||
if (timestamp >= yesterday) {
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date
|
||||
.getMinutes()
|
||||
return `${timestamp >= today ? '今天' : '昨天'} ${date
|
||||
.getHours()
|
||||
.toString()
|
||||
.padStart(2, '0')}`
|
||||
.padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
return `${(date.getMonth() + 1).toString().padStart(2, '0')}-${date
|
||||
.getDate()
|
||||
@ -154,7 +153,9 @@ const parseHistoryItem = (item: any): HistoryItem => {
|
||||
cover,
|
||||
covers: item.covers?.map(https) ?? [],
|
||||
progress,
|
||||
progressText: Number.isNaN(progress) ? null : `${fixed(progress * 100, 1)}%`,
|
||||
progressText: Number.isNaN(progress)
|
||||
? null
|
||||
: `${formatDuration(item.progress)} / ${formatDuration(item.duration)}`,
|
||||
duration: item.duration,
|
||||
durationText: item.duration ? formatDuration(item.duration) : null,
|
||||
upName: item.author_name,
|
||||
|
||||
@ -34,18 +34,24 @@
|
||||
<VIcon icon="mdi-close" :size="16"></VIcon>
|
||||
</div>
|
||||
<div class="floating duration">{{ card.durationText }}</div>
|
||||
<div v-if="card.complete" class="floating viewed">已观看</div>
|
||||
<div v-if="card.totalPages > 1" class="floating pages">
|
||||
{{ card.currentPage }}P / {{ card.totalPages }}P
|
||||
</div>
|
||||
<div v-if="card.percent" class="progress" :style="{ width: card.percent * 100 + '%' }" />
|
||||
</a>
|
||||
<a class="title" target="_blank" :href="card.href" :title="card.title">{{ card.title }}</a>
|
||||
<a
|
||||
class="up"
|
||||
target="_blank"
|
||||
:href="'https://space.bilibili.com/' + card.upID"
|
||||
:title="card.upName"
|
||||
>
|
||||
<DpiImage class="face" :src="card.upFaceUrl" :size="20"></DpiImage>
|
||||
<div class="name">{{ card.upName }}</div>
|
||||
</a>
|
||||
<div class="info">
|
||||
<a
|
||||
class="up"
|
||||
target="_blank"
|
||||
:href="'https://space.bilibili.com/' + card.upID"
|
||||
:title="card.upName"
|
||||
>
|
||||
<DpiImage class="face" :src="card.upFaceUrl" :size="20"></DpiImage>
|
||||
<div class="name">{{ card.upName }}</div>
|
||||
</a>
|
||||
<div v-if="card.complete" class="viewed">已观看</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
@ -73,6 +79,9 @@ interface WatchlaterCard {
|
||||
upName: string
|
||||
upFaceUrl: string
|
||||
upID: number
|
||||
currentPage?: number
|
||||
totalPages: number
|
||||
percent: number
|
||||
}
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -125,28 +134,31 @@ export default Vue.extend({
|
||||
return `https://www.bilibili.com/medialist/play/watchlater/${item.bvid}`
|
||||
}
|
||||
const cards = rawList.map(item => {
|
||||
const currentPage = item.pages?.find(p => p.cid === item.cid)
|
||||
const duration = currentPage?.duration ?? item.duration
|
||||
const href = (() => {
|
||||
if (item.pages === undefined || !this.redirect) {
|
||||
if (!currentPage || !this.redirect) {
|
||||
return getLink(item)
|
||||
}
|
||||
const pages = item.pages.map(it => it.cid)
|
||||
const page = item.cid === 0 ? 1 : pages.indexOf(item.cid) + 1
|
||||
|
||||
const { page } = currentPage
|
||||
return page <= 1 ? getLink(item) : `${getLink(item)}?p=${page}`
|
||||
})()
|
||||
const percent = Math.round((1000 * item.progress) / item.duration) / 1000
|
||||
const percent = Math.round((1000 * item.progress) / duration) / 1000
|
||||
|
||||
return {
|
||||
aid: item.aid,
|
||||
href,
|
||||
coverUrl: item.pic.replace('http:', 'https:'),
|
||||
durationText: formatDuration(item.duration),
|
||||
duration: item.duration,
|
||||
// percent: `${fixed(percent * 100)}%`,
|
||||
durationText: formatDuration(duration),
|
||||
duration,
|
||||
complete: item.progress < 0 || percent > 0.95, // 进度过95%算看完, -1值表示100%
|
||||
title: item.title,
|
||||
upName: item.owner.name,
|
||||
upFaceUrl: item.owner.face.replace('http:', 'https:'),
|
||||
upID: item.owner.mid,
|
||||
currentPage: currentPage?.page,
|
||||
totalPages: item.videos,
|
||||
percent,
|
||||
} as WatchlaterCard
|
||||
})
|
||||
this.cards = cards
|
||||
@ -291,6 +303,11 @@ export default Vue.extend({
|
||||
border-radius: 8px 0 0 8px;
|
||||
position: relative;
|
||||
$padding: 6px;
|
||||
.floating {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
.remove {
|
||||
top: $padding;
|
||||
left: $padding;
|
||||
@ -301,20 +318,22 @@ export default Vue.extend({
|
||||
bottom: $padding;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.viewed {
|
||||
white-space: nowrap;
|
||||
right: $padding;
|
||||
.pages {
|
||||
top: $padding;
|
||||
right: $padding;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.floating {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
.cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.floating {
|
||||
@ -333,17 +352,21 @@ export default Vue.extend({
|
||||
color: var(--theme-color) !important;
|
||||
}
|
||||
}
|
||||
// .info {
|
||||
// grid-area: info;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: space-around;
|
||||
// margin: 8px;
|
||||
// }
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
grid-area: info;
|
||||
margin: 6px 8px;
|
||||
.viewed {
|
||||
opacity: 0.75;
|
||||
font-size: 11px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
.up {
|
||||
flex: 0 1 auto;
|
||||
padding: 2px 10px 2px 2px;
|
||||
margin: 0 8px 6px;
|
||||
justify-self: start;
|
||||
align-self: center;
|
||||
max-width: calc(100% - 16px);
|
||||
|
||||
@ -85,6 +85,10 @@ export const component = wrapSwitchOptions({
|
||||
defaultValue: false,
|
||||
displayName: '高能榜提示',
|
||||
},
|
||||
recommendedRooms: {
|
||||
defaultValue: false,
|
||||
displayName: '推荐直播间',
|
||||
},
|
||||
skin: {
|
||||
defaultValue: false,
|
||||
displayName: '房间皮肤',
|
||||
|
||||
@ -27,7 +27,8 @@ $prefix: 'simplifyLiveroom-switch';
|
||||
&-pk .chaos-pk,
|
||||
&-pk .awesome-pk-box,
|
||||
&-topRank .chat-item.top3-notice,
|
||||
&-topRank .chat-item .rank-icon {
|
||||
&-topRank .chat-item .rank-icon,
|
||||
&-recommendedRooms .left-container .room-info-ctnr {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { getComponentSettings } from '@/core/settings'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { retrieveImageUrl } from '@/core/utils'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { formatTitle } from '@/core/utils/title'
|
||||
import { formatTitle, getTitleVariablesFromDate } from '@/core/utils/title'
|
||||
import { DefaultWidget } from '@/ui'
|
||||
|
||||
export default Vue.extend({
|
||||
@ -31,25 +31,45 @@ export default Vue.extend({
|
||||
const toast = Toast.info('下载中...', '导出图片')
|
||||
this.busy = true
|
||||
try {
|
||||
const publishDate = getTitleVariablesFromDate(
|
||||
new Date(
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(unsafeWindow.__INITIAL_STATE__?.readInfo?.publish_time ?? 0) * 1000,
|
||||
),
|
||||
)
|
||||
const variables = {
|
||||
cv: document.URL.match(/read\/cv(\d+)/)?.at(1),
|
||||
publishYear: publishDate.year,
|
||||
publishMonth: publishDate.month,
|
||||
publishDay: publishDate.day,
|
||||
publishHour: publishDate.hour,
|
||||
publishMinute: publishDate.minute,
|
||||
publishSecond: publishDate.second,
|
||||
publishMillisecond: publishDate.millisecond,
|
||||
}
|
||||
|
||||
const images: { name: string; extension: string; url: string }[] = []
|
||||
const bannerElement = dq('.banner-image .card-image__image') as HTMLDivElement
|
||||
const bannerUrl = retrieveImageUrl(bannerElement)
|
||||
if (bannerUrl) {
|
||||
images.push({
|
||||
...bannerUrl,
|
||||
name: `${formatTitle(columnFormat, false, { n: '1' })}${bannerUrl.extension}`,
|
||||
name: `${formatTitle(columnFormat, false, { n: '1', ...variables })}${
|
||||
bannerUrl.extension
|
||||
}`,
|
||||
})
|
||||
console.log(bannerElement, bannerUrl, images)
|
||||
}
|
||||
const articleImages = dqa('.article-content .img-box img:not([class*="cut-off-"])')
|
||||
const articleImages = dqa('.article-content :is(img.normal-img, .normal-img img)')
|
||||
articleImages.forEach((image: HTMLElement) => {
|
||||
const url = retrieveImageUrl(image)
|
||||
if (url) {
|
||||
images.push({
|
||||
...url,
|
||||
name: `${formatTitle(columnFormat, false, { n: (images.length + 1).toString() })}${
|
||||
url.extension
|
||||
}`,
|
||||
name: `${formatTitle(columnFormat, false, {
|
||||
n: (images.length + 1).toString(),
|
||||
...variables,
|
||||
})}${url.extension}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -68,7 +88,8 @@ export default Vue.extend({
|
||||
)
|
||||
const pack = new DownloadPackage()
|
||||
imageBlobs.forEach((blob, index) => pack.add(images[index].name, blob))
|
||||
await pack.emit(`${formatTitle(columnFormat, false, { n: '' })}.zip`)
|
||||
|
||||
await pack.emit(`${formatTitle(columnFormat, false, { n: '', ...variables })}.zip`)
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
} finally {
|
||||
|
||||
@ -3,8 +3,8 @@ import { ComponentEntry } from '@/components/types'
|
||||
import { getBlob } from '@/core/ajax'
|
||||
import { DownloadPackage } from '@/core/download'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { matchUrlPattern, retrieveImageUrl } from '@/core/utils'
|
||||
import { formatTitle } from '@/core/utils/title'
|
||||
import { getVue2Data, matchUrlPattern, retrieveImageUrl } from '@/core/utils'
|
||||
import { formatTitle, getTitleVariablesFromDate } from '@/core/utils/title'
|
||||
import { feedsUrls } from '@/core/utils/urls'
|
||||
import { Options } from '.'
|
||||
|
||||
@ -36,6 +36,38 @@ export const setupFeedImageExporter: ComponentEntry<Options> = async ({
|
||||
}
|
||||
const toast = Toast.info('下载中...', '导出图片')
|
||||
let downloadedCount = 0
|
||||
|
||||
const vueData = getVue2Data(card.element)
|
||||
const authorModule = lodash.get(vueData, 'data.modules.module_author', {})
|
||||
const repostAuthorModule = lodash.get(vueData, 'data.orig.modules.module_author', {})
|
||||
const date = getTitleVariablesFromDate(new Date(authorModule.pub_ts * 1000))
|
||||
const repostDate = getTitleVariablesFromDate(
|
||||
new Date((repostAuthorModule.pub_ts ?? authorModule.pub_ts) * 1000),
|
||||
)
|
||||
const variables = {
|
||||
id: card.id,
|
||||
user: card.username,
|
||||
userID: authorModule.mid?.toString(),
|
||||
originalUser: (card as RepostFeedsCard).repostUsername ?? card.username,
|
||||
originalUserID: repostAuthorModule.mid?.toString() ?? authorModule.mid?.toString(),
|
||||
originalID: lodash.get(vueData, 'data.orig.id_str', card.id),
|
||||
|
||||
publishYear: date.year,
|
||||
publishMonth: date.month,
|
||||
publishDay: date.day,
|
||||
publishHour: date.hour,
|
||||
publishMinute: date.minute,
|
||||
publishSecond: date.second,
|
||||
publishMillisecond: date.millisecond,
|
||||
|
||||
originalPublishYear: repostDate.year,
|
||||
originalPublishMonth: repostDate.month,
|
||||
originalPublishDay: repostDate.day,
|
||||
originalPublishHour: repostDate.hour,
|
||||
originalPublishMinute: repostDate.minute,
|
||||
originalPublishSecond: repostDate.second,
|
||||
originalPublishMillisecond: repostDate.millisecond,
|
||||
}
|
||||
const imageBlobs = await Promise.all(
|
||||
imageUrls.map(async ({ url }) => {
|
||||
const blob = await getBlob(url)
|
||||
@ -48,10 +80,8 @@ export const setupFeedImageExporter: ComponentEntry<Options> = async ({
|
||||
const { feedFormat } = options
|
||||
imageBlobs.forEach((blob, index) => {
|
||||
const titleData = {
|
||||
user: card.username,
|
||||
id: card.id,
|
||||
originalUser: (card as RepostFeedsCard).repostUsername ?? card.username,
|
||||
n: (index + 1).toString(),
|
||||
...variables,
|
||||
}
|
||||
pack.add(
|
||||
`${formatTitle(feedFormat, false, titleData)}${imageUrls[index].extension}`,
|
||||
@ -60,10 +90,8 @@ export const setupFeedImageExporter: ComponentEntry<Options> = async ({
|
||||
})
|
||||
toast.close()
|
||||
const packTitleData = {
|
||||
user: card.username,
|
||||
id: card.id,
|
||||
originalUser: (card as RepostFeedsCard).repostUsername ?? card.username,
|
||||
n: '',
|
||||
...variables,
|
||||
}
|
||||
await pack.emit(`${formatTitle(feedFormat, false, packTitleData)}.zip`)
|
||||
},
|
||||
|
||||
@ -1,11 +1,40 @@
|
||||
可以批量导出某个地方的图片, 目前支持动态和专栏.
|
||||
|
||||
动态文件名变量:
|
||||
- `user`: 用户名
|
||||
- `originalUser`: 被转发用户名, 如果不是转发类型的动态则等于 `user`
|
||||
- `id`: 动态 ID
|
||||
- `n`: 第 n 张图
|
||||
- `id`: 动态 ID
|
||||
- `user`: 用户名
|
||||
- `userID`: 用户 ID
|
||||
- 动态发布时间:
|
||||
- `publishYear`
|
||||
- `publishMonth`
|
||||
- `publishDay`
|
||||
- `publishHour`
|
||||
- `publishMinute`
|
||||
- `publishSecond`
|
||||
- `publishMillisecond`
|
||||
- 被转发的数据 (如果不是转发类型的动态, 则和上面的对应变量相同):
|
||||
- `originalID`: 被转发的动态 ID
|
||||
- `originalUser`: 被转发的用户名
|
||||
- `originalUserID`: 被转发用户 ID
|
||||
- 被转发的动态发布时间:
|
||||
- `originalPublishYear`
|
||||
- `originalPublishMonth`
|
||||
- `originalPublishDay`
|
||||
- `originalPublishHour`
|
||||
- `originalPublishMinute`
|
||||
- `originalPublishSecond`
|
||||
- `originalPublishMillisecond`
|
||||
|
||||
专栏文件名变量:
|
||||
- `n`: 第 n 张图
|
||||
- `title`: 专栏标题
|
||||
- `n`: 第 n 张图
|
||||
- `cv`: 专栏 cv 号
|
||||
- 专栏发布时间:
|
||||
- `publishYear`
|
||||
- `publishMonth`
|
||||
- `publishDay`
|
||||
- `publishHour`
|
||||
- `publishMinute`
|
||||
- `publishSecond`
|
||||
- `publishMillisecond`
|
||||
|
||||
@ -11,10 +11,12 @@ const options = defineOptionsMetadata({
|
||||
columnFormat: {
|
||||
defaultValue: '[title][ - n]',
|
||||
displayName: '专栏图片命名格式',
|
||||
multiline: true,
|
||||
},
|
||||
feedFormat: {
|
||||
defaultValue: '[user][ - id][ - n]',
|
||||
displayName: '动态图片命名格式',
|
||||
multiline: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -99,7 +99,6 @@ import { videoBatchInput, videoSeasonBatchInput } from './inputs/video/batch'
|
||||
import { videoSingleInput } from './inputs/video/input'
|
||||
import { videoDashAvc, videoDashHevc, videoDashAv1, videoAudioDash } from './apis/dash'
|
||||
import { videoFlv } from './apis/flv'
|
||||
import { toastOutput } from './outputs/toast'
|
||||
import { streamSaverOutput } from './outputs/stream-saver'
|
||||
import {
|
||||
DownloadVideoAction,
|
||||
@ -125,7 +124,6 @@ const [apis] = registerAndGetData('downloadVideo.apis', [
|
||||
] as DownloadVideoApi[])
|
||||
const [assets] = registerAndGetData('downloadVideo.assets', [] as DownloadVideoAssets[])
|
||||
const [outputs] = registerAndGetData('downloadVideo.outputs', [
|
||||
toastOutput,
|
||||
streamSaverOutput,
|
||||
] as DownloadVideoOutput[])
|
||||
const { basicConfig } = getComponentSettings('downloadVideo').options as {
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
在功能面板中添加下载视频支持. 请注意:
|
||||
- 不能下载超出账号权限的视频, 例如非大会员下载大会员清晰度视频, 或者大陆地区网络下载港澳台地区番剧, 都是不可以的.
|
||||
- 请勿短时间进行大量下载, 以免遭到 b 站 IP 封禁.
|
||||
|
||||
在使用视频 (非番剧) 批量下载时, 文件的批量命名格式中可以使用以下额外变量:
|
||||
- `user`: UP 主用户名
|
||||
- `userID`: UP 主用户 ID
|
||||
- 视频发布时间:
|
||||
- `publishYear`
|
||||
- `publishMonth`
|
||||
- `publishDay`
|
||||
- `publishHour`
|
||||
- `publishMinute`
|
||||
- `publishSecond`
|
||||
- `publishMillisecond`
|
||||
|
||||
@ -2,7 +2,7 @@ import { getJsonWithCredentials } from '@/core/ajax'
|
||||
import { getGeneralSettings } from '@/core/settings'
|
||||
import { formatDuration, formatNumber } from '@/core/utils/formatters'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { formatTitle } from '@/core/utils/title'
|
||||
import { formatTitle, getTitleVariablesFromDate } from '@/core/utils/title'
|
||||
import { videoUrls } from '@/core/utils/urls'
|
||||
import { DownloadVideoInput } from '../../types'
|
||||
import { createEpisodesPicker, EpisodeItem } from '../episode-item'
|
||||
@ -22,11 +22,12 @@ export const videoBatchInput: DownloadVideoInput = {
|
||||
logError(`获取视频选集列表失败, message = ${json.message}`)
|
||||
return []
|
||||
}
|
||||
const { pages } = json.data
|
||||
const { pages, owner, pubdate } = json.data
|
||||
if (pages === undefined) {
|
||||
logError('获取视频选集列表失败, 没有找到选集信息.')
|
||||
return []
|
||||
}
|
||||
const date = getTitleVariablesFromDate(new Date(pubdate * 1000))
|
||||
return pages.map((page: any, index: number) => {
|
||||
const key = page.cid
|
||||
const title = `P${page.page} ${page.part}`
|
||||
@ -38,9 +39,18 @@ export const videoBatchInput: DownloadVideoInput = {
|
||||
inputItem: {
|
||||
allowQualityDrop: true,
|
||||
title: formatTitle(getGeneralSettings().batchFilenameFormat, false, {
|
||||
cid: page.cid,
|
||||
n: formatNumber(page.page, pages.length),
|
||||
cid: page.cid,
|
||||
ep: page.part,
|
||||
user: owner?.name,
|
||||
userID: owner?.mid?.toString(),
|
||||
publishYear: date.year,
|
||||
publishMonth: date.month,
|
||||
publishDay: date.day,
|
||||
publishHour: date.hour,
|
||||
publishMinute: date.minute,
|
||||
publishSecond: date.second,
|
||||
publishMillisecond: date.millisecond,
|
||||
}),
|
||||
cid: page.cid,
|
||||
aid,
|
||||
@ -64,6 +74,7 @@ export const videoSeasonBatchInput: DownloadVideoInput = {
|
||||
logError(`获取视频合集列表失败, message = ${json.message}`)
|
||||
return []
|
||||
}
|
||||
const owner = lodash.get(json, 'data.View.owner', {})
|
||||
const sections: { episodes: any[] }[] = lodash.get(json, 'data.View.ugc_season.sections', [])
|
||||
if (sections.length === 0) {
|
||||
return []
|
||||
@ -77,6 +88,7 @@ export const videoSeasonBatchInput: DownloadVideoInput = {
|
||||
const key = episode.cid
|
||||
const page = currentIndex + 1
|
||||
const title = `P${page} ${episode.title}`
|
||||
const date = getTitleVariablesFromDate(new Date(episode.arc.pubdate * 1000))
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
@ -85,10 +97,19 @@ export const videoSeasonBatchInput: DownloadVideoInput = {
|
||||
inputItem: {
|
||||
allowQualityDrop: true,
|
||||
title: formatTitle(getGeneralSettings().batchFilenameFormat, false, {
|
||||
n: formatNumber(page, totalEpisodesLength),
|
||||
title: episode.page.part,
|
||||
cid: episode.cid,
|
||||
n: formatNumber(page, totalEpisodesLength),
|
||||
ep: episode.title,
|
||||
user: owner.name,
|
||||
userID: owner.mid?.toString(),
|
||||
publishYear: date.year,
|
||||
publishMonth: date.month,
|
||||
publishDay: date.day,
|
||||
publishHour: date.hour,
|
||||
publishMinute: date.minute,
|
||||
publishSecond: date.second,
|
||||
publishMillisecond: date.millisecond,
|
||||
}),
|
||||
cid: episode.cid,
|
||||
aid: episode.aid,
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { Toast } from '@/core/toast'
|
||||
import { DownloadVideoOutput } from '../types'
|
||||
|
||||
export const toastOutput: DownloadVideoOutput = {
|
||||
name: 'toast',
|
||||
displayName: 'Toast',
|
||||
description:
|
||||
'弹一条消息显示出下载链接, 右键新标签页打开就可以下载. 链接有 referer 限制, 复制无用, 且不能保留视频文件名.',
|
||||
runAction: async action => {
|
||||
const fragments = action.infos.flatMap(it => it.titledFragments)
|
||||
const urls = fragments.map(f => f.url).join('\n')
|
||||
Toast.show(
|
||||
fragments
|
||||
.map(f => `<a class="link" href="${f.url}" download="${f.title}">${f.title}</a>`)
|
||||
.join('\n'),
|
||||
'下载视频',
|
||||
)
|
||||
console.log(urls)
|
||||
console.log(action)
|
||||
},
|
||||
}
|
||||
@ -13,7 +13,10 @@
|
||||
<button class="save" title="保存" @click="save">
|
||||
<VIcon :size="28" icon="mdi-content-save-outline"></VIcon>
|
||||
</button>
|
||||
<button title="丢弃" class="discard" @click="discard">
|
||||
<button class="copy" title="复制" @click="copy">
|
||||
<VIcon :size="24" :icon="showCopyTip ? 'mdi-check' : 'mdi-content-copy'"></VIcon>
|
||||
</button>
|
||||
<button class="discard" title="丢弃" @click="discard">
|
||||
<VIcon :size="28" icon="mdi-delete-forever-outline"></VIcon>
|
||||
</button>
|
||||
<span class="time">{{ time }}</span>
|
||||
@ -23,29 +26,48 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { VIcon } from '@/ui'
|
||||
import { Screenshot } from './screenshot'
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
VIcon,
|
||||
},
|
||||
props: {
|
||||
objectUrl: {
|
||||
type: String,
|
||||
screenshot: {
|
||||
type: Screenshot,
|
||||
required: true,
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCopyTip: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
objectUrl() {
|
||||
return this.screenshot.url
|
||||
},
|
||||
time: {
|
||||
type: String,
|
||||
required: true,
|
||||
filename() {
|
||||
return this.screenshot.filename
|
||||
},
|
||||
time() {
|
||||
return this.screenshot.time
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
discard() {
|
||||
this.$emit('discard')
|
||||
},
|
||||
async copy() {
|
||||
const screenshot = this.screenshot as Screenshot
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({ [screenshot.mimeType]: screenshot.blob }),
|
||||
])
|
||||
this.showCopyTip = true
|
||||
setTimeout(() => {
|
||||
this.showCopyTip = false
|
||||
}, 1000)
|
||||
},
|
||||
save() {
|
||||
const link = this.$refs.link as HTMLAnchorElement
|
||||
link.addEventListener(
|
||||
|
||||
@ -4,9 +4,7 @@
|
||||
<VideoScreenshot
|
||||
v-for="screenshot of screenshots"
|
||||
:key="screenshot.id"
|
||||
:filename="screenshot.filename"
|
||||
:object-url="screenshot.url"
|
||||
:time="screenshot.time"
|
||||
:screenshot="screenshot"
|
||||
@discard="discard(screenshot)"
|
||||
></VideoScreenshot>
|
||||
</transition-group>
|
||||
|
||||
@ -3,6 +3,7 @@ import { getFriendlyTitle } from '@/core/utils/title'
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
export class Screenshot {
|
||||
readonly mimeType = 'image/png'
|
||||
url = ''
|
||||
blob: Blob
|
||||
timeStamp = new Date().getTime()
|
||||
@ -57,7 +58,7 @@ export class Screenshot {
|
||||
}
|
||||
this.blob = blob
|
||||
this.url = URL.createObjectURL(blob)
|
||||
}, 'image/png')
|
||||
}, this.mimeType)
|
||||
} catch (error) {
|
||||
logError(
|
||||
'视频截图失败: 操作被浏览器阻止. 这通常发生于电影的试看片段, 请在正片尝试使用截图功能.',
|
||||
|
||||
@ -11,6 +11,14 @@
|
||||
:placeholder="value.toString()"
|
||||
@change="type === 'text' ? valueChange($event) : numberChange($event)"
|
||||
></TextBox>
|
||||
<TextArea
|
||||
v-if="type === 'textArea'"
|
||||
change-on-blur
|
||||
:validator="option.validator"
|
||||
:text="value.toString()"
|
||||
:placeholder="value.toString()"
|
||||
@change="valueChange($event)"
|
||||
></TextArea>
|
||||
<SwitchBox v-if="type === 'boolean'" :checked="value" @change="valueChange($event)"></SwitchBox>
|
||||
<ColorPicker
|
||||
v-if="type === 'color'"
|
||||
@ -54,7 +62,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { TextBox, SwitchBox, ColorPicker, RangeInput, VDropdown, ImagePicker, VSlider } from '@/ui'
|
||||
import {
|
||||
TextBox,
|
||||
TextArea,
|
||||
SwitchBox,
|
||||
ColorPicker,
|
||||
RangeInput,
|
||||
VDropdown,
|
||||
ImagePicker,
|
||||
VSlider,
|
||||
} from '@/ui'
|
||||
import { getComponentSettings, ComponentSettings } from '@/core/settings'
|
||||
import { OptionMetadata } from '../component'
|
||||
import { getDropdownItems } from './dropdown'
|
||||
@ -70,6 +87,7 @@ export default {
|
||||
components: {
|
||||
SwitchOptions,
|
||||
TextBox,
|
||||
TextArea,
|
||||
SwitchBox,
|
||||
ColorPicker,
|
||||
RangeInput,
|
||||
@ -123,6 +141,9 @@ export default {
|
||||
if (option.dropdownEnum) {
|
||||
return 'dropdown'
|
||||
}
|
||||
if (option.multiline) {
|
||||
return 'textArea'
|
||||
}
|
||||
return 'text'
|
||||
}
|
||||
case 'object': {
|
||||
@ -159,7 +180,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.component-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -187,5 +208,9 @@ export default {
|
||||
.be-slider {
|
||||
margin: 0 8px;
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -45,10 +45,12 @@ const options = defineOptionsMetadata({
|
||||
filenameFormat: {
|
||||
defaultValue: '[title][ - ep]',
|
||||
displayName: '文件命名格式',
|
||||
multiline: true,
|
||||
},
|
||||
batchFilenameFormat: {
|
||||
defaultValue: '[n - ][ep]',
|
||||
displayName: '批量命名格式',
|
||||
multiline: true,
|
||||
},
|
||||
downloadPackageEmitMode: {
|
||||
defaultValue: DownloadPackageEmitMode.Packed,
|
||||
|
||||
@ -59,6 +59,8 @@ export interface OptionMetadata<V = unknown> {
|
||||
hidden?: boolean
|
||||
/** 设为 `true` 时, 将用颜色选取器替代文本框 */
|
||||
color?: boolean
|
||||
/** 设为 `true` 时, 使用多行文本框 */
|
||||
multiline?: boolean
|
||||
/** 设置范围, 可以显示为一个滑动条 */
|
||||
slider?: {
|
||||
min?: number
|
||||
|
||||
@ -31,12 +31,24 @@ const tokenSplit = (format: string) => {
|
||||
}
|
||||
return tokens.filter(it => it !== '')
|
||||
}
|
||||
|
||||
export const getTitleVariablesFromDate = (date: Date, padStartLength = 2) => {
|
||||
return {
|
||||
year: date.getFullYear().toString(),
|
||||
month: (date.getMonth() + 1).toString().padStart(padStartLength, '0'),
|
||||
day: date.getDate().toString().padStart(padStartLength, '0'),
|
||||
hour: date.getHours().toString().padStart(padStartLength, '0'),
|
||||
minute: date.getMinutes().toString().padStart(padStartLength, '0'),
|
||||
second: date.getSeconds().toString().padStart(padStartLength, '0'),
|
||||
millisecond: date.getMilliseconds().toString().substring(0, 3),
|
||||
}
|
||||
}
|
||||
|
||||
export const formatTitle = (
|
||||
format: string,
|
||||
includesPageTitle = true,
|
||||
extraVariables: StringMap = {},
|
||||
) => {
|
||||
const now = new Date()
|
||||
const getLegacyTitle = () => {
|
||||
return (
|
||||
document.title
|
||||
@ -55,6 +67,7 @@ export const formatTitle = (
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
const dateVariables = getTitleVariablesFromDate(new Date())
|
||||
const builtInVariables: StringMap = {
|
||||
title: (() => {
|
||||
const videoPageTitle = dq('.video-info-container .video-title')
|
||||
@ -95,14 +108,13 @@ export const formatTitle = (
|
||||
bvid: unsafeWindow.bvid,
|
||||
cid: unsafeWindow.cid,
|
||||
lid: document.URL.replace(/https:\/\/live\.bilibili\.com\/(blanc\/)?([\d]+)/, '$2'),
|
||||
// 年月日这方法名真够乱的
|
||||
y: now.getFullYear().toString(),
|
||||
M: (now.getMonth() + 1).toString().padStart(2, '0'), // zero-based
|
||||
d: now.getDate().toString().padStart(2, '0'),
|
||||
h: now.getHours().toString().padStart(2, '0'),
|
||||
m: now.getMinutes().toString().padStart(2, '0'),
|
||||
s: now.getSeconds().toString().padStart(2, '0'),
|
||||
ms: now.getMilliseconds().toString().substring(0, 3),
|
||||
y: dateVariables.year,
|
||||
M: dateVariables.month,
|
||||
d: dateVariables.day,
|
||||
h: dateVariables.hour,
|
||||
m: dateVariables.minute,
|
||||
s: dateVariables.second,
|
||||
ms: dateVariables.millisecond,
|
||||
}
|
||||
const variables = {
|
||||
...builtInVariables,
|
||||
|
||||
@ -36,6 +36,7 @@ export default Vue.extend({
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
field-sizing: content;
|
||||
width: 0;
|
||||
flex: 1 0 0;
|
||||
padding: 4px 6px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user