feat: add support for ZSpace media server integration including UI configuration and logo assets

This commit is contained in:
jxxghp
2026-05-11 18:09:29 +08:00
parent c97247b92b
commit 4207a70716
12 changed files with 217 additions and 10 deletions

View File

@@ -68,6 +68,10 @@ export const mediaServerOptions = [
value: 'emby',
title: i18n.global.t('setting.system.emby'),
},
{
value: 'zspace',
title: i18n.global.t('setting.system.zspace'),
},
{
value: 'jellyfin',
title: i18n.global.t('setting.system.jellyfin'),

View File

@@ -1145,7 +1145,7 @@ export interface StorageConf {
export interface MediaServerConf {
// 名称
name: string
// 类型 emby/jellyfin/plex/trimemedia/ugreen
// 类型 emby/zspace/jellyfin/plex/trimemedia/ugreen
type: string
// 配置
config: { [key: string]: any }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -40,6 +40,7 @@ function imageErrorHandler() {
function getDefaultImage() {
if (props.media?.server_type === 'plex') return plex
else if (props.media?.server_type === 'emby') return emby
else if (props.media?.server_type === 'zspace') return getLogoUrl('zspace')
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 === 'ugreen') return getLogoUrl('ugreen')

View File

@@ -121,6 +121,8 @@ const getIcon = computed(() => {
switch (props.mediaserver.type) {
case 'emby':
return getLogoUrl('emby')
case 'zspace':
return getLogoUrl('zspace')
case 'jellyfin':
return getLogoUrl('jellyfin')
case 'trimemedia':
@@ -318,6 +320,77 @@ onMounted(() => {
/>
</VCol>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'zspace'">
<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')"
:hint="t('mediaserver.usernameHint')"
persistent-hint
active
prepend-inner-icon="mdi-account"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
type="password"
v-model="mediaServerInfo.config.password"
:label="t('mediaserver.password')"
persistent-hint
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>
</VRow>
<VRow v-else-if="mediaServerInfo.type == 'jellyfin'">
<VCol cols="12" md="6">
<VTextField

View File

@@ -320,6 +320,7 @@ export function useSetupWizard() {
// 媒体服务器映射
mediaServer: {
'emby': 'EmbyModule',
'zspace': 'ZSpaceModule',
'jellyfin': 'JellyfinModule',
'plex': 'PlexModule',
'trimemedia': 'TrimeMediaModule',
@@ -617,6 +618,15 @@ export function useSetupWizard() {
errors.push(t('mediaserver.apiKeyRequired'))
validationErrors.value.mediaServer.apikey = true
}
} else if (wizardData.value.mediaServer.type === 'zspace') {
if (!wizardData.value.mediaServer.config?.username?.trim()) {
errors.push(t('mediaserver.usernameRequired'))
validationErrors.value.mediaServer.username = true
}
if (!wizardData.value.mediaServer.config?.password?.trim()) {
errors.push(t('mediaserver.passwordRequired'))
validationErrors.value.mediaServer.password = true
}
} else if (wizardData.value.mediaServer.type === 'plex') {
if (!wizardData.value.mediaServer.config?.token?.trim()) {
errors.push(t('mediaserver.tokenRequired'))

View File

@@ -321,7 +321,7 @@ export default {
system: {
title: 'System',
description:
'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex, TrimeMedia, Ugreen)',
'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, ZSpace, Jellyfin, Plex, TrimeMedia, Ugreen)',
},
directory: {
title: 'Storage & Directories',
@@ -920,6 +920,7 @@ export default {
plex: 'Plex',
jellyfin: 'Jellyfin',
emby: 'Emby',
zspace: 'ZSpace',
appLaunchFailed: 'App launch failed, redirecting to web version',
appNotInstalled: 'App not detected, redirecting to web version',
downloadApp: 'Download App',
@@ -1493,6 +1494,7 @@ export default {
transmission: 'Transmission',
rtorrent: 'rTorrent',
emby: 'Emby',
zspace: 'ZSpace',
jellyfin: 'Jellyfin',
plex: 'Plex',
ugreen: 'Ugreen',
@@ -3392,12 +3394,13 @@ export default {
description: 'Configure media server',
info: 'Media Server Configuration',
infoDesc:
'Configure media server for media library management, can choose Emby, Jellyfin, Plex, TrimeMedia or Ugreen.',
'Configure media server for media library management, can choose Emby, ZSpace, Jellyfin, Plex, TrimeMedia or Ugreen.',
type: 'Media Server Type',
typeHint: 'Select the type of media server to use',
name: 'Server Name',
nameHint: 'Set a name for the media server',
embyConfig: 'Emby Configuration',
zspaceConfig: 'ZSpace Configuration',
jellyfinConfig: 'Jellyfin Configuration',
plexConfig: 'Plex Configuration',
host: 'Server Address',

View File

@@ -319,7 +319,7 @@ export default {
settingTabs: {
system: {
title: '系统',
description: '基础设置、下载器Qbittorrent、Transmission、媒体服务器Emby、Jellyfin、Plex、飞牛影视、绿联影视',
description: '基础设置、下载器Qbittorrent、Transmission、媒体服务器Emby、极影视、Jellyfin、Plex、飞牛影视、绿联影视',
},
directory: {
title: '存储 & 目录',
@@ -915,6 +915,7 @@ export default {
plex: 'Plex',
jellyfin: 'Jellyfin',
emby: 'Emby',
zspace: '极影视',
appLaunchFailed: 'APP启动失败正在跳转到网页版',
appNotInstalled: '未检测到APP正在跳转到网页版',
downloadApp: '下载APP',
@@ -1478,6 +1479,7 @@ export default {
transmission: 'Transmission',
rtorrent: 'rTorrent',
emby: 'Emby',
zspace: '极影视',
jellyfin: 'Jellyfin',
plex: 'Plex',
ugreen: '绿联影视',
@@ -3337,12 +3339,13 @@ export default {
title: '媒体服务器',
description: '配置媒体服务器',
info: '媒体服务器配置说明',
infoDesc: '配置媒体服务器用于媒体库管理可选择Emby、Jellyfin、Plex、飞牛影视或绿联影视',
infoDesc: '配置媒体服务器用于媒体库管理可选择Emby、极影视、Jellyfin、Plex、飞牛影视或绿联影视',
type: '媒体服务器类型',
typeHint: '选择要使用的媒体服务器类型',
name: '服务器名称',
nameHint: '为媒体服务器设置一个名称',
embyConfig: 'Emby 配置',
zspaceConfig: '极影视 配置',
jellyfinConfig: 'Jellyfin 配置',
plexConfig: 'Plex 配置',
host: '服务器地址',

View File

@@ -320,7 +320,7 @@ export default {
system: {
title: '系統',
description:
'基礎設置、下載器Qbittorrent、Transmission、媒體服務器Emby、Jellyfin、Plex、飛牛影視、綠聯影視',
'基礎設置、下載器Qbittorrent、Transmission、媒體服務器Emby、極影視、Jellyfin、Plex、飛牛影視、綠聯影視',
},
directory: {
title: '存儲 & 目錄',
@@ -916,6 +916,7 @@ export default {
plex: 'Plex',
jellyfin: 'Jellyfin',
emby: 'Emby',
zspace: '極影視',
appLaunchFailed: 'APP啟動失敗正在跳轉到網頁版',
appNotInstalled: '未檢測到APP正在跳轉到網頁版',
downloadApp: '下載APP',
@@ -1480,6 +1481,7 @@ export default {
transmission: 'Transmission',
rtorrent: 'rTorrent',
emby: 'Emby',
zspace: '極影視',
jellyfin: 'Jellyfin',
plex: 'Plex',
ugreen: '綠聯影視',
@@ -3339,12 +3341,13 @@ export default {
title: '媒體伺服器',
description: '設定媒體伺服器',
info: '媒體伺服器設定說明',
infoDesc: '設定媒體伺服器用於媒體庫管理可選擇Emby、Jellyfin、Plex、飛牛影視或綠聯影視',
infoDesc: '設定媒體伺服器用於媒體庫管理可選擇Emby、極影視、Jellyfin、Plex、飛牛影視或綠聯影視',
type: '媒體伺服器類型',
typeHint: '選擇要使用的媒體伺服器類型',
name: '伺服器名稱',
nameHint: '為媒體伺服器設定一個名稱',
embyConfig: 'Emby 設定',
zspaceConfig: '極影視 設定',
jellyfinConfig: 'Jellyfin 設定',
plexConfig: 'Plex 設定',
host: '伺服器位址',

View File

@@ -1,6 +1,6 @@
/**
* 通用APP深度链接工具类
* 支持媒体服务器Plex、Jellyfin、Emby和豆瓣的APP跳转和网页跳转
* 支持媒体服务器Plex、Jellyfin、Emby、极影视、飞牛影视和豆瓣的APP跳转和网页跳转
*
* 深度链接格式参考:
* - Plex: https://forums.plex.tv/t/plex-mobile-app-deep-linking/123456
@@ -12,7 +12,7 @@
import { isMobileDevice, isIOSDevice, isAndroidDevice } from '@/@core/utils'
// APP类型
export type AppType = 'plex' | 'jellyfin' | 'emby' | 'trimemedia' | 'douban'
export type AppType = 'plex' | 'jellyfin' | 'emby' | 'zspace' | 'trimemedia' | 'douban'
// 深度链接配置
interface DeepLinkConfig {
@@ -38,6 +38,11 @@ const DEEP_LINK_CONFIGS: Record<AppType, DeepLinkConfig> = {
webUrl: 'https://emby.media',
timeout: 2000,
},
zspace: {
appScheme: 'emby://',
webUrl: 'https://www.zspace.com.cn',
timeout: 2000,
},
trimemedia: {
appScheme: 'trimemedia://',
webUrl: 'https://trimemedia.com',
@@ -135,6 +140,9 @@ function buildDeepLinkUrl(appType: AppType, params: string | DoubanAppParams): s
case 'emby':
return buildEmbyDeepLink(params as string)
case 'zspace':
return buildEmbyDeepLink(params as string)
case 'trimemedia':
return buildTrimemediaDeepLink(params as string)
@@ -634,7 +642,7 @@ export async function openMediaServerWithAutoDetect(
// 优先使用传入的 serverType 参数
if (serverType) {
const type = serverType.toLowerCase()
if (type === 'plex' || type === 'jellyfin' || type === 'emby' || type === 'trimemedia') {
if (type === 'plex' || type === 'jellyfin' || type === 'emby' || type === 'zspace' || type === 'trimemedia') {
detectedServerType = type as AppType
}
}
@@ -649,6 +657,8 @@ export async function openMediaServerWithAutoDetect(
detectedServerType = 'jellyfin'
} else if (url.includes('emby')) {
detectedServerType = 'emby'
} else if (url.includes('zspace')) {
detectedServerType = 'zspace'
}
}
@@ -698,6 +708,8 @@ export function getAppDownloadUrl(appType: AppType): string {
return 'https://jellyfin.org/downloads/'
case 'emby':
return 'https://emby.media/download.html'
case 'zspace':
return 'https://www.zspace.com.cn/'
case 'trimemedia':
return 'https://trimemedia.com/download'
case 'douban':

View File

@@ -8,6 +8,7 @@ import qbittorrentLogo from '@/assets/images/logos/qbittorrent.png'
import transmissionLogo from '@/assets/images/logos/transmission.png'
import rtorrentLogo from '@/assets/images/logos/rtorrent.png'
import embyLogo from '@/assets/images/logos/emby.png'
import zspaceLogo from '@/assets/images/logos/zspace.webp'
import jellyfinLogo from '@/assets/images/logos/jellyfin.png'
import plexLogo from '@/assets/images/logos/plex.png'
import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
@@ -40,6 +41,7 @@ const logoMap: Record<string, string> = {
transmission: transmissionLogo,
rtorrent: rtorrentLogo,
emby: embyLogo,
zspace: zspaceLogo,
jellyfin: jellyfinLogo,
plex: plexLogo,
trimemedia: trimemediaLogo,

View File

@@ -122,6 +122,19 @@ watch(
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.mediaServer.type === 'zspace' ? 'primary' : 'default'"
:variant="wizardData.mediaServer.type === 'zspace' ? 'tonal' : 'outlined'"
class="cursor-pointer"
@click="selectMediaServerWithLibrary('zspace')"
>
<VCardText class="text-center">
<VImg :src="getLogoUrl('zspace')" height="48" width="48" class="mx-auto mb-2" />
<div class="text-h6">极影视</div>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="3">
<VCard
:color="wizardData.mediaServer.type === 'jellyfin' ? 'primary' : 'default'"
@@ -263,6 +276,89 @@ watch(
/>
</VCol>
</VRow>
<VRow v-else-if="wizardData.mediaServer.type === 'zspace'">
<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')"
:hint="t('mediaserver.usernameHint')"
:error="validationErrors.mediaServer.username"
:error-messages="validationErrors.mediaServer.username ? [t('mediaserver.usernameRequired')] : []"
persistent-hint
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')] : []"
persistent-hint
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>
</VRow>
<VRow v-else-if="wizardData.mediaServer.type === 'jellyfin'">
<VCol cols="12" md="6">
<VTextField