feat: add subtitle search actions

This commit is contained in:
jxxghp
2026-06-09 17:04:17 +08:00
parent b1cb07ae8c
commit 2f46c19826
5 changed files with 92 additions and 7 deletions

View File

@@ -953,6 +953,7 @@ export default {
episodeCount: '{count} Episodes',
actions: {
searchResource: 'Search Resource',
searchSubtitle: 'Search Subtitle',
subscribe: 'Subscribe',
playOnline: 'Play Online',
playInApp: 'Play in App',

View File

@@ -949,6 +949,7 @@ export default {
episodeCount: '{count}集',
actions: {
searchResource: '搜索资源',
searchSubtitle: '搜索字幕',
subscribe: '订阅',
playOnline: '在线播放',
playInApp: 'APP播放',

View File

@@ -949,6 +949,7 @@ export default {
episodeCount: '{count}集',
actions: {
searchResource: '搜索資源',
searchSubtitle: '搜索字幕',
subscribe: '訂閱',
playOnline: '線上播放',
playInApp: 'APP播放',

View File

@@ -43,6 +43,7 @@ interface SearchParams {
title: string
year: string
season: string
episode: string
sites: string
result_type: string
}
@@ -65,6 +66,7 @@ function createSearchParams(query: LocationQuery): SearchParams {
title: query?.title?.toString() ?? '',
year: query?.year?.toString() ?? '',
season: query?.season?.toString() ?? '',
episode: query?.episode?.toString() ?? '',
sites: query?.sites?.toString() ?? '',
result_type: query?.result_type?.toString() === 'subtitle' ? 'subtitle' : 'torrent',
}
@@ -78,6 +80,7 @@ function normalizeSearchParams(params?: Partial<SearchParams> | null): SearchPar
title: params?.title?.toString() ?? '',
year: params?.year?.toString() ?? '',
season: params?.season?.toString() ?? '',
episode: params?.episode?.toString() ?? '',
sites: params?.sites?.toString() ?? '',
result_type: params?.result_type?.toString() === 'subtitle' ? 'subtitle' : 'torrent',
}
@@ -528,13 +531,22 @@ function buildSearchStreamUrl(params: SearchParams, requestToken?: string) {
const isMediaSearch = /^[a-zA-Z]+:/.test(params.keyword)
const url = getApiUrl(
params.result_type === 'subtitle'
? 'search/subtitle/title/stream'
? isMediaSearch
? `search/subtitle/media/${encodeURIComponent(params.keyword)}/stream`
: 'search/subtitle/title/stream'
: isMediaSearch
? `search/media/${encodeURIComponent(params.keyword)}/stream`
: 'search/title/stream',
)
if (params.result_type === 'subtitle') {
if (params.result_type === 'subtitle' && isMediaSearch) {
setSearchParam(url.searchParams, 'mtype', params.type)
setSearchParam(url.searchParams, 'title', params.title)
setSearchParam(url.searchParams, 'year', params.year)
setSearchParam(url.searchParams, 'season', params.season)
setSearchParam(url.searchParams, 'episode', params.episode)
setSearchParam(url.searchParams, 'sites', params.sites)
} else if (params.result_type === 'subtitle') {
setSearchParam(url.searchParams, 'keyword', params.keyword)
setSearchParam(url.searchParams, 'sites', params.sites)
} else if (isMediaSearch) {
@@ -732,8 +744,21 @@ async function searchByRequest(params: SearchParams, requestToken?: string) {
// 静默刷新使用普通请求,保留当前结果直到新数据完整返回,避免返回页面时露出搜索进度态。
async function requestSearchResults(params: SearchParams, requestToken?: string) {
let result: { [key: string]: any }
const isMediaSearch = /^[a-zA-Z]+:/.test(params.keyword)
// 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符则按照媒体ID格式搜索
if (params.result_type === 'subtitle') {
if (params.result_type === 'subtitle' && isMediaSearch) {
result = await api.get(`search/subtitle/media/${params.keyword}`, {
params: {
mtype: params.type,
title: params.title,
year: params.year,
season: params.season,
episode: params.episode,
sites: params.sites,
_ts: requestToken,
},
})
} else if (params.result_type === 'subtitle') {
result = await api.get('search/subtitle/title', {
params: {
keyword: params.keyword,
@@ -741,7 +766,7 @@ async function requestSearchResults(params: SearchParams, requestToken?: string)
_ts: requestToken,
},
})
} else if (/^[a-zA-Z]+:/.test(params.keyword)) {
} else if (isMediaSearch) {
result = await api.get(`search/media/${params.keyword}`, {
params: {
mtype: params.type,

View File

@@ -40,6 +40,10 @@ const globalSettings = globalSettingsStore.globalSettings
// 用户 Store
const userStore = useUserStore()
const canSearch = computed(() =>
hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search'),
)
// 提示框
const $toast = useToast()
@@ -491,9 +495,16 @@ function joinArray(arr: string[]) {
return arr.join('、')
}
interface MediaSearchOptions {
season?: number | null
episode?: number | null
}
// 开始搜索
function handleSearch() {
function handleSearch(resultType: 'torrent' | 'subtitle' = 'torrent', options: MediaSearchOptions = {}) {
const keyword = getMediaId()
const season = options.season ?? mediaDetail.value.season
const episode = options.episode ?? null
router.push({
path: '/resource',
query: {
@@ -502,8 +513,10 @@ function handleSearch() {
area: searchType.value,
title: mediaDetail.value.title,
year: mediaDetail.value.year,
season: mediaDetail.value.season,
season,
episode,
sites: selectedSites.value.join(','),
result_type: resultType,
},
})
}
@@ -572,6 +585,16 @@ function searchSites(sites: number[]) {
handleSearch()
}
// 搜索字幕
function handleSubtitleSearch() {
handleSearch('subtitle')
}
// 搜索单集字幕
function handleEpisodeSubtitleSearch(season: number | null, episode: number | null) {
handleSearch('subtitle', { season, episode })
}
onBeforeMount(() => {
getMediaDetail()
})
@@ -637,7 +660,7 @@ onBeforeMount(() => {
<VBtn
v-if="
(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) &&
hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')
canSearch
"
variant="tonal"
color="info"
@@ -658,6 +681,21 @@ onBeforeMount(() => {
</VList>
</VMenu>
</VBtn>
<VBtn
v-if="
(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) &&
canSearch
"
variant="tonal"
color="info"
class="ms-2 mb-2"
@click="handleSubtitleSearch"
>
<template #prepend>
<VIcon icon="mdi-subtitles-outline" />
</template>
{{ t('media.actions.searchSubtitle') }}
</VBtn>
<VBtn
v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id"
class="ms-2 mb-2"
@@ -816,6 +854,25 @@ onBeforeMount(() => {
class="ms-2"
size="small"
/>
<VTooltip v-if="canSearch" location="top">
<template #activator="{ props }">
<IconBtn
class="ms-1"
color="info"
variant="text"
v-bind="props"
@click.stop="
handleEpisodeSubtitleSearch(
season.season_number ?? null,
episode.episode_number ?? null,
)
"
>
<VIcon icon="mdi-subtitles-outline" size="small" />
</IconBtn>
</template>
<span>{{ t('media.actions.searchSubtitle') }}</span>
</VTooltip>
</div>
<p>{{ episode.overview }}</p>
</div>