Files
MoviePilot-Frontend/src/components/dialog/ReorganizeDialog.vue

468 lines
15 KiB
Vue

<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import api from '@/api'
import { transferTypeOptions } from '@/api/constants'
import { numberValidator } from '@/@validators'
import { useDisplay } from 'vuetify'
import ProgressDialog from './ProgressDialog.vue'
import { FileItem, StorageConf, TransferDirectoryConf, TransferForm } from '@/api/types'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
// 输入参数
const props = defineProps({
logids: Array<number>,
items: Array<FileItem>,
target_storage: String,
target_path: String,
})
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 当前识别类型
const mediaSource = ref(globalSettings.data?.RECOGNIZE_SOURCE || 'themoviedb')
// 定义事件
const emit = defineEmits(['done', 'close'])
// 生成1到100季的下拉框选项
const seasonItems = ref(
Array.from({ length: 101 }, (_, i) => i).map(item => ({
title: `${t('dialog.subscribeEdit.seasonFormat', { number: item })}`,
value: item,
})),
)
// 提示框
const $toast = useToast()
// TMDB选择对话框
const mediaSelectorDialog = ref(false)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref(t('dialog.reorganize.processing'))
// 整理进度
const progressValue = ref(0)
// 所有存储
const storages = ref<StorageConf[]>([])
// 查询存储
async function loadStorages() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Storages')
storages.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 存储字典
const storageOptions = computed(() => {
return storages.value.map(item => ({
title: item.name,
value: item.type,
}))
})
// 标题
const dialogTitle = computed(() => {
if (props.items) {
if (props.items.length > 1) return t('dialog.reorganize.multipleItemsTitle', { count: props.items.length })
return t('dialog.reorganize.singleItemTitle', { path: props.items[0].path })
} else if (props.logids) {
return t('dialog.reorganize.multipleItemsTitle', { count: props.logids.length })
}
return t('dialog.reorganize.manualTitle')
})
// 禁用指定集数
const disableEpisodeDetail = computed(() => {
if (props.items) {
if (transferForm.episode_format) return false
return !(props.items.length === 1 && props.items[0].type !== 'dir')
}
})
// 表单
const transferForm = reactive<TransferForm>({
fileitem: {} as FileItem,
logid: 0,
target_storage: props.target_storage ?? 'local',
transfer_type: '',
target_path: '',
min_filesize: 0,
scrape: false,
from_history: false,
})
// 所有媒体库目录
const directories = ref<TransferDirectoryConf[]>([])
// 查询目录
async function loadDirectories() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Directories')
directories.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 目的目录下拉框
const targetDirectories = computed(() => {
const libraryDirectories = directories.value.map(item => item.library_path)
return [...new Set(libraryDirectories)]
})
// 监听目的路径变化,配置默认值
watch(
() => transferForm.target_path,
async newPath => {
if (newPath) {
const directory = directories.value.find(item => item.library_path === newPath)
if (directory) {
transferForm.target_storage = directory.library_storage ?? 'local'
transferForm.transfer_type = transferForm.transfer_type || directory.transfer_type
transferForm.scrape = directory.scraping ?? false
transferForm.library_category_folder = directory.library_category_folder ?? false
transferForm.library_type_folder = directory.library_type_folder ?? false
} else {
transferForm.transfer_type = transferForm.transfer_type || 'copy'
transferForm.scrape = false
transferForm.library_category_folder = false
transferForm.library_type_folder = false
}
} else {
// 路径为空时, 恢复到`自动`条件
transferForm.transfer_type = ''
transferForm.library_type_folder = undefined
transferForm.library_category_folder = undefined
}
},
)
// 整理文件
async function handleTransfer(item: FileItem, background: boolean = false) {
transferForm.fileitem = item
transferForm.logid = 0
try {
const result: { [key: string]: any } = await api.post(`transfer/manual?background=${background}`, transferForm)
if (!result.success) $toast.error(result.message)
else if (background) $toast.success(t('dialog.reorganize.successMessage', { name: item.name }))
} catch (e) {
console.log(e)
}
}
// 整理日志
async function handleTransferLog(logid: number, background: boolean = false) {
transferForm.logid = logid
transferForm.fileitem = {} as FileItem
try {
const result: { [key: string]: any } = await api.post(`transfer/manual?background=${background}`, transferForm)
if (!result.success) $toast.error(result.message)
else if (background) $toast.success(`历史记录 ${logid} 已加入整理队列!`)
} catch (e) {
console.log(e)
}
}
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = t('dialog.reorganize.processing')
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
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()
}
// 整理文件
async function transfer(background: boolean = false) {
if (!props.logids && !props.items) return
// 显示进度条
progressDialog.value = true
if (!background) {
// 开始监听进度
startLoadingProgress()
}
// 文件整理
if (props.items) {
for (const item of props.items) {
await handleTransfer(item, background)
}
}
// 日志整理
if (props.logids) {
for (const logid of props.logids) {
await handleTransferLog(logid, background)
}
}
if (!background) {
// 停止监听进度
stopLoadingProgress()
}
// 关闭进度条
progressDialog.value = false
// 重新加载
emit('done')
}
onMounted(() => {
loadDirectories()
loadStorages()
})
onUnmounted(() => {
stopLoadingProgress()
})
</script>
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="dialogTitle" class="rounded-t">
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="transferForm.target_storage"
:items="storageOptions"
:label="t('dialog.reorganize.targetStorage')"
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetStorageHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="transferForm.transfer_type"
:label="t('dialog.reorganize.transferType')"
:items="transferTypeOptions"
:hint="t('dialog.reorganize.transferTypeHint')"
persistent-hint
>
<template v-slot:selection="{ item }">
{{ transferForm.transfer_type === '' ? t('dialog.reorganize.auto') : item.title }}
</template>
</VSelect>
</VCol>
<VCol cols="12">
<VCombobox
v-model="transferForm.target_path"
:items="targetDirectories"
:label="t('dialog.reorganize.targetPath')"
:placeholder="t('dialog.reorganize.targetPathPlaceholder')"
:hint="t('dialog.reorganize.targetPathHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="transferForm.type_name"
:label="t('dialog.reorganize.mediaType')"
:items="[
{ title: t('dialog.reorganize.auto'), value: '' },
{ title: t('dialog.reorganize.movie'), value: '电影' },
{ title: t('dialog.reorganize.tv'), value: '电视剧' },
]"
:hint="t('dialog.reorganize.mediaTypeHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="transferForm.tmdbid"
:disabled="transferForm.type_name === ''"
:label="t('dialog.reorganize.tmdbId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
v-else
v-model="transferForm.doubanid"
:disabled="transferForm.type_name === ''"
:label="t('dialog.reorganize.doubanId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
</VRow>
<VRow v-show="transferForm.type_name === '电视剧'">
<VCol cols="12" md="6">
<VTextField
v-model="transferForm.episode_group"
:label="t('dialog.reorganize.episodeGroup')"
:placeholder="t('dialog.reorganize.episodeGroupPlaceholder')"
:hint="t('dialog.reorganize.episodeGroupHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VSelect
v-model.number="transferForm.season"
:label="t('dialog.reorganize.season')"
:items="seasonItems"
:hint="t('dialog.reorganize.seasonHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="3">
<VTextField
v-model="transferForm.episode_detail"
:disabled="disableEpisodeDetail"
:label="t('dialog.reorganize.episodeDetail')"
:placeholder="t('dialog.reorganize.episodeDetailPlaceholder')"
:hint="t('dialog.reorganize.episodeDetailHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="transferForm.episode_format"
:label="t('dialog.reorganize.episodeFormat')"
:placeholder="t('dialog.reorganize.episodeFormatPlaceholder')"
:hint="t('dialog.reorganize.episodeFormatHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="transferForm.episode_offset"
:label="t('dialog.reorganize.episodeOffset')"
:placeholder="t('dialog.reorganize.episodeOffsetPlaceholder')"
:hint="t('dialog.reorganize.episodeOffsetHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="transferForm.episode_part"
:label="t('dialog.reorganize.episodePart')"
:placeholder="t('dialog.reorganize.episodePartPlaceholder')"
:hint="t('dialog.reorganize.episodePartHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="transferForm.min_filesize"
:label="t('dialog.reorganize.minFileSize')"
:rules="[numberValidator]"
placeholder="0"
:hint="t('dialog.reorganize.minFileSizeHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VSwitch
v-model="transferForm.library_type_folder"
:label="t('dialog.reorganize.typeFolderOption')"
:hint="t('dialog.reorganize.typeFolderHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6" v-if="transferForm.target_path">
<VSwitch
v-model="transferForm.library_category_folder"
:label="t('dialog.reorganize.categoryFolderOption')"
:hint="t('dialog.reorganize.categoryFolderHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="transferForm.scrape"
:label="t('dialog.reorganize.scrapeOption')"
:hint="t('dialog.reorganize.scrapeHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6" v-if="props.logids">
<VSwitch
v-model="transferForm.from_history"
:label="t('dialog.reorganize.fromHistoryOption')"
:hint="t('dialog.reorganize.fromHistoryHint')"
persistent-hint
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions class="pt-3">
<VSpacer />
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
{{ t('dialog.reorganize.addToQueue') }}
</VBtn>
<VBtn variant="elevated" @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
{{ t('dialog.reorganize.reorganizeNow') }}
</VBtn>
</VCardActions>
</VCard>
<!-- 手动整理进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
<!-- TMDB ID搜索框 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="transferForm.tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector
v-else
v-model="transferForm.doubanid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
</VDialog>
</VDialog>
</template>