Compare commits

..

71 Commits

Author SHA1 Message Date
jxxghp
2c05f5779e v1.4.3 2023-11-19 13:49:16 +08:00
jxxghp
9af200f89e feat 支持更多豆瓣详情展示 2023-11-18 21:52:35 +08:00
jxxghp
7e221cfd46 fix douban check subscribe 2023-11-13 20:23:13 +08:00
jxxghp
640882d178 v1.4.2 2023-11-13 12:05:26 +08:00
jxxghp
3a1436abef fix #1100 2023-11-10 21:41:57 +08:00
jxxghp
d431f0490d fix user add 2023-11-10 20:41:01 +08:00
jxxghp
4c2a6c92a6 fix 2023-11-10 12:22:06 +08:00
jxxghp
086c230e9e fix text 2023-11-09 23:26:46 +08:00
jxxghp
27e2ff50f2 fix ui 2023-11-09 12:31:00 +08:00
jxxghp
3134e5596b fix 2023-11-07 09:57:46 +08:00
jxxghp
315274abf9 fix ui 2023-11-06 11:42:13 +08:00
jxxghp
52bbf65fa8 fix #1046 2023-11-05 21:45:42 +08:00
jxxghp
9c018ec63b Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2023-11-05 21:43:33 +08:00
jxxghp
bd7e457cdb fix ui 2023-11-05 21:43:27 +08:00
jxxghp
36a0f8515b 更新 package.json 2023-11-05 09:14:17 +08:00
jxxghp
cac10a337d fix 2023-11-04 22:27:56 +08:00
jxxghp
edb53cc58f fix plugin market icon 2023-11-02 12:50:57 +08:00
jxxghp
1dceeecdad fix ui 2023-11-02 11:16:38 +08:00
jxxghp
f8071ada0b feat 插件支持在线图标 2023-11-01 22:03:46 +08:00
jxxghp
21bc8edbd8 feat 在线插件市场 2023-11-01 21:05:31 +08:00
jxxghp
2a8aeb5041 feat 插件市场显示版本号 2023-11-01 18:02:51 +08:00
jxxghp
1a7760cf6d fix build 2023-11-01 17:05:12 +08:00
jxxghp
aee4eed5ac feat 拆分插件图标 2023-11-01 17:02:57 +08:00
jxxghp
87215fb590 add icons 2023-11-01 16:23:50 +08:00
jxxghp
5409126187 add versions 2023-11-01 12:23:08 +08:00
jxxghp
9840782ce5 v1.3.8 2023-10-31 11:48:17 +08:00
jxxghp
d18f42cd6f v1.3.7-1 2023-10-29 18:41:18 +08:00
jxxghp
9372e98459 fix #57 2023-10-29 18:40:37 +08:00
jxxghp
9400f4660d Merge pull request #57 from Shurelol/main 2023-10-29 17:53:05 +08:00
Shurelol
f0d66b8fba feat: 识别测试结果在应用识别词时显示应用详情 2023-10-28 02:56:08 +08:00
jxxghp
78abe72815 Merge pull request #56 from thsrite/main 2023-10-27 17:07:13 +08:00
thsrite
1ce75916ef fix 正在下载显示剩余下载时间 2023-10-27 16:53:09 +08:00
jxxghp
46959d4baa v1.3.7 2023-10-26 17:09:29 +08:00
jxxghp
b24cc44493 Merge pull request #55 from thsrite/main 2023-10-26 16:04:43 +08:00
thsrite
46f6c29e1d feat 云盘文件删除插件 2023-10-26 14:49:09 +08:00
thsrite
5ad75b8420 fix 登录页海报支持自定义tmdb/bing 2023-10-24 10:55:45 +08:00
jxxghp
2030459f20 v1.3.6-1 2023-10-22 08:23:43 +08:00
jxxghp
2855bf812b fix #947 2023-10-22 08:18:01 +08:00
jxxghp
69989893d9 v1.3.6 2023-10-21 14:22:49 +08:00
jxxghp
ffc61f4a31 Merge pull request #54 from thsrite/main 2023-10-20 14:33:15 +08:00
thsrite
dd051f28d2 feat 站点自动登录插件 2023-10-20 12:54:12 +08:00
jxxghp
a3d2def72b fix ui bug 2023-10-20 12:42:54 +08:00
jxxghp
e8552b4385 v1.3.5-1 2023-10-20 07:37:44 +08:00
jxxghp
d73e4853a8 fix ui 2023-10-19 17:06:46 +08:00
jxxghp
7f991da183 fix ui 2023-10-18 21:07:26 +08:00
jxxghp
046d96a012 Merge pull request #52 from thsrite/main 2023-10-17 17:04:33 +08:00
thsrite
9ee6ca43e3 feat MoviePilot更新推送插件 2023-10-17 16:17:05 +08:00
jxxghp
43b1f7e620 v1.3.4-2 2023-10-17 14:04:29 +08:00
jxxghp
ba76f79d85 fix ui 2023-10-17 14:02:54 +08:00
jxxghp
ce47afa698 fix ui 2023-10-16 19:56:49 +08:00
jxxghp
6da110948c v1.3.4 2023-10-16 17:35:23 +08:00
jxxghp
533c564db5 feat 普通用户下载中只显示自己添加的下载 2023-10-16 08:32:02 +08:00
jxxghp
4a65056909 fix 普通用户订阅权限 2023-10-16 08:23:38 +08:00
jxxghp
c52ad73101 fix dialogs 2023-10-16 06:58:09 +08:00
jxxghp
5a3673efc6 更新 package.json 2023-10-14 14:34:16 +08:00
jxxghp
c03ec1d741 fix ui 2023-10-14 14:29:57 +08:00
jxxghp
e62d0809b3 fix ui 2023-10-14 14:15:38 +08:00
jxxghp
7f13597517 feat merge form 2023-10-14 13:48:02 +08:00
jxxghp
c822f1fffd feat 整合站点编辑组件 2023-10-14 09:13:38 +08:00
jxxghp
14ca74a29d Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2023-10-13 22:56:07 +08:00
jxxghp
3ee897a350 fix 2023-10-13 22:56:02 +08:00
jxxghp
789aac60c9 更新 package.json 2023-10-13 22:39:49 +08:00
jxxghp
2c73a8f3e1 fix ui 2023-10-13 22:37:04 +08:00
jxxghp
539bc656f8 fix 2023-10-13 22:33:58 +08:00
jxxghp
feda0cad2d feat 默认过滤规则拆分 2023-10-13 21:28:34 +08:00
jxxghp
c723d89739 fix 2023-10-13 17:30:14 +08:00
jxxghp
0a0e7a059a fix 2023-10-13 17:29:23 +08:00
jxxghp
0263fbbee6 fix 2023-10-13 17:26:12 +08:00
jxxghp
e205296e22 feat 订阅实时编辑 2023-10-13 17:24:18 +08:00
jxxghp
261f5a9c68 fix #822 2023-10-13 15:14:26 +08:00
jxxghp
fa097651f4 fix rules 2023-10-13 11:41:27 +08:00
92 changed files with 1882 additions and 1426 deletions

View File

@@ -27,6 +27,13 @@ jobs:
node-version: '18'
cache: 'yarn'
- name: Download Icons
run: |
pwd
curl -sL "https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip" | busybox unzip -d /tmp -
mv /tmp/MoviePilot-Plugins-main/icons public/plugin_icon
rm -rf /tmp/MoviePilot-Plugins-main
- name: Build frontend
id: build_frontend
run: |

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ dist-ssr
# iconify dist files
src/@iconify/*.js
public/plugin_icon/**

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.3.2-2",
"version": "1.4.3",
"private": true,
"bin": "dist/service.js",
"scripts": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -55,7 +55,7 @@ export function formatFileSize(bytes: number) {
if (bytes < 0)
throw new Error('字节数不能为负数。')
const units = ['B', 'K', 'M', 'G', 'T']
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = bytes
let unitIndex = 0

View File

@@ -77,8 +77,8 @@ export interface Subscribe {
// 订阅站点
sites: number[]
// 是否洗版
best_version: number
// 是否洗版数字或者boolean
best_version: any
// 当前优先级
current_priority: number
@@ -337,7 +337,7 @@ export interface TmdbEpisode {
guest_stars: Object[]
}
// TMDB人信息
// TMDB人信息
export interface TmdbPerson {
// ID
id?: number
@@ -388,6 +388,34 @@ export interface TmdbPerson {
biography?: string
}
// 豆瓣人物信息
export interface DoubanPerson {
// ID
id?: string
// 名称
name?: string
// 角色
roles?: string[]
// 简介
title?: string
// 详情页面
url?: string
// 饰演
character?: string
// 图片 large/normal
avatar?: { [key: string]: string }
// 别名
latin_name?: string
}
// 站点
export interface Site {
@@ -416,13 +444,13 @@ export interface Site {
ua?: string
// 是否使用代理
proxy?: number
proxy?: any
// 过滤规则
filter?: string
// 是否演染
render?: number
render?: any
// 是否公开站点
public?: number
@@ -478,6 +506,9 @@ export interface DownloadingInfo {
// 媒体信息
media: { [key: string]: any }
// 下载用户
userid?: string
}
// 缺失剧集信息
@@ -538,6 +569,15 @@ export interface Plugin {
// 是否有详情页面
has_page?: boolean
// 是否有新版本
has_update?: boolean
// 是否本地插件
is_local?: boolean
// 插件仓库地址
repo_url?: string
}
// 种子信息
@@ -622,6 +662,9 @@ export interface MetaInfo {
// 原字符串
org_string?: string
// 原标题(未经识别词转换)
title?: string
// 副标题
subtitle?: string
@@ -723,6 +766,9 @@ export interface MetaInfo {
// 资源类型+特效
edition: string
// 应用的自定义识别词
apply_words: string[]
}
// 上下文信息

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { DoubanPerson } from '@/api/types'
const personProps = defineProps({
person: Object as PropType<DoubanPerson>,
width: String,
height: String,
})
// 当前人物
const personInfo = ref(personProps.person)
// 人物图片是否加载
const isImageLoaded = ref(false)
// 人物图片地址
function getPersonImage() {
if (!personInfo.value?.avatar)
return personIcon
return personInfo.value?.avatar?.large
}
// 打开人物详情
function goPersonDetail() {
if (!personInfo.value?.id)
return
window.open(`https://movie.douban.com/celebrity/${personInfo.value?.id}/`, '_blank')
}
</script>
<template>
<VHover v-bind="personProps">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="personProps.height"
:width="personProps.width"
class="rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
<VAvatar
size="120"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
</div>
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
{{ personInfo?.character }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
</div>
</div>
</div>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.person-card {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
}
.person-card:hover {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
}
</style>

View File

@@ -17,7 +17,7 @@ function getPercentage() {
// 速度
function getSpeedText() {
return `${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`
return `${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s ${props.info?.left_time}`
}
// 下载状态

View File

@@ -35,6 +35,11 @@ function filtersChanged(value: string[]) {
const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '粤语配音', value: ' HKVOI ' },
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '分辨率: 4K', value: ' 4K ' },
{ title: '分辨率: 1080P', value: ' 1080P ' },
{ title: '分辨率: 720P', value: ' 720P ' },
@@ -49,8 +54,8 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: REMUX', value: ' !REMUX ' },
{ title: '质量: WEB-DL', value: ' WEBDL ' },
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
{ title: '质量: 60fps', value: '60FPS' },
{ title: '排除: 60fps', value: '!60FPS' },
{ title: '质量: 60fps', value: ' 60FPS ' },
{ title: '排除: 60fps', value: ' !60FPS ' },
{ title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ' },
{ title: '编码: H264', value: ' H264 ' },
@@ -61,9 +66,10 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
{ title: '效果: HDR', value: ' HDR ' },
{ title: '排除: HDR', value: ' !HDR ' },
{ title: '国语配音', value: ' CNVOI ' },
{ title: '排除: 国语配音', value: ' !CNVOI ' },
{ title: '促销: 免费', value: ' FREE ' },
{ title: '效果: SDR', value: ' SDR ' },
{ title: '排除: SDR', value: ' !SDR ' },
{ title: '效果: 3D', value: ' 3D ' },
{ title: '排除: 3D', value: ' !3D ' },
])
</script>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
@@ -33,12 +34,18 @@ const isSubscribed = ref(false)
// 本地存在状态
const isExists = ref(false)
// 各季缺失状态0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
// 各季缺失状态0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 订阅季弹窗
const subscribeSeasonDialog = ref(false)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 订阅ID
const subscribeId = ref<number>()
// 季详情
const seasonInfos = ref<TmdbSeason[]>([])
@@ -86,6 +93,7 @@ async function handleAddSubscribe() {
}
else {
// 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true
}
}
@@ -136,6 +144,12 @@ async function addSubscribe(season = 0) {
result.message,
best_version,
)
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
catch (error) {
console.error(error)
@@ -156,9 +170,9 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0)
subname = '洗版订阅'
if (result)
if (result && seasonsSelected.value.length > 1)
$toast.success(`${title} 添加${subname}成功!`)
else
else if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
}
@@ -206,7 +220,7 @@ async function handleCheckSubscribe() {
}
}
// 查询当前媒体是否已存在
// 查询当前媒体是否已入库
async function handleCheckExists() {
try {
const result: { [key: string]: any } = await api.get('media/exists', {
@@ -237,6 +251,7 @@ async function checkSubscribe(season = 0) {
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
season,
title: props.media?.title,
},
})
@@ -257,7 +272,7 @@ async function checkSeasonsNotExists() {
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
if (result) {
result.forEach((item) => {
// 0-已存在 1-部分缺失 2-全部缺失
// 0-已入库 1-部分缺失 2-全部缺失
let state = 0
if (item.episodes.length === 0)
state = 2
@@ -313,14 +328,14 @@ function getExistColor(season: number) {
function getExistText(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return '已存在'
return '已入库'
if (state === 1)
return '部分缺失'
else if (state === 2)
return '缺失'
else
return '已存在'
return '已入库'
}
// 打开详情页
@@ -480,7 +495,7 @@ function getYear(airDate: string) {
inset
scrollable
>
<VCard>
<VCard class="rounded-t">
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
<VCardTitle class="pe-10">
订阅 - {{ props.media?.title }}
@@ -557,6 +572,14 @@ function getYear(airDate: string) {
</div>
</VCard>
</VBottomSheet>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
/>
</template>
<style lang="scss">

View File

@@ -143,5 +143,33 @@ function openTmdbPage(type: string, tmdbId: number) {
识别失败无法识别到有效信息
</VAlert>
</VCol>
<VExpansionPanels
v-show="context?.meta_info?.title !== context?.meta_info.org_string"
>
<VExpansionPanel>
<VExpansionPanelTitle>
识别词应用详情
</VExpansionPanelTitle>
<VExpansionPanelText>
<VChip
variant="elevated"
class="me-1 mb-1 break-all"
color="primary"
>
{{ context?.meta_info.org_string }}
</VChip>
<VChip
v-for="(val, key) in context?.meta_info.apply_words"
:key="key"
:val="val"
variant="outlined"
color="info"
class="me-1 mb-1 break-all"
>
{{ val }}
</VChip>
</VExpansionPanelText>
</VExpansionPanel>
</VExpansionPanels>
</div>
</template>

View File

@@ -16,16 +16,35 @@ const emit = defineEmits(['install'])
// 提示框
const $toast = useToast()
// 进度框
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在安装插件...')
// 图片是否加载完成
const isImageLoaded = ref(false)
// 安装插件
async function installPlugin() {
try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...`
const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`,
{
params: {
repo_url: props.plugin?.repo_url,
force: props.plugin?.has_update,
},
},
)
// 隐藏等待提示框
progressDialog.value = false
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
@@ -40,6 +59,13 @@ async function installPlugin() {
console.error(error)
}
}
// 计算图标路径
const iconPath = computed(() => {
return props.plugin?.plugin_icon?.startsWith('http')
? props.plugin?.plugin_icon
: `/plugin_icon/${props.plugin?.plugin_icon}`
})
</script>
<template>
@@ -52,12 +78,21 @@ async function installPlugin() {
class="relative pa-4 text-center card-cover-blurred"
:style="{ background: `${props.plugin?.plugin_color}` }"
>
<div
v-if="props.plugin?.has_update"
class="me-n3 absolute top-0 right-5"
>
<VIcon
icon="mdi-new-box"
class="text-white"
/>
</div>
<VAvatar
size="8rem"
:class="{ shadow: isImageLoaded }"
>
<VImg
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
:src="iconPath"
aspect-ratio="4/3"
cover
@load="isImageLoaded = true"
@@ -76,9 +111,29 @@ async function installPlugin() {
@click.stop
>
{{ props.plugin?.plugin_author }}
</a>
</a><br>
版本{{ props.plugin?.plugin_version }}
</VCardText>
</VCard>
<!-- 安装插件进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>

View File

@@ -136,6 +136,13 @@ async function showPluginConfig() {
pluginConfigDialog.value = true
}
// 计算图标路径
const iconPath = computed(() => {
return props.plugin?.plugin_icon?.startsWith('http')
? props.plugin?.plugin_icon
: `/plugin_icon/${props.plugin?.plugin_icon}`
})
// 弹出菜单
const dropdownItems = ref([
{
@@ -216,7 +223,7 @@ const dropdownItems = ref([
:class="{ shadow: isImageLoaded }"
>
<VImg
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
:src="iconPath"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@@ -226,7 +233,7 @@ const dropdownItems = ref([
<VCardItem class="py-2">
<VCardTitle class="flex items-center flex-row">
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
{{ props.plugin?.plugin_name }}
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">{{ props.plugin?.plugin_version }}</span>
</VCardTitle>
</VCardItem>
<VCardText>
@@ -236,11 +243,13 @@ const dropdownItems = ref([
<!-- 插件配置页面 -->
<VDialog
v-model="pluginConfigDialog"
max-width="50rem"
scrollable
persistent
max-width="60rem"
>
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
<VCard
:title="`${props.plugin?.plugin_name} - 配置`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginConfigDialog = false" />
<VCardText>
<FormRender
@@ -255,7 +264,10 @@ const dropdownItems = ref([
查看详情
</VBtn>
<VSpacer />
<VBtn @click="savePluginConf">
<VBtn
variant="tonal"
@click="savePluginConf"
>
保存
</VBtn>
</VCardActions>
@@ -265,11 +277,13 @@ const dropdownItems = ref([
<!-- 插件详情页面 -->
<VDialog
v-model="pluginInfoDialog"
max-width="62.5rem"
scrollable
persistent
max-width="80rem"
>
<VCard :title="`${props.plugin?.plugin_name}`">
<VCard
:title="`${props.plugin?.plugin_name}`"
class="rounded-t"
>
<DialogCloseBtn @click="pluginInfoDialog = false" />
<VCardText>
<PageRender
@@ -279,11 +293,16 @@ const dropdownItems = ref([
/>
</VCardText>
<VCardActions>
<VBtn @click="showPluginConfig">
<VBtn
@click="showPluginConfig"
>
配置
</VBtn>
<VSpacer />
<VBtn @click="pluginInfoDialog = false">
<VBtn
variant="tonal"
@click="pluginInfoDialog = false"
>
关闭
</VBtn>
</VCardActions>

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
import { formatFileSize } from '@core/utils/formatters'
import { numberValidator, requiredValidator } from '@/@validators'
import { requiredValidator } from '@/@validators'
import api from '@/api'
import type { Site, TorrentInfo } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue'
@@ -15,7 +16,7 @@ const cardProps = defineProps({
})
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'update'])
const emit = defineEmits(['update', 'remove'])
// 密码输入
const isPasswordVisible = ref(false)
@@ -32,9 +33,6 @@ const testButtonText = ref('测试')
// 测试按钮可用性
const testButtonDisable = ref(false)
// 更新按钮文字
const updateButtonText = ref('更新')
// 更新按钮可用性
const updateButtonDisable = ref(false)
@@ -42,11 +40,17 @@ const updateButtonDisable = ref(false)
const siteCookieDialog = ref(false)
// 站点编辑弹窗
const siteInfoDialog = ref(false)
const siteEditDialog = ref(false)
// 资源浏览弹窗
const resourceDialog = ref(false)
// 进度条
const progressDialog = ref(false)
// 进度文本
const progressText = ref('请稍候 ...')
// 资源浏览表头
const resourceHeaders = [
{ title: '标题', key: 'title', sortable: false },
@@ -78,27 +82,6 @@ const userPwForm = ref({
password: '',
})
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 站点编辑表单数据
const siteForm = reactive<any>(cardProps.site ?? {})
// 类型转换
siteForm.proxy = siteForm.proxy === 1
siteForm.render = siteForm.render === 1
// 打开种子详情页面
function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank')
@@ -144,11 +127,6 @@ async function handleSiteUpdate() {
siteCookieDialog.value = true
}
// 打开站点编辑弹窗
async function handleSiteInfo() {
siteInfoDialog.value = true
}
// 打开资源浏览弹窗
async function handleResourceBrowse() {
resourceDialog.value = true
@@ -163,9 +141,11 @@ async function updateSiteCookie() {
// 更新按钮状态
siteCookieDialog.value = false
updateButtonText.value = '更新中 ...'
updateButtonDisable.value = true
progressDialog.value = true
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
const result: { [key: string]: any } = await api.get(
`site/cookie/${cardProps.site?.id}`,
{
@@ -181,7 +161,7 @@ async function updateSiteCookie() {
else
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
updateButtonText.value = '更新'
progressDialog.value = false
updateButtonDisable.value = false
}
catch (error) {
@@ -189,42 +169,6 @@ async function updateSiteCookie() {
}
}
// 调用API删除站点信息
async function deleteSiteInfo() {
try {
siteInfoDialog.value = false
const result: { [key: string]: any } = await api.delete(`site/${cardProps.site?.id}`)
if (result.success) {
$toast.success(`${cardProps.site?.name} 删除成功!`)
emit('remove')
}
else { $toast.error(`${cardProps.site?.name} 删除失败:${result.message}`) }
}
catch (error) {
$toast.error(`${cardProps.site?.name} 删除失败!`)
console.error(error)
}
}
// 调用API更新站点信息
async function updateSiteInfo() {
try {
// 更新按钮状态
siteInfoDialog.value = false
const result: { [key: string]: any } = await api.put('site/', siteForm)
if (result.success) {
$toast.success(`${cardProps.site?.name} 更新成功!`)
emit('update')
}
else { $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`) }
}
catch (error) {
$toast.error(`${cardProps.site?.name} 更新失败!`)
console.error(error)
}
}
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
@@ -264,9 +208,9 @@ onMounted(() => {
<VCard
:height="cardProps.height"
:width="cardProps.width"
:flat="!siteForm.is_active"
:flat="!cardProps.site?.is_active"
class="overflow-hidden"
@click="handleSiteInfo"
@click="siteEditDialog = true"
>
<template #image>
<VAvatar
@@ -278,17 +222,19 @@ onMounted(() => {
</VAvatar>
</template>
<VCardItem>
<VCardTitle class="font-bold" @click.stop="openSitePage">
{{ cardProps.site?.name }}
<VCardTitle class="font-bold">
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
</VCardTitle>
<VCardSubtitle>{{ cardProps.site?.url }}</VCardSubtitle>
<VCardSubtitle>
{{ cardProps.site?.url }}
</VCardSubtitle>
</VCardItem>
<ExistIcon v-if="siteForm.is_active" />
<ExistIcon v-if="cardProps.site?.is_active" />
<VCardText class="py-2">
<VTooltip
v-if="siteForm.render"
v-if="cardProps.site?.render === 1"
text="浏览器仿真"
>
<template #activator="{ props }">
@@ -302,7 +248,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.proxy"
v-if="cardProps.site?.proxy === 1"
text="代理"
>
<template #activator="{ props }">
@@ -316,7 +262,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.limit_interval"
v-if="cardProps.site?.limit_interval"
text="流控"
>
<template #activator="{ props }">
@@ -330,7 +276,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.filter"
v-if="cardProps.site?.filter"
text="过滤"
>
<template #activator="{ props }">
@@ -358,7 +304,7 @@ onMounted(() => {
<template #prepend>
<VIcon icon="mdi-refresh" />
</template>
{{ updateButtonText }}
更新
</VBtn>
<VBtn
:disabled="testButtonDisable"
@@ -419,143 +365,22 @@ onMounted(() => {
<VCardActions>
<VSpacer />
<VBtn @click="updateSiteCookie">
<VBtn
variant="tonal"
@click="updateSiteCookie"
>
开始更新
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 站点编辑弹窗 -->
<VDialog
v-model="siteInfoDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`编辑站点 - ${cardProps.site?.name}`">
<VCardText class="pt-2">
<DialogCloseBtn @click="siteInfoDialog = false" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn color="error" @click="deleteSiteInfo">
删除
</VBtn>
<VSpacer />
<VBtn @click="updateSiteInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<SiteAddEditForm
v-model="siteEditDialog"
:siteid="cardProps.site?.id"
@save="siteEditDialog = false; emit('update')"
@remove="emit('remove')"
@close="siteEditDialog = false"
/>
<!-- 站点资源弹窗 -->
<VDialog
v-model="resourceDialog"
@@ -668,6 +493,24 @@ onMounted(() => {
</VCardText>
</VCard>
</VDialog>
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import { calculateTimeDifference } from '@/@core/utils'
import { formatSeason } from '@/@core/utils/formatters'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
import type { Subscribe } from '@/api/types'
// 输入参数
const props = defineProps({
@@ -21,19 +21,7 @@ const $toast = useToast()
const imageLoaded = ref(false)
// 订阅弹窗
const subscribeInfoDialog = ref(false)
// 站点数据列表
const siteList = ref<Site[]>([])
// 站点选择下载框
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
// 订阅编辑表单
const subscribeForm = reactive<any>(props.media ?? {})
// 类型转换
subscribeForm.best_version = subscribeForm.best_version === 1
const subscribeEditDialog = ref(false)
// 上一次更新时间
const lastUpdateText = ref(
@@ -114,58 +102,9 @@ async function searchSubscribe() {
}
}
// 调用API修改订阅
async function updateSubscribeInfo() {
subscribeInfoDialog.value = false
try {
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 更新成功!`)
// 通知父组件刷新
emit('remove')
}
else { $toast.error(`${props.media?.name} 更新失败:${result.message}`) }
}
catch (e) {
console.log(e)
}
}
// 获取站点列表数据
async function loadSites() {
try {
const data: Site[] = await api.get('site/rss')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
console.error(error)
}
}
// 获取站点列表选择框数据
async function getSiteList() {
// 加载订阅站点列表
if (!siteList.value.length)
await loadSites()
const maps = siteList.value.map((item) => {
return {
title: item.name,
value: item.id,
}
})
selectSitesOptions.value = maps.flat()
}
// 编辑订阅响应
async function editSubscribeDialog() {
await getSiteList()
subscribeInfoDialog.value = true
subscribeEditDialog.value = true
}
// 弹出菜单
@@ -196,96 +135,12 @@ const dropdownItems = ref([
},
},
])
// 质量选择框数据
const qualityOptions = ref([
{
title: '全部',
value: '',
},
{
title: '蓝光原盘',
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
},
{
title: 'Remux',
value: 'Remux',
},
{
title: 'BluRay',
value: 'Blu-?Ray',
},
{
title: 'UHD',
value: 'UHD|UltraHD',
},
{
title: 'WEB-DL',
value: 'WEB-?DL|WEB-?RIP',
},
{
title: 'HDTV',
value: 'HDTV',
},
{
title: 'H265',
value: '[Hx].?265|HEVC',
},
{
title: 'H264',
value: '[Hx].?264|AVC',
},
])
// 分辨率选择框数据
const resolutionOptions = ref([
{
title: '全部',
value: '',
},
{
title: '8K',
value: '8K|4320p|x4320',
},
{
title: '4k',
value: '4K|2160p|x2160',
},
{
title: '1080p',
value: '1080[pi]|x1080',
},
{
title: '720p',
value: '720[pi]|x720',
},
])
// 特效选择框数据
const effectOptions = ref([
{
title: '全部',
value: '',
},
{
title: '杜比视界',
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
},
{
title: '杜比全景声',
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
},
{
title: 'HDR',
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
},
])
</script>
<template>
<VCard
:key="props.media?.id"
:class="`${subscribeForm.best_version ? 'outline-dashed outline-1' : ''}`"
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
@click="editSubscribeDialog"
>
<template #image>
@@ -407,133 +262,11 @@ const effectOptions = ref([
/>
</VCard>
<!-- 订阅编辑弹窗 -->
<VDialog
v-model="subscribeInfoDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`订阅 - ${props.media?.name}`">
<VCardText class="pt-2">
<DialogCloseBtn @click="subscribeInfoDialog = false" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.keyword"
label="搜索关键词"
/>
</VCol>
<VCol
v-if="props.media?.type === '电视剧'"
cols="12"
md="3"
>
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
v-if="props.media?.type === '电视剧'"
cols="12"
md="3"
>
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.quality"
label="质量"
:items="qualityOptions"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:items="resolutionOptions"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.effect"
label="特效"
:items="effectOptions"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSelect
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
label="订阅站点"
multiple
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="subscribeInfoDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="updateSubscribeInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="props.media?.id"
@remove="() => { emit('remove');subscribeEditDialog = false; }"
@save="() => { emit('save');subscribeEditDialog = false; }"
@close="subscribeEditDialog = false"
/>
</template>

View File

@@ -64,6 +64,9 @@ async function handleAddDownload(_site: any = undefined,
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)

View File

@@ -61,6 +61,9 @@ async function handleAddDownload(_site: any = undefined,
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)

View File

@@ -4,13 +4,12 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios'
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import ReorganizeForm from '../form/ReorganizeForm.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 输入参数
const inProps = defineProps({
@@ -32,6 +31,15 @@ const $toast = useToast()
// 是否正在加载
const loading = ref(true)
// 识别进度条
const progressDialog = ref(false)
// 识别进度文本
const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 确认框
const createConfirm = useConfirm()
@@ -53,57 +61,18 @@ const renamePopper = ref(false)
// 整理弹窗
const transferPopper = ref(false)
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 新名称
const newName = ref('')
// 当前名称
const currentItem = ref<FileItem>()
// 文件转移表单
const transferForm = reactive({
path: '',
target: '',
tmdbid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 目录过滤
const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
@@ -158,6 +127,9 @@ async function deleteItem(item: FileItem) {
dialogProps: {
maxWidth: '50rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
@@ -245,41 +217,6 @@ function showTransfer(item: FileItem) {
transferPopper.value = true
}
// 整理文件
async function transfer() {
transferForm.path = currentItem.value?.path ?? ''
// 开始整理文件
try {
// 关闭弹窗
transferPopper.value = false
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
// 异步调API结束后关闭进度条
api.post('transfer/manual', {}, {
params: transferForm,
}).then((res: any) => {
// 关闭进度条
progressDialog.value = false
// 停止监听进度
stopLoadingProgress()
// 显示结果
if (res.success) {
$toast.success(`${currentItem.value?.name} 整理完成!`)
// 重新加载
load()
}
else {
$toast.error(`${currentItem.value?.name} 整理失败:${res.message}`)
}
})
}
catch (e) {
console.log(e)
}
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
@@ -307,29 +244,6 @@ watch(
},
)
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 调用API识别
async function recognize(path: string) {
try {
@@ -586,23 +500,19 @@ onMounted(() => {
v-model="renamePopper"
max-width="50rem"
>
<template #activator="{ props }">
<IconBtn title="重命名" v-bind="props">
<VIcon icon="mdi-rename-outline" />
</IconBtn>
</template>
<VCard title="重命名">
<VCardText>
<VTextField v-model="newName" label="名称" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn depressed @click="renamePopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
:disabled="!newName"
depressed
variant="tonal"
@click="rename"
>
重命名
@@ -611,183 +521,44 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<VDialog
<ReorganizeForm
v-model="transferPopper"
max-width="50rem"
scrollable
>
<template #activator="{ props }">
<IconBtn title="整理" v-bind="props">
<VIcon icon="mdi-folder-arrow-right-outline" />
</IconBtn>
</template>
<VCard :title="`文件整理 - ${currentItem?.name}`">
<DialogCloseBtn @click="transferPopper = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="transferForm.target"
label="目的路径"
placeholder="留空自动"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: 'Rclone复制', value: 'rclone_copy' },
{ title: 'Rclone移动', value: 'rclone_move' },
]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[{ title: '请选择', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="transferPopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
@click="transfer"
>
开始整理
</VBtn>
</VCardActions>
</VCard>
</VDialog>
:path="currentItem?.path"
@done="transferPopper = false; load()"
@close="transferPopper = false"
/>
<!-- 手动整理进度框 -->
<vDialog
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<vCard
<VCard
color="primary"
>
<vCardText class="text-center">
<VCardText class="text-center">
{{ progressText }}
<vProgressLinear
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
</VCardText>
</VCard>
</VDialog>
<!-- 识别结果对话框 -->
<vDialog
<VDialog
v-model="nameTestDialog"
width="50rem"
>
<vCard>
<VCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>

View File

@@ -171,6 +171,7 @@ const sortIcon = computed(() => {
<VBtn
:disabled="!newFolderName"
depressed
variant="tonal"
@click="mkdir"
>
新建

View File

@@ -0,0 +1,307 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
import store from '@/store'
import api from '@/api'
import { numberValidator } from '@/@validators'
// 输入参数
const props = defineProps({
path: String,
target: String,
logids: Array<number>,
})
// 定义事件
const emit = defineEmits(['done', 'close'])
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 提示框
const $toast = useToast()
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 文件转移表单
const transferForm = reactive({
logid: 0,
path: '',
target: props.target ?? '',
tmdbid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
watchEffect(() => {
transferForm.path = props.path ?? ''
transferForm.target = props.target ?? ''
})
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 整理文件
// eslint-disable-next-line sonarjs/cognitive-complexity
async function transfer() {
if (!props.logids && !props.path)
return
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
if (props.path) {
// 文件整理
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
// 显示结果
if (result.success)
$toast.success(`${props.path} 整理完成!`)
else
$toast.error(`${props.path} 整理失败:${result.message}`)
}
catch (e) {
console.log(e)
}
}
else if (props.logids) {
// 日志整理
for (const logid of props.logids) {
transferForm.logid = logid
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
params: transferForm,
})
if (!result.success)
$toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
}
catch (e) {
console.log(e)
}
}
}
// 停止监听进度
stopLoadingProgress()
// 关闭进度条
progressDialog.value = false
// 重新加载
emit('done')
}
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VCard
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="transferForm.target"
label="目的路径"
placeholder="留空自动"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.transfer_type"
label="整理方式"
:items="[
{ title: '默认', value: '' },
{ title: '移动', value: 'move' },
{ title: '复制', value: 'copy' },
{ title: '硬链接', value: 'link' },
{ title: '软链接', value: 'softlink' },
{ title: 'Rclone复制', value: 'rclone_copy' },
{ title: 'Rclone移动', value: 'rclone_move' },
]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="transferForm.type_name"
label="类型"
:items="[{ title: '自动', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="emit('close')">
取消
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="transfer"
>
开始整理
</VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</VCardText>
</VCard>
</VDialog>
<!-- TMDB ID搜索框 -->
<VDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</VDialog>
</VDialog>
</template>

View File

@@ -0,0 +1,274 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import type { Site } from '@/api/types'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
// 输入参数
const props = defineProps({
siteid: Number,
oper: String,
})
// 注册事件
const emit = defineEmits(['save', 'remove', 'close'])
// 站点编辑表单数据
const siteForm = ref<Site>({
id: props.siteid ?? 0,
url: '',
rss: '',
cookie: '',
ua: '',
pri: 0,
is_active: true,
limit_interval: 0,
limit_seconds: 0,
name: '',
domain: '',
})
// 提示框
const $toast = useToast()
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 监控输入参数
watchEffect(async () => {
if (props.siteid)
fetchSiteInfo()
})
// 查询站点信息
async function fetchSiteInfo() {
try {
siteForm.value = await api.get(`site/${props.siteid}`)
siteForm.value.proxy = siteForm.value.proxy === 1
siteForm.value.render = siteForm.value.render === 1
}
catch (error) {
console.error(error)
}
}
// 调用API 新增站点
async function addSite() {
if (!siteForm.value?.url)
return
startNProgress()
try {
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
if (result.success) {
$toast.success('新增站点成功')
emit('save')
}
else { $toast.error(`新增站点失败:${result.message}`) }
}
catch (error) {
console.error(error)
}
doneNProgress()
}
// 调用API删除站点信息
async function deleteSiteInfo() {
try {
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
if (result.success)
emit('remove')
else $toast.error(`${siteForm.value?.name} 删除失败:${result.message}`)
}
catch (error) {
$toast.error(`${siteForm.value?.name} 删除失败!`)
console.error(error)
}
}
// 调用API更新站点信息
async function updateSiteInfo() {
startNProgress()
try {
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
if (result.success) {
$toast.success(`${siteForm.value?.name} 更新成功!`)
emit('save')
}
else { $toast.error(`${siteForm.value?.name} 更新失败:${result.message}`) }
}
catch (error) {
$toast.error(`${siteForm.value?.name} 更新失败!`)
console.error(error)
}
doneNProgress()
}
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VCard
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
v-if="props.oper === 'add'"
@click="emit('close')"
>
取消
</VBtn>
<VBtn
v-else
color="error"
@click="deleteSiteInfo"
>
删除
</VBtn>
<VSpacer />
<VBtn
v-if="props.oper === 'add'"
color="primary"
variant="tonal"
@click="addSite"
>
新增
</VBtn>
<VBtn
v-else
color="primary"
variant="tonal"
@click="updateSiteInfo"
>
保存
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,353 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
// 输入参数
const props = defineProps({
subid: Number,
})
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save', 'close'])
// 站点数据列表
const siteList = ref<Site[]>([])
// 站点选择下载框
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
// 订阅编辑表单
const subscribeForm = ref<Subscribe>({
id: props.subid ?? 0,
keyword: '',
quality: '',
resolution: '',
effect: '',
include: '',
exclude: '',
total_episode: 0,
start_episode: 0,
best_version: 0,
sites: [],
type: '',
name: '',
year: '',
tmdbid: 0,
state: '',
last_update: '',
username: '',
current_priority: 0,
})
// 提示框
const $toast = useToast()
// 调用API修改订阅
async function updateSubscribeInfo() {
try {
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value)
// 提示
if (result.success) {
$toast.success(`${subscribeForm.value.name} 更新成功!`)
// 通知父组件刷新
emit('save')
}
else { $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}`) }
}
catch (e) {
console.log(e)
}
}
// 获取站点列表数据
async function loadSites() {
try {
const data: Site[] = await api.get('site/rss')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
console.error(error)
}
}
// 获取站点列表选择框数据
async function getSiteList() {
// 加载订阅站点列表
if (!siteList.value.length)
await loadSites()
const maps = siteList.value.map((item) => {
return {
title: item.name,
value: item.id,
}
})
selectSitesOptions.value = maps.flat()
}
// 获取订阅信息
async function getSubscribeInfo() {
try {
const result: Subscribe = await api.get(
`subscribe/${props.subid}`,
)
subscribeForm.value = result
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
}
catch (e) {
console.log(e)
}
}
// 删除订阅
async function removeSubscribe() {
try {
const result: { [key: string]: any } = await api.delete(
`subscribe/${props.subid}`,
)
if (result.success) {
// 通知父组件刷新
emit('remove')
}
}
catch (e) {
console.log(e)
}
}
// 质量选择框数据
const qualityOptions = ref([
{
title: '全部',
value: '',
},
{
title: '蓝光原盘',
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
},
{
title: 'Remux',
value: 'Remux',
},
{
title: 'BluRay',
value: 'Blu-?Ray',
},
{
title: 'UHD',
value: 'UHD|UltraHD',
},
{
title: 'WEB-DL',
value: 'WEB-?DL|WEB-?RIP',
},
{
title: 'HDTV',
value: 'HDTV',
},
{
title: 'H265',
value: '[Hx].?265|HEVC',
},
{
title: 'H264',
value: '[Hx].?264|AVC',
},
])
// 分辨率选择框数据
const resolutionOptions = ref([
{
title: '全部',
value: '',
},
{
title: '4k',
value: '4K|2160p|x2160',
},
{
title: '1080p',
value: '1080[pi]|x1080',
},
{
title: '720p',
value: '720[pi]|x720',
},
])
// 特效选择框数据
const effectOptions = ref([
{
title: '全部',
value: '',
},
{
title: '杜比视界',
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
},
{
title: '杜比全景声',
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
},
{
title: 'HDR',
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
},
{
title: 'SDR',
value: '[\\s.]+SDR[\\s.]+',
},
])
watchEffect(() => {
if (props.subid) {
getSiteList()
getSubscribeInfo()
}
})
</script>
<template>
<VDialog
scrollable
max-width="60rem"
>
<VCard
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`"
class="rounded-t"
>
<VCardText class="pt-2">
<DialogCloseBtn @click="emit('close')" />
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="8"
>
<VTextField
v-model="subscribeForm.keyword"
label="搜索关键词"
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
v-if="subscribeForm.type === '电视剧'"
cols="12"
md="2"
>
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.quality"
label="质量"
:items="qualityOptions"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.resolution"
label="分辨率"
:items="resolutionOptions"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.effect"
label="特效"
:items="effectOptions"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-model="subscribeForm.sites"
:items="selectSitesOptions"
chips
label="订阅站点"
multiple
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VSwitch
v-model="subscribeForm.best_version"
label="洗版"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn color="error" @click="removeSubscribe">
取消订阅
</VBtn>
<VSpacer />
<VBtn
variant="tonal"
@click="updateSubscribeInfo"
>
保存
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -91,7 +91,6 @@ const superUser = store.state.auth.superUser
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
@@ -99,7 +98,6 @@ const superUser = store.state.auth.superUser
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '电视剧',
icon: 'mdi-television-classic',

View File

@@ -63,6 +63,7 @@ function openSearchDialog() {
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="search"
>
搜索

View File

@@ -38,6 +38,9 @@ async function restart() {
dialogProps: {
maxWidth: '30rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
@@ -121,6 +124,25 @@ const avatar = store.state.auth.avatar
<VListItemTitle>设定</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem
v-if="superUser"
@click="restart"
>
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
<VListItem
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
@@ -137,22 +159,6 @@ const avatar = store.state.auth.avatar
<VListItemTitle>帮助</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem @click="restart">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 Logout -->
<VListItem @click="logout">
<template #prepend>
@@ -170,21 +176,21 @@ const avatar = store.state.auth.avatar
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<vDialog
<VDialog
v-model="progressDialog"
width="25rem"
>
<vCard
<VCard
color="primary"
>
<vCardText class="text-center">
<VCardText class="text-center">
正在重启 ...
<vProgressLinear
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</vCardText>
</vCard>
</vDialog>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -13,6 +13,9 @@ const route = useRoute()
// 标题
const title = route.query?.title?.toString()
// 类型
const type = route.query?.type?.toString()
// 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths))
@@ -34,6 +37,7 @@ function getApiPath(paths: string[] | string) {
<PersonCardListView
:apipath="getApiPath(props.paths || '')"
:params="route.query"
:type="type"
/>
</div>
</template>

View File

@@ -33,7 +33,7 @@ const isImageLoaded = ref(false)
// 获取背景图片
async function fetchBackgroundImage() {
api
.get('/login/tmdb')
.get('/login/wallpaper')
.then((response: any) => {
backgroundImageUrl.value = response.message
})

View File

@@ -28,6 +28,18 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/movie_hot"
linkurl="/browse/douban/movie_hot?title=热门电影"
title="热门电影"
/>
<MediaCardSlideView
apipath="douban/tv_hot"
linkurl="/browse/douban/tv_hot?title=热门电视剧"
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/tv_animation"
linkurl="/browse/douban/tv_animation?title=热门动漫"

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import NoDataFound from '@/components/NoDataFound.vue'
import api from '@/api'
import type { Context } from '@/api/types'
import store from '@/store'
@@ -129,12 +130,14 @@ onMounted(() => {
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5">
<VBtn
v-if="viewType === 'list'"
size="x-large"
icon="mdi-view-grid"
color="primary"
@click="setViewType('card')"
/>
<VBtn
v-else
size="x-large"
icon="mdi-view-list"
color="primary"
@click="setViewType('list')"

View File

@@ -2,7 +2,6 @@
import { useRoute } from 'vue-router'
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
@@ -41,11 +40,6 @@ const tabs = [
icon: 'mdi-list-box',
tab: 'service',
},
{
title: '规则',
icon: 'mdi-filter-cog',
tab: 'filter',
},
{
title: '通知',
icon: 'mdi-bell',
@@ -117,13 +111,6 @@ const tabs = [
</transition>
</VWindowItem>
<!-- 规则 -->
<VWindowItem value="filter">
<transition name="fade-slide" appear>
<AccountSettingRule />
</transition>
</VWindowItem>
<!-- 通知 -->
<VWindowItem value="notification">
<transition name="fade-slide" appear>

View File

@@ -6,6 +6,10 @@ import { hexToRgb } from '@layouts/utils'
const vuetifyTheme = useTheme()
// 从Vuex Store中获取信息
const store = useStore()
const superUser = store.state.auth.superUser
const options = controlledComputed(
() => vuetifyTheme.name.value,
() => {
@@ -129,6 +133,7 @@ onMounted(() => {
</div>
<VBtn
v-if="superUser"
block
to="/history"
>

View File

@@ -156,7 +156,7 @@ async function fetchData({ done }: { done: any }) {
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有数据"
error-description="无法获取到TMDB媒体信息"
error-description="无法获取到媒体信息"
/>
</VInfiniteScroll>
</template>

View File

@@ -8,6 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
// 输入参数
const mediaProps = defineProps({
@@ -21,6 +22,9 @@ const $toast = useToast()
// 媒体详情
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 本地是否存在
const isExists = ref(false)
@@ -33,12 +37,15 @@ const isRefreshed = ref(false)
// 存储每一季的集信息
const seasonEpisodesInfo = ref({} as { [key: number]: TmdbEpisode[] })
// 各季缺失状态0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
// 各季缺失状态0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 各季的订阅状态
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号
const subscribeId = ref<number>()
// 调用API查询详情
async function getMediaDetail() {
if (mediaProps.mediaid && mediaProps.type) {
@@ -78,7 +85,7 @@ async function loadSeasonEpisodes(season: number) {
}
}
// 查询当前媒体是否已存在
// 查询当前媒体是否已入库
async function checkMovieExists() {
try {
const result: { [key: string]: any } = await api.get('media/exists', {
@@ -102,11 +109,12 @@ async function checkMovieExists() {
// 查询当前媒体是否已订阅
async function checkSubscribe(season = 0) {
try {
const mediaid = `tmdb:${mediaDetail.value.tmdb_id}`
const mediaid = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
season,
title: mediaDetail.value.title,
},
})
@@ -131,7 +139,7 @@ async function checkSeasonsNotExists() {
isExists.value = true
result.forEach((item) => {
// 0-已存在 1-部分缺失 2-全部缺失
// 0-已入库 1-部分缺失 2-全部缺失
let state = 0
if (item.episodes.length === 0)
state = 2
@@ -211,6 +219,12 @@ async function addSubscribe(season = 0) {
result.message,
best_version,
)
// 显示编辑弹窗
if (result.success) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
catch (error) {
console.error(error)
@@ -231,9 +245,7 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0)
subname = '洗版订阅'
if (result)
$toast.success(`${title} 添加${subname}成功!`)
else
if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
}
@@ -347,14 +359,14 @@ function getExistColor(season: number) {
function getExistText(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return '已存在'
return '已入库'
if (state === 1)
return '部分缺失'
else if (state === 2)
return '缺失'
else
return '已存在'
return '已入库'
}
// 计算订阅图标
@@ -380,10 +392,11 @@ function joinArray(arr: string[]) {
// 开始搜索
function handleSearch(area: string) {
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
router.push({
path: '/resource',
query: {
keyword: `tmdb:${mediaDetail.value.tmdb_id}`,
keyword,
type: mediaDetail.value.type,
area,
},
@@ -407,9 +420,9 @@ onBeforeMount(() => {
/>
</div>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
<VImg class="h-96" :src="mediaDetail.backdrop_path" cover />
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
</div>
<div class="vue-media-back absolute left-0 top-0 w-full h-96" />
</template>
@@ -445,7 +458,7 @@ onBeforeMount(() => {
</span>
</div>
<div class="media-actions">
<VBtn v-if="mediaDetail.tmdb_id" variant="tonal" color="info">
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info">
<template #prepend>
<VIcon icon="mdi-magnify" />
</template>
@@ -471,7 +484,7 @@ onBeforeMount(() => {
</VList>
</VMenu>
</VBtn>
<VBtn v-if="mediaDetail.type === '电影'" class="ms-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
<template #prepend>
<VIcon :icon="getSubscribeIcon" />
</template>
@@ -499,10 +512,6 @@ onBeforeMount(() => {
<span>{{ joinArray(director.roles) }}</span>
<a class="crew-name" :href="`${director.url}`" target="_blank">{{ director.name }}</a>
</li>
<li v-for="director in mediaDetail.actors" :key="director.id">
<span>{{ joinArray(director.roles) }}</span>
<a class="crew-name" :href="`${director.url}`" target="_blank">{{ director.name }}</a>
</li>
</ul>
<div class="mt-6">
<a v-if="mediaDetail.tmdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTheMovieDbLink()" target="_blank">
@@ -654,12 +663,56 @@ onBeforeMount(() => {
</div>
</div>
</div>
<div v-else-if="mediaDetail.douban_id" class="media-overview-right">
<div class="media-facts">
<div v-if="mediaDetail.vote_average" class="media-ratings">
<VRating
v-model="mediaDetail.vote_average"
density="compact"
length="10"
class="ma-2"
readonly
/>
</div>
<div v-if="mediaDetail.douban_id" class="media-fact">
<span>豆瓣ID</span>
<span class="media-fact-value">{{ mediaDetail.douban_id }}</span>
</div>
<div v-if="mediaDetail.original_title" class="media-fact">
<span>原始标题</span>
<span class="media-fact-value">{{ mediaDetail.original_title }}</span>
</div>
<div v-if="mediaDetail.release_date" class="media-fact">
<span>上映日期</span>
<span class="media-fact-value">
{{ mediaDetail.release_date }}
</span>
</div>
<div v-if="mediaDetail.production_countries" class="media-fact border-b-0">
<span>出品国家</span>
<span class="media-fact-value">
<span v-for="country in getProductionCountries" :key="country" class="flex items-center justify-end text-end">
{{ country }}
</span>
</span>
</div>
</div>
</div>
</div>
<div v-if="mediaDetail.tmdb_id">
<PersonCardSlideView
:apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`"
:linkurl="`/credits/tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}?title=演员阵容`"
:linkurl="`/credits/tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}?title=演员阵容&type=tmdb`"
title="演员阵容"
type="tmdb"
/>
</div>
<div v-else-if="mediaDetail.douban_id">
<PersonCardSlideView
:apipath="`douban/credits/${mediaDetail.douban_id}/${mediaProps.type}`"
:linkurl="`/credits/douban/credits/${mediaDetail.douban_id}/${mediaProps.type}?title=演员阵容&type=douban`"
title="演员阵容"
type="douban"
/>
</div>
<div v-if="mediaDetail.tmdb_id">
@@ -669,6 +722,13 @@ onBeforeMount(() => {
title="推荐"
/>
</div>
<div v-else-if="mediaDetail.douban_id">
<MediaCardSlideView
:apipath="`douban/recommend/${mediaDetail.douban_id}/${mediaProps.type}`"
:linkurl="`/browse/douban/recommend/${mediaDetail.douban_id}/${mediaProps.type}?title=推荐`"
title="推荐"
/>
</div>
<div v-if="mediaDetail.tmdb_id">
<MediaCardSlideView
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -682,7 +742,21 @@ onBeforeMount(() => {
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed"
error-code="500"
error-title="出错啦"
error-description="未识别到TMDB媒体信息"
error-description="未识别到媒体信息"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => {
subscribeEditDialog = false;
if (mediaDetail.type === '电影')
checkMovieSubscribed()
else
checkSeasonsSubscribed();
}"
/>
</template>

View File

@@ -1,13 +1,14 @@
<script lang="ts" setup>
import api from '@/api'
import type { TmdbPerson } from '@/api/types'
import PersonCard from '@/components/cards/PersonCard.vue'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
// 输入参数
const props = defineProps({
apipath: String,
params: Object as PropType<{ [key: string]: any }>,
type: String,
})
// 判断是否有滚动条
@@ -29,8 +30,8 @@ const loading = ref(false)
const isRefreshed = ref(false)
// 数据列表
const dataList = ref<TmdbPerson[]>([])
const currData = ref<TmdbPerson[]>([])
const dataList = ref<any>([])
const currData = ref<any>([])
// 获取列表数据
async function fetchData({ done }: { done: any }) {
@@ -135,11 +136,22 @@ async function fetchData({ done }: { done: any }) {
>
<template #loading />
<div
v-if="dataList.length > 0"
v-if="dataList.length > 0 && props.type === 'tmdb'"
class="grid gap-4 grid-media-card mx-3"
tabindex="0"
>
<PersonCard
<TmdbPersonCard
v-for="data in dataList"
:key="data.id"
:person="data"
/>
</div>
<div
v-if="dataList.length > 0 && props.type === 'douban'"
class="grid gap-4 grid-media-card mx-3"
tabindex="0"
>
<DoubanPersonCard
v-for="data in dataList"
:key="data.id"
:person="data"
@@ -149,7 +161,7 @@ async function fetchData({ done }: { done: any }) {
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有数据"
error-description="无法获取到TMDB媒体信息"
error-description="无法获取到媒体信息"
/>
</VInfiniteScroll>
</template>

View File

@@ -1,21 +1,22 @@
<script lang="ts" setup>
import PersionCard from '@/components/cards/PersonCard.vue'
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import api from '@/api'
import type { TmdbPerson } from '@/api/types'
import SlideView from '@/components/slide/SlideView.vue'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
// 输入参数
const props = defineProps({
apipath: String,
linkurl: String,
title: String,
type: String,
})
// 组件加载完成
const componentLoaded = ref(false)
// 数据列表
const dataList = ref<TmdbPerson[]>([])
const dataList = ref<any>([])
// 获取订阅列表数据
async function fetchData() {
@@ -46,7 +47,14 @@ onMounted(fetchData)
v-for="data in dataList"
:key="data.id"
>
<PersionCard
<TmdbPersonCard
v-if="props.type === 'tmdb'"
:person="data"
height="15rem"
width="10rem"
/>
<DoubanPersonCard
v-if="props.type === 'douban'"
:person="data"
height="15rem"
width="10rem"

View File

@@ -80,7 +80,7 @@ watchEffect(() => {
// group data
const key = `${torrent_info.title}_${torrent_info.size}`
if (groupMap.has(key)) {
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
// 已入库相同标题和大小的分组,将当前上下文信息添加到分组中
const group = groupMap.get(key)
group?.push(item)
}

View File

@@ -169,7 +169,7 @@ onMounted(() => {
>
{{ resolution }}
</VChip>
</vchipgroup>
</VChipGroup>
</VListItem>
<VListSubheader v-if="releaseGroupFilterOptions.length > 0">
制作组
@@ -186,7 +186,7 @@ onMounted(() => {
>
{{ releaseGroup }}
</VChip>
</vchipgroup>
</VChipGroup>
</VListItem>
<VListSubheader v-if="videoCodeFilterOptions.length > 0">
视频编码
@@ -203,7 +203,7 @@ onMounted(() => {
>
{{ videoCode }}
</VChip>
</vchipgroup>
</VChipGroup>
</VListItem>
<VListSubheader v-if="freeStateFilterOptions.length > 0">
促销状态
@@ -220,7 +220,7 @@ onMounted(() => {
>
{{ freeState }}
</VChip>
</vchipgroup>
</VChipGroup>
</VListItem>
<VListSubheader v-if="seasonFilterOptions.length > 0">
季集

View File

@@ -19,9 +19,9 @@ const getInstalledPluginList = computed(() => {
return dataList.value.filter(item => item.installed)
})
// 获取未安装的插件列表
// 获取未安装或者有更新的插件列表
const getUninstalledPluginList = computed(() => {
return dataList.value.filter(item => !item.installed)
return dataList.value.filter(item => !item.installed || item.has_update)
})
// 关闭插件市场窗口
@@ -84,13 +84,14 @@ onBeforeMount(fetchData)
<VDialog
v-model="PluginAppDialog"
fullscreen
scrollable
:scrim="false"
transition="dialog-bottom-transition"
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-plus"
icon="mdi-store-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
@@ -119,7 +120,7 @@ onBeforeMount(fetchData)
</VToolbarItems>
</VToolbar>
</div>
<div class="pa-4">
<VCardText>
<div class="grid gap-4 grid-plugin-card">
<PluginAppCard
v-for="data in getUninstalledPluginList"
@@ -134,7 +135,7 @@ onBeforeMount(fetchData)
error-title="没有未安装插件"
error-description="所有可用插件均已安装"
/>
</div>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -4,6 +4,11 @@ import api from '@/api'
import type { DownloadingInfo } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
import store from '@/store'
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
// 定时器
let refreshTimer: NodeJS.Timer | null = null
@@ -35,6 +40,14 @@ function onRefresh() {
loading.value = false
}
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => {
if (superUser)
return dataList.value
else
return dataList.value.filter(data => data.userid === userName)
})
// 加载时获取数据
onBeforeMount(() => {
fetchData()
@@ -71,17 +84,17 @@ onUnmounted(() => {
@refresh="onRefresh"
>
<div
v-if="dataList.length > 0"
v-if="filteredDataList.length > 0"
class="grid gap-3 grid-downloading-card"
>
<DownloadingCard
v-for="data in dataList"
v-for="data in filteredDataList"
:key="data.hash"
:info="data"
/>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
v-if="filteredDataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有任务"
error-description="正在下载的任务将会显示在这里"

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
// 提示框
const $toast = useToast()
@@ -12,22 +11,15 @@ const $toast = useToast()
// 重新整理对话框
const redoDialog = ref(false)
// TMDB编号
const redoTmdbId = ref('')
// 类型
const redoType = ref('电影')
// 类型下拉框:电影、电视剧
const redoTypeItems = ref([
{ title: '自动', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
])
// 当前操作记录
const currentHistory = ref<TransferHistory>()
// 重新整理IDS
const redoIds = ref<number[]>([])
// 重新整理target
const redoTarget = ref('')
// 已选中的数据
const selected = ref<TransferHistory[]>([])
@@ -55,7 +47,7 @@ const loading = ref(false)
const totalItems = ref(0)
// 每页条数
const itemsPerPage = ref(25)
const itemsPerPage = ref(50)
// 当前页码
const currentPage = ref(1)
@@ -69,9 +61,6 @@ const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 删除确认对话框
const deleteConfirmDialog = ref(false)
@@ -227,85 +216,17 @@ async function retransferBatch() {
return
// 清空当前操作记录
currentHistory.value = undefined
// 重新整理IDS
redoIds.value = selected.value.map(item => item.id)
// 重新整理target
if (selected.value.length === 1)
redoTarget.value = selected.value[0].dest ?? ''
else
redoTarget.value = ''
// 打开识别弹窗
redoType.value = ''
redoTmdbId.value = ''
redoDialog.value = true
}
// 调API重新整理
async function retransfer(item: TransferHistory, redoType = '', redoTmdbId = 0) {
try {
const result: { [key: string]: any } = await api.post(
'history/transfer',
item,
{
params: {
mtype: redoType,
new_tmdbid: redoTmdbId,
},
},
)
if (result.success) {
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
}
else {
$toast.error(`重新整理失败: ${result.message}`)
}
}
catch (e) {
console.log(e)
}
}
// 重新整理
async function rehandleHistory() {
try {
// 关闭弹窗
redoDialog.value = false
let tmdbid = 0
if (redoTmdbId.value)
tmdbid = parseInt(redoTmdbId.value)
// 转移当前选中记录
if (currentHistory.value) {
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
await retransfer(currentHistory.value, redoType.value, tmdbid)
}
else if (selected.value.length > 0) {
// 总条数
const total = selected.value.length
if (total === 0)
return
// 已处理条数
let handled = 0
// 显示进度条
progressDialog.value = true
for (const item of selected.value) {
progressText.value = `正在重新整理 ${item.src} ...`
await retransfer(item, redoType.value, tmdbid)
handled++
progressValue.value = handled / total * 100
}
// 清空选中项
selected.value = []
// 隐藏进度条
progressDialog.value = false
}
// 批量转移
else { $toast.error('没有选中任何记录!') }
}
catch (e) {
console.log(e)
}
}
// 弹出菜单
const dropdownItems = ref([
{
@@ -314,10 +235,9 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-redo-variant',
click: (item: TransferHistory) => {
redoTmdbId.value = ''
redoType.value = ''
redoIds.value = [item.id]
redoTarget.value = item.dest ?? ''
redoDialog.value = true
currentHistory.value = item
},
},
},
@@ -444,47 +364,11 @@ const dropdownItems = ref([
</template>
</VDataTableServer>
</VCard>
<VDialog
v-model="redoDialog"
max-width="50rem"
<!-- 底部操作按钮 -->
<span
v-if="selected.length > 0"
class="fixed right-5 bottom-5"
>
<!-- Dialog Content -->
<VCard title="重新整理">
<VCardText>
<VRow>
<VCol cols="12" md="4">
<VSelect
v-model="redoType"
label="类型"
:items="redoTypeItems"
/>
</VCol>
<VCol cols="12" md="8">
<VTextField
v-model="redoTmdbId"
label="TMDB编号"
placeholder="留空自动识别"
:disabled="redoType === ''"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner.stop="tmdbSelectorDialog = true"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
@click="rehandleHistory"
@keydown.enter="rehandleHistory"
>
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
<VBtn
icon="mdi-redo-variant"
class="me-2"
@@ -499,39 +383,9 @@ const dropdownItems = ref([
@click="removeHistoryBatch"
/>
</span>
<!-- 进度框 -->
<vDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<vCard
color="primary"
>
<vCardText class="text-center">
{{ progressText }}
<vProgressLinear
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
scrollable
>
<TmdbSelectorCard
v-model="redoTmdbId"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center">
<VCard class="text-center rounded-t">
<DialogCloseBtn @click="deleteConfirmDialog = false" />
<VCardTitle class="pe-10">
{{ confirmTitle }}
@@ -568,6 +422,24 @@ const dropdownItems = ref([
</div>
</VCard>
</VBottomSheet>
<!-- 文件整理弹窗 -->
<ReorganizeForm
v-model="redoDialog"
:logids="redoIds"
:target="redoTarget"
@done="() => {
redoDialog = false
// 清空当前操作记录
currentHistory = undefined
selected = []
// 刷新
fetchData({
page: currentPage,
itemsPerPage,
})
}"
@close="redoDialog = false"
/>
</template>
<style lang="scss">

View File

@@ -84,7 +84,7 @@ onMounted(() => {
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
当前版本
软件版本
</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
@@ -98,6 +98,30 @@ onMounted(() => {
</dd>
</div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
认证资源版本
</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemEnv.AUTH_VERSION }}</code>
</span>
</dd>
</div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">
站点资源版本
</dt>
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
<span class="flex-grow flex flex-row items-center truncate">
<code class="truncate">{{ systemEnv.INDEXER_VERSION }}</code>
</span>
</dd>
</div>
</div>
<div>
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="block text-sm font-bold">

View File

@@ -147,6 +147,10 @@ async function deactivateUser(user: User) {
// 新增用户
async function addUser() {
if (!userForm.name || !userForm.password || !userForm.email) {
$toast.error('请填写完整信息!')
return
}
try {
const result: { [key: string]: any } = await api.post('user', userForm)
if (result.success) {
@@ -447,6 +451,7 @@ onMounted(() => {
>
<VTextField
v-model="userForm.email"
:rules="[requiredValidator]"
label="邮箱"
/>
</VCol>

View File

@@ -1,163 +0,0 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
// 提示框
const $toast = useToast()
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryTorrentPriority()
queryDefaultFilter()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="下载优先规则">
<VCardSubtitle> 按站点优先级或资源种子数量排序和择优下载 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCol>
</VRow>
</vform>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在搜索和订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -24,6 +24,12 @@ const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询已设置优先级规则
async function queryCustomFilters() {
try {
@@ -190,9 +196,41 @@ function onLevelDown(pri: string) {
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultSearchFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultSearchFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryCustomFilters()
querySites()
queryDefaultFilter()
})
</script>
@@ -260,6 +298,39 @@ onMounted(() => {
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在搜索时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -14,6 +14,15 @@ const resetSitesText = ref('重置站点数据')
// 站点重置按钮可用状态
const resetSitesDisabled = ref(false)
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 重置站点
async function resetSites() {
try {
@@ -34,10 +43,74 @@ async function resetSites() {
console.log(error)
}
}
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryTorrentPriority()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="下载优先规则">
<VCardSubtitle> 按站点或做种数量优先下载 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="站点重置">
<VCardText>

View File

@@ -27,6 +27,12 @@ const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedRssSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询用户选中的订阅站点
async function querySelectedRssSites() {
try {
@@ -207,10 +213,42 @@ function onLevelDown(filterCards: FilterCard[], pri: string) {
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
querySites()
queryCustomFilters('SubscribeFilterRules')
queryCustomFilters('BestVersionFilterRules')
queryDefaultFilter()
})
</script>
@@ -314,6 +352,39 @@ onMounted(() => {
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>

View File

@@ -166,13 +166,26 @@ onMounted(() => {
<VTextarea
v-model="customIdentifiers"
auto-grow
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持以下几种配置格式:
屏蔽词
被替换词 => 替换词
前定位词 <> 后定位词 >> 集偏移量EP
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP"
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组"
/>
</VCardItem>
<VCardItem>
<VAlert
type="info"
variant="tonal"
title="支持的配置格式:"
>
<span
v-html="`
屏蔽词<br>
被替换词 => 替换词<br>
前定位词 <> 后定位词 >> 集偏移量EP<br>
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP<br>
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选<br>
`"
/>
</VAlert>
</VCardItem>
<VCardItem>
<VBtn
type="submit"

View File

@@ -1,14 +1,9 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { numberValidator, requiredValidator } from '@/@validators'
import { doneNProgress, startNProgress } from '@/api/nprogress'
// 提示框
const $toast = useToast()
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
// 数据列表
const dataList = ref<Site[]>([])
@@ -16,45 +11,9 @@ const dataList = ref<Site[]>([])
// 是否刷新过
const isRefreshed = ref(false)
// 新增按钮文本
const addBtnText = ref('新增站点')
// 新增按钮状态
const addBtnState = ref(false)
// 新增站点对话框
const siteAddDialog = ref(false)
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 站点编辑表单数据
const siteForm = reactive<Site>({
id: 0,
url: '',
pri: 1,
is_active: true,
cookie: '',
ua: '',
limit_interval: 0,
limit_seconds: 0,
limit_count: 0,
proxy: 0,
render: 0,
name: '',
domain: '',
})
// 获取站点列表数据
async function fetchData() {
try {
@@ -66,38 +25,6 @@ async function fetchData() {
}
}
// 调用API 新增站点
async function addSite() {
if (!siteForm.url)
return
startNProgress()
addBtnText.value = '新增中...'
addBtnState.value = true
try {
const result: { [key: string]: string } = await api.post('site/', siteForm)
if (result.success) {
$toast.success('新增站点成功')
// 刷新数据
fetchData()
}
else { $toast.error(`新增站点失败:${result.message}`) }
siteAddDialog.value = false
}
catch (error) {
console.error(error)
}
doneNProgress()
addBtnText.value = '新增站点'
addBtnState.value = false
}
// 加载时获取数据
onBeforeMount(fetchData)
</script>
@@ -132,150 +59,20 @@ onBeforeMount(fetchData)
error-title="没有站点"
error-description="已添加并支持的站点将会在这里显示"
/>
<!-- Dialog Content -->
<VDialog
<!-- 新增站点按钮 -->
<VBtn
icon="mdi-plus"
size="x-large"
class="fixed right-5 bottom-5"
oper="add"
@click="siteAddDialog = true"
/>
<SiteAddEditForm
v-model="siteAddDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
/>
</template>
<VCard title="新增站点">
<DialogCloseBtn @click="siteAddDialog = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
@click="siteAddDialog = false"
>
取消
</VBtn>
<VSpacer />
<VBtn
color="primary"
:disabled="addBtnState"
@click="addSite"
>
{{ addBtnText }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
oper="add"
@save="siteAddDialog = false; fetchData()"
@close="siteAddDialog = false"
/>
</template>
<style lang="scss">

View File

@@ -4,12 +4,17 @@ import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import store from '@/store'
// 输入参数
const props = defineProps({
type: String,
})
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
// 是否刷新过
const isRefreshed = ref(false)
@@ -40,9 +45,12 @@ function onRefresh() {
loading.value = false
}
// 过滤数据
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => {
return dataList.value.filter(data => data.type === props.type)
if (superUser)
return dataList.value.filter(data => data.type === props.type)
else
return dataList.value.filter(data => data.type === props.type && data.username === userName)
})
</script>