mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-27 11:10:12 +08:00
feat: 完成绿联媒体服务前端接入与展示优化
This commit is contained in:
@@ -80,6 +80,10 @@ export const mediaServerOptions = [
|
|||||||
value: 'trimemedia',
|
value: 'trimemedia',
|
||||||
title: i18n.global.t('setting.system.trimeMedia'),
|
title: i18n.global.t('setting.system.trimeMedia'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'ugreen',
|
||||||
|
title: i18n.global.t('setting.system.ugreen'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
|
export const mediaServerDict = mediaServerOptions.reduce((dict, item) => {
|
||||||
|
|||||||
@@ -885,8 +885,8 @@ export interface MediaStatistic {
|
|||||||
movie_count: number
|
movie_count: number
|
||||||
// 电视剧总数
|
// 电视剧总数
|
||||||
tv_count: number
|
tv_count: number
|
||||||
// 电视剧总集数
|
// 电视剧总集数,未获取时为 null
|
||||||
episode_count: number
|
episode_count: number | null
|
||||||
// 用户数量
|
// 用户数量
|
||||||
user_count: number
|
user_count: number
|
||||||
}
|
}
|
||||||
@@ -1134,7 +1134,7 @@ export interface StorageConf {
|
|||||||
export interface MediaServerConf {
|
export interface MediaServerConf {
|
||||||
// 名称
|
// 名称
|
||||||
name: string
|
name: string
|
||||||
// 类型 emby/jellyfin/plex
|
// 类型 emby/jellyfin/plex/trimemedia/ugreen
|
||||||
type: string
|
type: string
|
||||||
// 配置
|
// 配置
|
||||||
config: { [key: string]: any }
|
config: { [key: string]: any }
|
||||||
|
|||||||
BIN
src/assets/images/logos/ugreen.jpg
Normal file
BIN
src/assets/images/logos/ugreen.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MediaServerPlayItem } from '@/api/types'
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import noImage from '@images/no-image.jpeg'
|
||||||
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
|
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -10,12 +11,18 @@ const props = defineProps({
|
|||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
|
const imageLoadError = ref(false)
|
||||||
|
|
||||||
// 图片加载完成响应
|
// 图片加载完成响应
|
||||||
function imageLoadHandler() {
|
function imageLoadHandler() {
|
||||||
imageLoaded.value = true
|
imageLoaded.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图片加载失败响应
|
||||||
|
function imageErrorHandler() {
|
||||||
|
imageLoadError.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 跳转播放
|
// 跳转播放
|
||||||
async function goPlay() {
|
async function goPlay() {
|
||||||
if (props.media?.link) {
|
if (props.media?.link) {
|
||||||
@@ -26,6 +33,7 @@ async function goPlay() {
|
|||||||
// 计算图片地址
|
// 计算图片地址
|
||||||
const getImgUrl = computed(() => {
|
const getImgUrl = computed(() => {
|
||||||
const image = props.media?.image || ''
|
const image = props.media?.image || ''
|
||||||
|
if (!image || imageLoadError.value) return noImage
|
||||||
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||||
const use_cookies = props.media?.use_cookies
|
const use_cookies = props.media?.use_cookies
|
||||||
if (use_cookies) {
|
if (use_cookies) {
|
||||||
@@ -50,7 +58,7 @@ const getImgUrl = computed(() => {
|
|||||||
@click="goPlay"
|
@click="goPlay"
|
||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler">
|
<VImg :src="getImgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
|
||||||
<template #placeholder>
|
<template #placeholder>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function imageLoadHandler() {
|
|||||||
// 图片加载错误
|
// 图片加载错误
|
||||||
function imageErrorHandler() {
|
function imageErrorHandler() {
|
||||||
imageError.value = true
|
imageError.value = true
|
||||||
|
imgUrl.value = getDefaultImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认图片
|
// 默认图片
|
||||||
@@ -41,6 +42,7 @@ function getDefaultImage() {
|
|||||||
else if (props.media?.server_type === 'emby') return emby
|
else if (props.media?.server_type === 'emby') return emby
|
||||||
else if (props.media?.server_type === 'jellyfin') return jellyfin
|
else if (props.media?.server_type === 'jellyfin') return jellyfin
|
||||||
else if (props.media?.server_type === 'trimemedia') return getLogoUrl('trimemedia')
|
else if (props.media?.server_type === 'trimemedia') return getLogoUrl('trimemedia')
|
||||||
|
else if (props.media?.server_type === 'ugreen') return getLogoUrl('ugreen')
|
||||||
else return plex
|
else return plex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ async function goPlay() {
|
|||||||
|
|
||||||
// 生成图片代理路径
|
// 生成图片代理路径
|
||||||
function getImgUrl(url: string, use_cookies?: boolean) {
|
function getImgUrl(url: string, use_cookies?: boolean) {
|
||||||
if (!url) return getDefaultImage()
|
if (!url || imageError.value) return getDefaultImage()
|
||||||
let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||||
if (use_cookies) {
|
if (use_cookies) {
|
||||||
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||||
@@ -64,7 +66,7 @@ function getImgUrl(url: string, use_cookies?: boolean) {
|
|||||||
// 根据多张图片生成媒体库封面
|
// 根据多张图片生成媒体库封面
|
||||||
async function drawImages(imageList: string[], use_cookies?: boolean) {
|
async function drawImages(imageList: string[], use_cookies?: boolean) {
|
||||||
// 图片
|
// 图片
|
||||||
const IMAGES = imageList
|
const IMAGES = [...imageList]
|
||||||
if (IMAGES.length === 0) return getDefaultImage()
|
if (IMAGES.length === 0) return getDefaultImage()
|
||||||
|
|
||||||
// 为所有图片添加system/img前缀
|
// 为所有图片添加system/img前缀
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const ugreenScanModeOptions = computed(() => [
|
||||||
|
{ title: t('mediaserver.scanModeOptions.newAndModified'), value: 'new_and_modified' },
|
||||||
|
{ title: t('mediaserver.scanModeOptions.supplementMissing'), value: 'supplement_missing' },
|
||||||
|
{ title: t('mediaserver.scanModeOptions.fullOverride'), value: 'full_override' },
|
||||||
|
])
|
||||||
|
|
||||||
// 媒体服务器详情弹窗
|
// 媒体服务器详情弹窗
|
||||||
const mediaServerInfoDialog = ref(false)
|
const mediaServerInfoDialog = ref(false)
|
||||||
|
|
||||||
@@ -77,6 +83,12 @@ function openMediaServerInfoDialog() {
|
|||||||
loadLibrary(props.mediaserver.name)
|
loadLibrary(props.mediaserver.name)
|
||||||
// 深复制
|
// 深复制
|
||||||
mediaServerInfo.value = cloneDeep(props.mediaserver)
|
mediaServerInfo.value = cloneDeep(props.mediaserver)
|
||||||
|
if (mediaServerInfo.value.type === 'ugreen') {
|
||||||
|
mediaServerInfo.value.config = mediaServerInfo.value.config || {}
|
||||||
|
}
|
||||||
|
if (mediaServerInfo.value.type === 'ugreen' && !mediaServerInfo.value.config.scan_mode) {
|
||||||
|
mediaServerInfo.value.config.scan_mode = 'supplement_missing'
|
||||||
|
}
|
||||||
mediaServerInfoDialog.value = true
|
mediaServerInfoDialog.value = true
|
||||||
if (!props.mediaserver.sync_libraries) {
|
if (!props.mediaserver.sync_libraries) {
|
||||||
mediaServerInfo.value.sync_libraries = ['all']
|
mediaServerInfo.value.sync_libraries = ['all']
|
||||||
@@ -110,6 +122,8 @@ const getIcon = computed(() => {
|
|||||||
return getLogoUrl('jellyfin')
|
return getLogoUrl('jellyfin')
|
||||||
case 'trimemedia':
|
case 'trimemedia':
|
||||||
return getLogoUrl('trimemedia')
|
return getLogoUrl('trimemedia')
|
||||||
|
case 'ugreen':
|
||||||
|
return getLogoUrl('ugreen')
|
||||||
case 'plex':
|
case 'plex':
|
||||||
return getLogoUrl('plex')
|
return getLogoUrl('plex')
|
||||||
default:
|
default:
|
||||||
@@ -424,6 +438,85 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VRow v-else-if="mediaServerInfo.type == 'ugreen'">
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerInfo.name"
|
||||||
|
:label="t('common.name')"
|
||||||
|
:placeholder="t('mediaserver.nameRequired')"
|
||||||
|
:hint="t('mediaserver.serverAlias')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-label"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerInfo.config.host"
|
||||||
|
:label="t('mediaserver.host')"
|
||||||
|
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||||
|
:hint="t('mediaserver.hostHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-server"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerInfo.config.play_host"
|
||||||
|
:label="t('mediaserver.playHost')"
|
||||||
|
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||||
|
:hint="t('mediaserver.playHostHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-play-network"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerInfo.config.username"
|
||||||
|
:label="t('mediaserver.username')"
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
type="password"
|
||||||
|
v-model="mediaServerInfo.config.password"
|
||||||
|
:label="t('mediaserver.password')"
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VAutocomplete
|
||||||
|
v-model="mediaServerInfo.sync_libraries"
|
||||||
|
:label="t('mediaserver.syncLibraries')"
|
||||||
|
:items="librariesOptions"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
:hint="t('mediaserver.syncLibrariesHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
append-inner-icon="mdi-refresh"
|
||||||
|
prepend-inner-icon="mdi-library"
|
||||||
|
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="mediaServerInfo.config.scan_mode"
|
||||||
|
:label="t('mediaserver.scanMode')"
|
||||||
|
:items="ugreenScanModeOptions"
|
||||||
|
:hint="t('mediaserver.scanModeHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-radar"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
<VRow v-else-if="mediaServerInfo.type == 'plex'">
|
<VRow v-else-if="mediaServerInfo.type == 'plex'">
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export function useSetupWizard() {
|
|||||||
'jellyfin': 'JellyfinModule',
|
'jellyfin': 'JellyfinModule',
|
||||||
'plex': 'PlexModule',
|
'plex': 'PlexModule',
|
||||||
'trimemedia': 'TrimeMediaModule',
|
'trimemedia': 'TrimeMediaModule',
|
||||||
|
'ugreen': 'UgreenModule',
|
||||||
},
|
},
|
||||||
// 通知映射
|
// 通知映射
|
||||||
notification: {
|
notification: {
|
||||||
@@ -405,7 +406,7 @@ export function useSetupWizard() {
|
|||||||
errors.push(t('mediaserver.tokenRequired'))
|
errors.push(t('mediaserver.tokenRequired'))
|
||||||
validationErrors.value.mediaServer.token = true
|
validationErrors.value.mediaServer.token = true
|
||||||
}
|
}
|
||||||
} else if (wizardData.value.mediaServer.type === 'trimemedia') {
|
} else if (wizardData.value.mediaServer.type === 'trimemedia' || wizardData.value.mediaServer.type === 'ugreen') {
|
||||||
if (!wizardData.value.mediaServer.config?.username?.trim()) {
|
if (!wizardData.value.mediaServer.config?.username?.trim()) {
|
||||||
errors.push(t('mediaserver.usernameRequired'))
|
errors.push(t('mediaserver.usernameRequired'))
|
||||||
validationErrors.value.mediaServer.username = true
|
validationErrors.value.mediaServer.username = true
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default {
|
|||||||
unsubscribe: 'Unsubscribe',
|
unsubscribe: 'Unsubscribe',
|
||||||
media: 'Media',
|
media: 'Media',
|
||||||
unknown: 'Unknown',
|
unknown: 'Unknown',
|
||||||
|
notFetched: 'Not Fetched',
|
||||||
notice: 'Notice',
|
notice: 'Notice',
|
||||||
itemsPerPage: 'Items per page',
|
itemsPerPage: 'Items per page',
|
||||||
pageText: '{0}-{1} of {2}',
|
pageText: '{0}-{1} of {2}',
|
||||||
@@ -314,7 +315,8 @@ export default {
|
|||||||
settingTabs: {
|
settingTabs: {
|
||||||
system: {
|
system: {
|
||||||
title: 'System',
|
title: 'System',
|
||||||
description: 'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex)',
|
description:
|
||||||
|
'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex, TrimeMedia, Ugreen)',
|
||||||
},
|
},
|
||||||
directory: {
|
directory: {
|
||||||
title: 'Storage & Directories',
|
title: 'Storage & Directories',
|
||||||
@@ -1341,6 +1343,7 @@ export default {
|
|||||||
emby: 'Emby',
|
emby: 'Emby',
|
||||||
jellyfin: 'Jellyfin',
|
jellyfin: 'Jellyfin',
|
||||||
plex: 'Plex',
|
plex: 'Plex',
|
||||||
|
ugreen: 'Ugreen',
|
||||||
reloadSuccess: 'System configuration has taken effect',
|
reloadSuccess: 'System configuration has taken effect',
|
||||||
reloadFailed: 'Failed to reload system!',
|
reloadFailed: 'Failed to reload system!',
|
||||||
auxAuthEnable: 'User Auxiliary Authentication',
|
auxAuthEnable: 'User Auxiliary Authentication',
|
||||||
@@ -2853,6 +2856,13 @@ export default {
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
syncLibraries: 'Sync Libraries',
|
syncLibraries: 'Sync Libraries',
|
||||||
syncLibrariesHint: 'Only selected libraries will be synchronized',
|
syncLibrariesHint: 'Only selected libraries will be synchronized',
|
||||||
|
scanMode: 'Scan Mode',
|
||||||
|
scanModeHint: 'Applies to full-library and targeted refresh: New & Modified / Supplement Missing / Full Override',
|
||||||
|
scanModeOptions: {
|
||||||
|
newAndModified: 'New & Modified',
|
||||||
|
supplementMissing: 'Supplement Missing',
|
||||||
|
fullOverride: 'Full Override',
|
||||||
|
},
|
||||||
hostRequired: 'Host cannot be empty',
|
hostRequired: 'Host cannot be empty',
|
||||||
apiKeyRequired: 'API Key cannot be empty',
|
apiKeyRequired: 'API Key cannot be empty',
|
||||||
tokenRequired: 'Token cannot be empty',
|
tokenRequired: 'Token cannot be empty',
|
||||||
@@ -3160,7 +3170,8 @@ export default {
|
|||||||
title: 'Media Server',
|
title: 'Media Server',
|
||||||
description: 'Configure media server',
|
description: 'Configure media server',
|
||||||
info: 'Media Server Configuration',
|
info: 'Media Server Configuration',
|
||||||
infoDesc: 'Configure media server for media library management, can choose Emby, Jellyfin or Plex etc.',
|
infoDesc:
|
||||||
|
'Configure media server for media library management, can choose Emby, Jellyfin, Plex, TrimeMedia or Ugreen.',
|
||||||
type: 'Media Server Type',
|
type: 'Media Server Type',
|
||||||
typeHint: 'Select the type of media server to use',
|
typeHint: 'Select the type of media server to use',
|
||||||
name: 'Server Name',
|
name: 'Server Name',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default {
|
|||||||
unsubscribe: '取消订阅',
|
unsubscribe: '取消订阅',
|
||||||
media: '媒体',
|
media: '媒体',
|
||||||
unknown: '未知',
|
unknown: '未知',
|
||||||
|
notFetched: '未获取',
|
||||||
notice: '注意',
|
notice: '注意',
|
||||||
itemsPerPage: '每页条数',
|
itemsPerPage: '每页条数',
|
||||||
pageText: '{0}-{1} 共 {2} 条',
|
pageText: '{0}-{1} 共 {2} 条',
|
||||||
@@ -313,7 +314,7 @@ export default {
|
|||||||
settingTabs: {
|
settingTabs: {
|
||||||
system: {
|
system: {
|
||||||
title: '系统',
|
title: '系统',
|
||||||
description: '基础设置、下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex)',
|
description: '基础设置、下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex、飞牛影视、绿联影视)',
|
||||||
},
|
},
|
||||||
directory: {
|
directory: {
|
||||||
title: '存储 & 目录',
|
title: '存储 & 目录',
|
||||||
@@ -1336,6 +1337,7 @@ export default {
|
|||||||
emby: 'Emby',
|
emby: 'Emby',
|
||||||
jellyfin: 'Jellyfin',
|
jellyfin: 'Jellyfin',
|
||||||
plex: 'Plex',
|
plex: 'Plex',
|
||||||
|
ugreen: '绿联影视',
|
||||||
reloadSuccess: '系统配置已生效',
|
reloadSuccess: '系统配置已生效',
|
||||||
reloadFailed: '重载系统失败!',
|
reloadFailed: '重载系统失败!',
|
||||||
auxAuthEnable: '用户辅助认证',
|
auxAuthEnable: '用户辅助认证',
|
||||||
@@ -2817,6 +2819,13 @@ export default {
|
|||||||
password: '密码',
|
password: '密码',
|
||||||
syncLibraries: '同步媒体库',
|
syncLibraries: '同步媒体库',
|
||||||
syncLibrariesHint: '只有选中的媒体库才会被同步',
|
syncLibrariesHint: '只有选中的媒体库才会被同步',
|
||||||
|
scanMode: '扫描模式',
|
||||||
|
scanModeHint: '用于全库刷新和按库刷新:新添加和修改 / 补充缺失 / 覆盖扫描',
|
||||||
|
scanModeOptions: {
|
||||||
|
newAndModified: '新添加和修改',
|
||||||
|
supplementMissing: '补充缺失',
|
||||||
|
fullOverride: '覆盖扫描',
|
||||||
|
},
|
||||||
nameExists: '【{name}】已存在,请替换为其他名称',
|
nameExists: '【{name}】已存在,请替换为其他名称',
|
||||||
hostRequired: '地址不能为空',
|
hostRequired: '地址不能为空',
|
||||||
apiKeyRequired: 'API密钥不能为空',
|
apiKeyRequired: 'API密钥不能为空',
|
||||||
@@ -3123,7 +3132,7 @@ export default {
|
|||||||
title: '媒体服务器',
|
title: '媒体服务器',
|
||||||
description: '配置媒体服务器',
|
description: '配置媒体服务器',
|
||||||
info: '媒体服务器配置说明',
|
info: '媒体服务器配置说明',
|
||||||
infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin或Plex等',
|
infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin、Plex、飞牛影视或绿联影视',
|
||||||
type: '媒体服务器类型',
|
type: '媒体服务器类型',
|
||||||
typeHint: '选择要使用的媒体服务器类型',
|
typeHint: '选择要使用的媒体服务器类型',
|
||||||
name: '服务器名称',
|
name: '服务器名称',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default {
|
|||||||
unsubscribe: '取消訂閱',
|
unsubscribe: '取消訂閱',
|
||||||
media: '媒體',
|
media: '媒體',
|
||||||
unknown: '未知',
|
unknown: '未知',
|
||||||
|
notFetched: '未獲取',
|
||||||
notice: '注意',
|
notice: '注意',
|
||||||
itemsPerPage: '每頁條數',
|
itemsPerPage: '每頁條數',
|
||||||
pageText: '{0}-{1} 共 {2} 條',
|
pageText: '{0}-{1} 共 {2} 條',
|
||||||
@@ -313,7 +314,7 @@ export default {
|
|||||||
settingTabs: {
|
settingTabs: {
|
||||||
system: {
|
system: {
|
||||||
title: '系統',
|
title: '系統',
|
||||||
description: '基礎設置、下載器(Qbittorrent、Transmission)、媒體服務器(Emby、Jellyfin、Plex)',
|
description: '基礎設置、下載器(Qbittorrent、Transmission)、媒體服務器(Emby、Jellyfin、Plex、飛牛影視、綠聯影視)',
|
||||||
},
|
},
|
||||||
directory: {
|
directory: {
|
||||||
title: '存儲 & 目錄',
|
title: '存儲 & 目錄',
|
||||||
@@ -1337,6 +1338,7 @@ export default {
|
|||||||
emby: 'Emby',
|
emby: 'Emby',
|
||||||
jellyfin: 'Jellyfin',
|
jellyfin: 'Jellyfin',
|
||||||
plex: 'Plex',
|
plex: 'Plex',
|
||||||
|
ugreen: '綠聯影視',
|
||||||
reloadSuccess: '系統配置已生效',
|
reloadSuccess: '系統配置已生效',
|
||||||
reloadFailed: '重載系統失敗!',
|
reloadFailed: '重載系統失敗!',
|
||||||
auxAuthEnable: '用戶輔助認證',
|
auxAuthEnable: '用戶輔助認證',
|
||||||
@@ -2823,6 +2825,13 @@ export default {
|
|||||||
password: '密碼',
|
password: '密碼',
|
||||||
syncLibraries: '同步媒體庫',
|
syncLibraries: '同步媒體庫',
|
||||||
syncLibrariesHint: '只有選中的媒體庫才會被同步',
|
syncLibrariesHint: '只有選中的媒體庫才會被同步',
|
||||||
|
scanMode: '掃描模式',
|
||||||
|
scanModeHint: '用於全庫刷新和按庫刷新:新添加和修改 / 補充缺失 / 覆蓋掃描',
|
||||||
|
scanModeOptions: {
|
||||||
|
newAndModified: '新添加和修改',
|
||||||
|
supplementMissing: '補充缺失',
|
||||||
|
fullOverride: '覆蓋掃描',
|
||||||
|
},
|
||||||
nameExists: '【{name}】已存在,請替換為其他名稱',
|
nameExists: '【{name}】已存在,請替換為其他名稱',
|
||||||
},
|
},
|
||||||
bangumi: {
|
bangumi: {
|
||||||
@@ -3124,7 +3133,7 @@ export default {
|
|||||||
title: '媒體伺服器',
|
title: '媒體伺服器',
|
||||||
description: '設定媒體伺服器',
|
description: '設定媒體伺服器',
|
||||||
info: '媒體伺服器設定說明',
|
info: '媒體伺服器設定說明',
|
||||||
infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin或Plex等',
|
infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin、Plex、飛牛影視或綠聯影視',
|
||||||
type: '媒體伺服器類型',
|
type: '媒體伺服器類型',
|
||||||
typeHint: '選擇要使用的媒體伺服器類型',
|
typeHint: '選擇要使用的媒體伺服器類型',
|
||||||
name: '伺服器名稱',
|
name: '伺服器名稱',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import embyLogo from '@/assets/images/logos/emby.png'
|
|||||||
import jellyfinLogo from '@/assets/images/logos/jellyfin.png'
|
import jellyfinLogo from '@/assets/images/logos/jellyfin.png'
|
||||||
import plexLogo from '@/assets/images/logos/plex.png'
|
import plexLogo from '@/assets/images/logos/plex.png'
|
||||||
import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
|
import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
|
||||||
|
import ugreenLogo from '@/assets/images/logos/ugreen.jpg'
|
||||||
import wechatLogo from '@/assets/images/logos/wechat.png'
|
import wechatLogo from '@/assets/images/logos/wechat.png'
|
||||||
import telegramLogo from '@/assets/images/logos/telegram.webp'
|
import telegramLogo from '@/assets/images/logos/telegram.webp'
|
||||||
import slackLogo from '@/assets/images/logos/slack.webp'
|
import slackLogo from '@/assets/images/logos/slack.webp'
|
||||||
@@ -40,6 +41,7 @@ const logoMap: Record<string, string> = {
|
|||||||
jellyfin: jellyfinLogo,
|
jellyfin: jellyfinLogo,
|
||||||
plex: plexLogo,
|
plex: plexLogo,
|
||||||
trimemedia: trimemediaLogo,
|
trimemedia: trimemediaLogo,
|
||||||
|
ugreen: ugreenLogo,
|
||||||
wechat: wechatLogo,
|
wechat: wechatLogo,
|
||||||
telegram: telegramLogo,
|
telegram: telegramLogo,
|
||||||
slack: slackLogo,
|
slack: slackLogo,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function loadMediaStatistic() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('dashboard.episodes'),
|
title: t('dashboard.episodes'),
|
||||||
stats: res.episode_count.toLocaleString(),
|
stats: res.episode_count == null ? t('common.notFetched') : res.episode_count.toLocaleString(),
|
||||||
icon: 'mdi-television-classic',
|
icon: 'mdi-television-classic',
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const ugreenScanModeOptions = computed(() => [
|
||||||
|
{ title: t('mediaserver.scanModeOptions.newAndModified'), value: 'new_and_modified' },
|
||||||
|
{ title: t('mediaserver.scanModeOptions.supplementMissing'), value: 'supplement_missing' },
|
||||||
|
{ title: t('mediaserver.scanModeOptions.fullOverride'), value: 'full_override' },
|
||||||
|
])
|
||||||
|
|
||||||
|
function ensureUgreenScanMode() {
|
||||||
|
if (wizardData.value.mediaServer.type !== 'ugreen') return
|
||||||
|
wizardData.value.mediaServer.config = wizardData.value.mediaServer.config || {}
|
||||||
|
if (!wizardData.value.mediaServer.config.scan_mode) {
|
||||||
|
wizardData.value.mediaServer.config.scan_mode = 'supplement_missing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 调用API查询媒体库
|
// 调用API查询媒体库
|
||||||
async function loadLibrary(server: string) {
|
async function loadLibrary(server: string) {
|
||||||
try {
|
try {
|
||||||
@@ -42,6 +56,7 @@ async function loadLibrary(server: string) {
|
|||||||
// 选择媒体服务器并自动加载媒体库
|
// 选择媒体服务器并自动加载媒体库
|
||||||
async function selectMediaServerWithLibrary(type: string) {
|
async function selectMediaServerWithLibrary(type: string) {
|
||||||
selectMediaServer(type)
|
selectMediaServer(type)
|
||||||
|
ensureUgreenScanMode()
|
||||||
// 如果选择了媒体服务器类型,自动加载媒体库
|
// 如果选择了媒体服务器类型,自动加载媒体库
|
||||||
if (type && wizardData.value.mediaServer.name) {
|
if (type && wizardData.value.mediaServer.name) {
|
||||||
await loadLibrary(wizardData.value.mediaServer.name)
|
await loadLibrary(wizardData.value.mediaServer.name)
|
||||||
@@ -50,6 +65,7 @@ async function selectMediaServerWithLibrary(type: string) {
|
|||||||
|
|
||||||
// 组件挂载时检查是否需要加载媒体库
|
// 组件挂载时检查是否需要加载媒体库
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
ensureUgreenScanMode()
|
||||||
// 如果已经有媒体服务器配置,自动加载媒体库
|
// 如果已经有媒体服务器配置,自动加载媒体库
|
||||||
if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) {
|
if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) {
|
||||||
await loadLibrary(wizardData.value.mediaServer.name)
|
await loadLibrary(wizardData.value.mediaServer.name)
|
||||||
@@ -60,6 +76,7 @@ onMounted(async () => {
|
|||||||
watch(
|
watch(
|
||||||
() => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name],
|
() => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name],
|
||||||
async ([type, name]) => {
|
async ([type, name]) => {
|
||||||
|
ensureUgreenScanMode()
|
||||||
console.log('Media server changed:', { type, name })
|
console.log('Media server changed:', { type, name })
|
||||||
if (type && name) {
|
if (type && name) {
|
||||||
await loadLibrary(name)
|
await loadLibrary(name)
|
||||||
@@ -141,6 +158,19 @@ watch(
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="3">
|
||||||
|
<VCard
|
||||||
|
:color="wizardData.mediaServer.type === 'ugreen' ? 'primary' : 'default'"
|
||||||
|
:variant="wizardData.mediaServer.type === 'ugreen' ? 'tonal' : 'outlined'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="selectMediaServerWithLibrary('ugreen')"
|
||||||
|
>
|
||||||
|
<VCardText class="text-center">
|
||||||
|
<VImg :src="getLogoUrl('ugreen')" height="48" width="48" class="mx-auto mb-2" />
|
||||||
|
<div class="text-h6">绿联影视</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</div>
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
@@ -380,6 +410,97 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VRow v-else-if="wizardData.mediaServer.type === 'ugreen'">
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="wizardData.mediaServer.name"
|
||||||
|
:label="t('common.name')"
|
||||||
|
:placeholder="t('mediaserver.nameRequired')"
|
||||||
|
:hint="t('mediaserver.serverAlias')"
|
||||||
|
:error="validationErrors.mediaServer.name"
|
||||||
|
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-label"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="wizardData.mediaServer.config.host"
|
||||||
|
:label="t('mediaserver.host')"
|
||||||
|
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||||
|
:hint="t('mediaserver.hostHint')"
|
||||||
|
:error="validationErrors.mediaServer.host"
|
||||||
|
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-server"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="wizardData.mediaServer.config.play_host"
|
||||||
|
:label="t('mediaserver.playHost')"
|
||||||
|
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||||
|
:hint="t('mediaserver.playHostHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-play-network"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="wizardData.mediaServer.config.username"
|
||||||
|
:label="t('mediaserver.username')"
|
||||||
|
:error="validationErrors.mediaServer.username"
|
||||||
|
:error-messages="validationErrors.mediaServer.username ? [t('mediaserver.usernameRequired')] : []"
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
type="password"
|
||||||
|
v-model="wizardData.mediaServer.config.password"
|
||||||
|
:label="t('mediaserver.password')"
|
||||||
|
:error="validationErrors.mediaServer.password"
|
||||||
|
:error-messages="validationErrors.mediaServer.password ? [t('mediaserver.passwordRequired')] : []"
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VAutocomplete
|
||||||
|
v-model="wizardData.mediaServer.sync_libraries"
|
||||||
|
:label="t('mediaserver.syncLibraries')"
|
||||||
|
:items="librariesOptions"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
:hint="t('mediaserver.syncLibrariesHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
append-inner-icon="mdi-refresh"
|
||||||
|
prepend-inner-icon="mdi-library"
|
||||||
|
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="wizardData.mediaServer.config.scan_mode"
|
||||||
|
:label="t('mediaserver.scanMode')"
|
||||||
|
:items="ugreenScanModeOptions"
|
||||||
|
:hint="t('mediaserver.scanModeHint')"
|
||||||
|
persistent-hint
|
||||||
|
active
|
||||||
|
prepend-inner-icon="mdi-radar"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
<VRow v-else-if="wizardData.mediaServer.type === 'plex'">
|
<VRow v-else-if="wizardData.mediaServer.type === 'plex'">
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
|
|||||||
Reference in New Issue
Block a user