mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
feat: add subtitle search actions
This commit is contained in:
@@ -953,6 +953,7 @@ export default {
|
|||||||
episodeCount: '{count} Episodes',
|
episodeCount: '{count} Episodes',
|
||||||
actions: {
|
actions: {
|
||||||
searchResource: 'Search Resource',
|
searchResource: 'Search Resource',
|
||||||
|
searchSubtitle: 'Search Subtitle',
|
||||||
subscribe: 'Subscribe',
|
subscribe: 'Subscribe',
|
||||||
playOnline: 'Play Online',
|
playOnline: 'Play Online',
|
||||||
playInApp: 'Play in App',
|
playInApp: 'Play in App',
|
||||||
|
|||||||
@@ -949,6 +949,7 @@ export default {
|
|||||||
episodeCount: '{count}集',
|
episodeCount: '{count}集',
|
||||||
actions: {
|
actions: {
|
||||||
searchResource: '搜索资源',
|
searchResource: '搜索资源',
|
||||||
|
searchSubtitle: '搜索字幕',
|
||||||
subscribe: '订阅',
|
subscribe: '订阅',
|
||||||
playOnline: '在线播放',
|
playOnline: '在线播放',
|
||||||
playInApp: 'APP播放',
|
playInApp: 'APP播放',
|
||||||
|
|||||||
@@ -949,6 +949,7 @@ export default {
|
|||||||
episodeCount: '{count}集',
|
episodeCount: '{count}集',
|
||||||
actions: {
|
actions: {
|
||||||
searchResource: '搜索資源',
|
searchResource: '搜索資源',
|
||||||
|
searchSubtitle: '搜索字幕',
|
||||||
subscribe: '訂閱',
|
subscribe: '訂閱',
|
||||||
playOnline: '線上播放',
|
playOnline: '線上播放',
|
||||||
playInApp: 'APP播放',
|
playInApp: 'APP播放',
|
||||||
|
|||||||
+29
-4
@@ -43,6 +43,7 @@ interface SearchParams {
|
|||||||
title: string
|
title: string
|
||||||
year: string
|
year: string
|
||||||
season: string
|
season: string
|
||||||
|
episode: string
|
||||||
sites: string
|
sites: string
|
||||||
result_type: string
|
result_type: string
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ function createSearchParams(query: LocationQuery): SearchParams {
|
|||||||
title: query?.title?.toString() ?? '',
|
title: query?.title?.toString() ?? '',
|
||||||
year: query?.year?.toString() ?? '',
|
year: query?.year?.toString() ?? '',
|
||||||
season: query?.season?.toString() ?? '',
|
season: query?.season?.toString() ?? '',
|
||||||
|
episode: query?.episode?.toString() ?? '',
|
||||||
sites: query?.sites?.toString() ?? '',
|
sites: query?.sites?.toString() ?? '',
|
||||||
result_type: query?.result_type?.toString() === 'subtitle' ? 'subtitle' : 'torrent',
|
result_type: query?.result_type?.toString() === 'subtitle' ? 'subtitle' : 'torrent',
|
||||||
}
|
}
|
||||||
@@ -78,6 +80,7 @@ function normalizeSearchParams(params?: Partial<SearchParams> | null): SearchPar
|
|||||||
title: params?.title?.toString() ?? '',
|
title: params?.title?.toString() ?? '',
|
||||||
year: params?.year?.toString() ?? '',
|
year: params?.year?.toString() ?? '',
|
||||||
season: params?.season?.toString() ?? '',
|
season: params?.season?.toString() ?? '',
|
||||||
|
episode: params?.episode?.toString() ?? '',
|
||||||
sites: params?.sites?.toString() ?? '',
|
sites: params?.sites?.toString() ?? '',
|
||||||
result_type: params?.result_type?.toString() === 'subtitle' ? 'subtitle' : 'torrent',
|
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 isMediaSearch = /^[a-zA-Z]+:/.test(params.keyword)
|
||||||
const url = getApiUrl(
|
const url = getApiUrl(
|
||||||
params.result_type === 'subtitle'
|
params.result_type === 'subtitle'
|
||||||
? 'search/subtitle/title/stream'
|
? isMediaSearch
|
||||||
|
? `search/subtitle/media/${encodeURIComponent(params.keyword)}/stream`
|
||||||
|
: 'search/subtitle/title/stream'
|
||||||
: isMediaSearch
|
: isMediaSearch
|
||||||
? `search/media/${encodeURIComponent(params.keyword)}/stream`
|
? `search/media/${encodeURIComponent(params.keyword)}/stream`
|
||||||
: 'search/title/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, 'keyword', params.keyword)
|
||||||
setSearchParam(url.searchParams, 'sites', params.sites)
|
setSearchParam(url.searchParams, 'sites', params.sites)
|
||||||
} else if (isMediaSearch) {
|
} else if (isMediaSearch) {
|
||||||
@@ -732,8 +744,21 @@ async function searchByRequest(params: SearchParams, requestToken?: string) {
|
|||||||
// 静默刷新使用普通请求,保留当前结果直到新数据完整返回,避免返回页面时露出搜索进度态。
|
// 静默刷新使用普通请求,保留当前结果直到新数据完整返回,避免返回页面时露出搜索进度态。
|
||||||
async function requestSearchResults(params: SearchParams, requestToken?: string) {
|
async function requestSearchResults(params: SearchParams, requestToken?: string) {
|
||||||
let result: { [key: string]: any }
|
let result: { [key: string]: any }
|
||||||
|
const isMediaSearch = /^[a-zA-Z]+:/.test(params.keyword)
|
||||||
// 如果keyword的格式是 xxxx:xxxxx 且:前面的xxxx为字符,则按照媒体ID格式搜索
|
// 如果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', {
|
result = await api.get('search/subtitle/title', {
|
||||||
params: {
|
params: {
|
||||||
keyword: params.keyword,
|
keyword: params.keyword,
|
||||||
@@ -741,7 +766,7 @@ async function requestSearchResults(params: SearchParams, requestToken?: string)
|
|||||||
_ts: requestToken,
|
_ts: requestToken,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (/^[a-zA-Z]+:/.test(params.keyword)) {
|
} else if (isMediaSearch) {
|
||||||
result = await api.get(`search/media/${params.keyword}`, {
|
result = await api.get(`search/media/${params.keyword}`, {
|
||||||
params: {
|
params: {
|
||||||
mtype: params.type,
|
mtype: params.type,
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ const globalSettings = globalSettingsStore.globalSettings
|
|||||||
// 用户 Store
|
// 用户 Store
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const canSearch = computed(() =>
|
||||||
|
hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search'),
|
||||||
|
)
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -491,9 +495,16 @@ function joinArray(arr: string[]) {
|
|||||||
return arr.join('、')
|
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 keyword = getMediaId()
|
||||||
|
const season = options.season ?? mediaDetail.value.season
|
||||||
|
const episode = options.episode ?? null
|
||||||
router.push({
|
router.push({
|
||||||
path: '/resource',
|
path: '/resource',
|
||||||
query: {
|
query: {
|
||||||
@@ -502,8 +513,10 @@ function handleSearch() {
|
|||||||
area: searchType.value,
|
area: searchType.value,
|
||||||
title: mediaDetail.value.title,
|
title: mediaDetail.value.title,
|
||||||
year: mediaDetail.value.year,
|
year: mediaDetail.value.year,
|
||||||
season: mediaDetail.value.season,
|
season,
|
||||||
|
episode,
|
||||||
sites: selectedSites.value.join(','),
|
sites: selectedSites.value.join(','),
|
||||||
|
result_type: resultType,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -572,6 +585,16 @@ function searchSites(sites: number[]) {
|
|||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索字幕
|
||||||
|
function handleSubtitleSearch() {
|
||||||
|
handleSearch('subtitle')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索单集字幕
|
||||||
|
function handleEpisodeSubtitleSearch(season: number | null, episode: number | null) {
|
||||||
|
handleSearch('subtitle', { season, episode })
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
getMediaDetail()
|
getMediaDetail()
|
||||||
})
|
})
|
||||||
@@ -637,7 +660,7 @@ onBeforeMount(() => {
|
|||||||
<VBtn
|
<VBtn
|
||||||
v-if="
|
v-if="
|
||||||
(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) &&
|
(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) &&
|
||||||
hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')
|
canSearch
|
||||||
"
|
"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
color="info"
|
color="info"
|
||||||
@@ -658,6 +681,21 @@ onBeforeMount(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
</VBtn>
|
</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
|
<VBtn
|
||||||
v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id"
|
v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id"
|
||||||
class="ms-2 mb-2"
|
class="ms-2 mb-2"
|
||||||
@@ -816,6 +854,25 @@ onBeforeMount(() => {
|
|||||||
class="ms-2"
|
class="ms-2"
|
||||||
size="small"
|
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>
|
</div>
|
||||||
<p>{{ episode.overview }}</p>
|
<p>{{ episode.overview }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user