mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 17:26:41 +08:00
fix: build Emby app deep links with server ids
This commit is contained in:
+66
-20
@@ -75,6 +75,12 @@ interface MediaServerLinkTarget {
|
|||||||
server_type?: string
|
server_type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emby hash 路由中的目标参数
|
||||||
|
interface EmbyHashTarget {
|
||||||
|
mediaId: string | null
|
||||||
|
serverId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断链接参数是否为有效值。
|
* 判断链接参数是否为有效值。
|
||||||
* @param value 待检查的链接参数
|
* @param value 待检查的链接参数
|
||||||
@@ -102,6 +108,43 @@ function getTargetServerId(target?: MediaServerLinkTarget): string | null {
|
|||||||
return getValidLinkValue(target?.server_id ?? target?.serverId)
|
return getValidLinkValue(target?.server_id ?? target?.serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析媒体服务器网页链接中的 hash 查询参数。
|
||||||
|
* @param playUrl 原始播放链接
|
||||||
|
*/
|
||||||
|
function getHashRouteParams(playUrl: string): { hashPath: string; params: URLSearchParams } | null {
|
||||||
|
const url = new URL(playUrl)
|
||||||
|
const hash = url.hash || ''
|
||||||
|
const queryIndex = hash.indexOf('?')
|
||||||
|
if (queryIndex === -1) return null
|
||||||
|
return {
|
||||||
|
hashPath: hash.slice(0, queryIndex),
|
||||||
|
params: new URLSearchParams(hash.slice(queryIndex + 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Emby 网页链接中提取 App 跳转需要的媒体ID和服务器ID。
|
||||||
|
* @param playUrl 原始播放链接
|
||||||
|
*/
|
||||||
|
function getEmbyHashTarget(playUrl: string): EmbyHashTarget {
|
||||||
|
const hashRoute = getHashRouteParams(playUrl)
|
||||||
|
if (!hashRoute) return { mediaId: null, serverId: null }
|
||||||
|
|
||||||
|
const serverId = getValidLinkValue(hashRoute.params.get('serverId'))
|
||||||
|
if (hashRoute.hashPath.includes('/videos')) {
|
||||||
|
return {
|
||||||
|
mediaId: getValidLinkValue(hashRoute.params.get('parentId')),
|
||||||
|
serverId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mediaId: getValidLinkValue(hashRoute.params.get('id')),
|
||||||
|
serverId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用后端返回的真实ID修正Emby网页链接。
|
* 使用后端返回的真实ID修正Emby网页链接。
|
||||||
* @param playUrl 原始播放链接
|
* @param playUrl 原始播放链接
|
||||||
@@ -110,12 +153,10 @@ function getTargetServerId(target?: MediaServerLinkTarget): string | null {
|
|||||||
function normalizeEmbyWebUrl(playUrl: string, target?: MediaServerLinkTarget): string {
|
function normalizeEmbyWebUrl(playUrl: string, target?: MediaServerLinkTarget): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL(playUrl)
|
const url = new URL(playUrl)
|
||||||
const hash = url.hash || ''
|
const hashRoute = getHashRouteParams(playUrl)
|
||||||
const queryIndex = hash.indexOf('?')
|
if (!hashRoute) return playUrl
|
||||||
if (queryIndex === -1) return playUrl
|
|
||||||
|
|
||||||
const hashPath = hash.slice(0, queryIndex)
|
const { hashPath, params } = hashRoute
|
||||||
const params = new URLSearchParams(hash.slice(queryIndex + 1))
|
|
||||||
const itemId = getTargetItemId(target)
|
const itemId = getTargetItemId(target)
|
||||||
const serverId = getTargetServerId(target)
|
const serverId = getTargetServerId(target)
|
||||||
|
|
||||||
@@ -123,6 +164,10 @@ function normalizeEmbyWebUrl(playUrl: string, target?: MediaServerLinkTarget): s
|
|||||||
params.set('id', itemId)
|
params.set('id', itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (itemId && (hashPath.includes('/videos') || params.has('parentId'))) {
|
||||||
|
params.set('parentId', itemId)
|
||||||
|
}
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
params.set('serverId', serverId)
|
params.set('serverId', serverId)
|
||||||
} else if (params.has('serverId') && !getValidLinkValue(params.get('serverId'))) {
|
} else if (params.has('serverId') && !getValidLinkValue(params.get('serverId'))) {
|
||||||
@@ -506,12 +551,13 @@ function buildEmbyDeepLink(playUrl: string): string {
|
|||||||
const serverAddress = url.hostname + (url.port ? `:${url.port}` : '')
|
const serverAddress = url.hostname + (url.port ? `:${url.port}` : '')
|
||||||
|
|
||||||
// 尝试多种格式提取媒体ID
|
// 尝试多种格式提取媒体ID
|
||||||
let mediaId: string | null = null
|
const hashTarget = getEmbyHashTarget(playUrl)
|
||||||
let serverId: string | null = null
|
let mediaId: string | null = hashTarget.mediaId
|
||||||
|
let serverId: string | null = hashTarget.serverId
|
||||||
|
|
||||||
// 格式1: /web/index.html#!/item?id=xxx&context=home&serverId=xxx (后台返回的格式)
|
// 格式1: /web/index.html#!/item?id=xxx&context=home&serverId=xxx (后台返回的格式)
|
||||||
const itemHashMatch = playUrl.match(/\/item\?id=([^&]+)/)
|
const itemHashMatch = !mediaId ? playUrl.match(/\/item\?id=([^&]+)/) : null
|
||||||
if (itemHashMatch) {
|
if (!mediaId && itemHashMatch) {
|
||||||
mediaId = itemHashMatch[1]
|
mediaId = itemHashMatch[1]
|
||||||
// 提取serverId
|
// 提取serverId
|
||||||
const serverIdMatch = playUrl.match(/serverId=([^&]+)/)
|
const serverIdMatch = playUrl.match(/serverId=([^&]+)/)
|
||||||
@@ -521,8 +567,8 @@ function buildEmbyDeepLink(playUrl: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 格式2: /web/index.html#!/videos?serverId=xxx&parentId=xxx (后台返回的格式)
|
// 格式2: /web/index.html#!/videos?serverId=xxx&parentId=xxx (后台返回的格式)
|
||||||
const videosHashMatch = playUrl.match(/\/videos\?serverId=([^&]+)&parentId=([^&]+)/)
|
const videosHashMatch = !mediaId ? playUrl.match(/\/videos\?serverId=([^&]+)&parentId=([^&]+)/) : null
|
||||||
if (videosHashMatch) {
|
if (!mediaId && videosHashMatch) {
|
||||||
// 对于videos格式,我们使用parentId作为媒体ID
|
// 对于videos格式,我们使用parentId作为媒体ID
|
||||||
mediaId = videosHashMatch[2]
|
mediaId = videosHashMatch[2]
|
||||||
serverId = getValidLinkValue(videosHashMatch[1])
|
serverId = getValidLinkValue(videosHashMatch[1])
|
||||||
@@ -562,22 +608,22 @@ function buildEmbyDeepLink(playUrl: string): string {
|
|||||||
|
|
||||||
if (mediaId) {
|
if (mediaId) {
|
||||||
let deepLink: string
|
let deepLink: string
|
||||||
|
const encodedMediaId = encodeURIComponent(mediaId)
|
||||||
|
const encodedServerId = serverId ? encodeURIComponent(serverId) : null
|
||||||
|
|
||||||
// 根据设备类型使用不同的深度链接格式
|
// 根据设备类型使用不同的深度链接格式
|
||||||
if (isIOSDevice()) {
|
if (isIOSDevice()) {
|
||||||
// iOS格式: emby://items?serverId={SERVER_ID}&itemId={ITEM_ID}
|
// iOS格式: emby://items?serverId={SERVER_ID}&itemId={ITEM_ID}
|
||||||
if (serverId) {
|
if (encodedServerId) {
|
||||||
deepLink = `emby://items?serverId=${serverId}&itemId=${mediaId}`
|
deepLink = `emby://items?serverId=${encodedServerId}&itemId=${encodedMediaId}`
|
||||||
} else {
|
} else {
|
||||||
// 如果没有serverId,尝试使用服务器地址作为serverId
|
deepLink = `emby://items?itemId=${encodedMediaId}`
|
||||||
deepLink = `emby://items?serverId=${serverAddress}&itemId=${mediaId}`
|
|
||||||
}
|
}
|
||||||
|
} else if (encodedServerId) {
|
||||||
|
// Android格式: emby://items/{SERVER_ID}/{ITEM_ID}
|
||||||
|
deepLink = `emby://items/${encodedServerId}/${encodedMediaId}`
|
||||||
} else {
|
} else {
|
||||||
// Android格式: emby://{服务器地址}/item/{媒体ID}
|
deepLink = `emby://${serverAddress}/item/${encodedMediaId}`
|
||||||
deepLink = `emby://${serverAddress}/item/${mediaId}`
|
|
||||||
if (serverId) {
|
|
||||||
deepLink += `?serverId=${serverId}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Emby深度链接构建成功:', {
|
console.log('Emby深度链接构建成功:', {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useTheme } from 'vuetify'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { hasPermission } from '@/utils/permission'
|
import { hasPermission } from '@/utils/permission'
|
||||||
import { useGlobalSettingsStore } from '@/stores'
|
import { useGlobalSettingsStore } from '@/stores'
|
||||||
import { openMediaServerWithAutoDetect, openDoubanApp } from '@/utils/appDeepLink'
|
import { openMediaServerItem, openDoubanApp } from '@/utils/appDeepLink'
|
||||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||||
|
|
||||||
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
|
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
|
||||||
@@ -526,7 +526,12 @@ async function handlePlay() {
|
|||||||
const result: { [key: string]: any } = await api.get(`mediaserver/play/${existsItemId.value}`)
|
const result: { [key: string]: any } = await api.get(`mediaserver/play/${existsItemId.value}`)
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
// 使用深度链接工具,优先跳转到APP,失败后跳转到网页
|
// 使用深度链接工具,优先跳转到APP,失败后跳转到网页
|
||||||
await openMediaServerWithAutoDetect(result.data.url, undefined, result.data.server_type)
|
await openMediaServerItem({
|
||||||
|
link: result.data.url,
|
||||||
|
item_id: result.data.item_id,
|
||||||
|
server_id: result.data.server_id,
|
||||||
|
server_type: result.data.server_type,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
$toast.error(`获取播放链接失败:${result.message}!`)
|
$toast.error(`获取播放链接失败:${result.message}!`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user