feat: 新增站点音乐资源直搜入口

This commit is contained in:
jxxghp
2026-08-13 21:14:27 +08:00
parent fd21968dc9
commit 2754db6ccb
6 changed files with 138 additions and 9 deletions

View File

@@ -68,7 +68,7 @@ const SubscribeItems = ref<Subscribe[]>([])
const chooseSiteDialog = ref(false)
const selectedSites = ref<number[]>([])
const allSites = ref<Site[]>([])
const siteSearchType = ref<'torrent' | 'subtitle'>('torrent')
const siteSearchType = ref<'torrent' | 'subtitle' | 'music'>('torrent')
// 定义事件
const emit = defineEmits(['close', 'update:modelValue'])
@@ -373,10 +373,7 @@ async function fetchSubscribes() {
const loadUserSitePreferences = async () => {
try {
const result = await api.get<{ value?: number[] }>('system/setting/public/IndexerSites')
if (result.value) {
selectedSites.value = result.value
return
}
selectedSites.value = result.value ?? []
} catch (err) {
console.error(err)
}
@@ -388,6 +385,8 @@ async function queryAllSites() {
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
const availableIds = new Set(allSites.value.map(site => site.id))
selectedSites.value = selectedSites.value.filter(siteId => availableIds.has(siteId))
// 如果没有选择任何站点并且有可用站点,才默认选择全部
if (selectedSites.value.length === 0 && allSites.value.length > 0) {
selectedSites.value = allSites.value.map((site: Site) => site.id)
@@ -397,12 +396,44 @@ async function queryAllSites() {
}
}
/** 查询支持音乐分类的启用站点,并将默认选择收敛到可用范围。 */
async function queryMusicSites() {
try {
const data: Site[] = await api.get('site/media/music')
allSites.value = data.filter(item => item.is_active)
const availableIds = new Set(allSites.value.map(site => site.id))
selectedSites.value = selectedSites.value.filter(siteId => availableIds.has(siteId))
if (selectedSites.value.length === 0) {
selectedSites.value = allSites.value.map(site => site.id)
}
} catch (error) {
console.error(error)
allSites.value = []
selectedSites.value = []
}
}
/** 打开指定资源类型的站点选择对话框。 */
const openSiteDialog = (type: 'torrent' | 'subtitle' = 'torrent') => {
async function openSiteDialog(type: 'torrent' | 'subtitle' | 'music' = 'torrent') {
siteSearchType.value = type
await loadUserSitePreferences()
if (type === 'music') {
await queryMusicSites()
} else {
await queryAllSites()
}
chooseSiteDialog.value = true
}
/** 按当前搜索类型刷新站点选择列表。 */
async function reloadSearchSites() {
if (siteSearchType.value === 'music') {
await queryMusicSites()
return
}
await queryAllSites()
}
// 匹配的订阅列表
const matchedSubscribeItems = computed(() => {
if (!searchWord.value) return []
@@ -421,6 +452,10 @@ function searchSites(sites: number[]) {
searchSubtitle()
return
}
if (siteSearchType.value === 'music') {
searchMusicTorrent()
return
}
searchTorrent()
}
@@ -458,6 +493,23 @@ function searchSubtitle() {
closeSearch()
}
/** 使用当前关键词直接搜索站点音乐资源,不经过媒体信息识别。 */
function searchMusicTorrent() {
if (!searchWord.value || !hasSearchPermission.value) return
saveRecentSearches(searchWord.value)
router.push({
path: '/resource',
query: {
keyword: searchWord.value,
type: '音乐',
area: 'title',
result_type: 'torrent',
sites: selectedSites.value.join(','),
},
})
closeSearch()
}
/** 使用当前关键词搜索指定类型的媒体信息。 */
function searchMedia(searchType: MediaSearchType) {
if (!searchWord.value || !hasDiscoveryPermission.value) return
@@ -881,6 +933,21 @@ onMounted(() => {
</template>
</VListItem>
<VListItem density="comfortable" link @click="openSiteDialog('music')" class="search-result-item mx-2 my-1">
<template #prepend>
<div class="result-icon-wrapper">
<VIcon icon="mdi-music-box-multiple-outline" size="small" color="medium-emphasis" />
</div>
</template>
<VListItemTitle class="font-weight-medium text-body-2">{{
t('dialog.searchBar.searchMusicInSites')
}}</VListItemTitle>
<VListItemSubtitle class="text-caption text-medium-emphasis">
{{ t('common.search') }} <span class="primary-text font-weight-medium">{{ searchWord }}</span>
{{ t('dialog.searchBar.relatedMusicResources') }}
</VListItemSubtitle>
</VListItem>
<VListItem density="comfortable" link @click="searchSubtitle" class="search-result-item mx-2 my-1">
<template #prepend>
<div class="result-icon-wrapper">
@@ -956,7 +1023,7 @@ onMounted(() => {
:selected="selectedSites"
@search="searchSites"
@close="chooseSiteDialog = false"
@reload="queryAllSites"
@reload="reloadSearchSites"
/>
</template>

View File

@@ -4,7 +4,17 @@ import { screen, waitFor, within } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { renderWithProviders } from '@tests/support/render'
import { defineComponent } from 'vue'
import { beforeEach, describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
apiGet: vi.fn(),
}))
vi.mock('@/api', () => ({
default: createDataApiMock({
get: (...args: unknown[]) => mocks.apiGet(...args),
}),
}))
const IconStub = defineComponent({
name: 'VIcon',
@@ -16,6 +26,7 @@ const IconStub = defineComponent({
async function renderSearchBar(
overrides: {
searchPermission?: boolean
searchSource?: string
} = {},
) {
@@ -31,7 +42,7 @@ async function renderSearchBar(
admin: false,
discovery: true,
manage: false,
search: false,
search: overrides.searchPermission ?? false,
subscribe: false,
},
superUser: false,
@@ -45,6 +56,7 @@ async function renderSearchBar(
stubActions: false,
global: {
stubs: {
VDialogCloseBtn: true,
VIcon: IconStub,
},
},
@@ -60,6 +72,16 @@ function getSearchItem(title: string): HTMLElement {
describe('SearchBarDialog media source selection', () => {
beforeEach(() => {
localStorage.clear()
mocks.apiGet.mockImplementation((path: string) => {
if (path === 'system/setting/public/IndexerSites') return Promise.resolve({ value: [11, 99] })
if (path === 'site/media/music') {
return Promise.resolve([
{ id: 11, is_active: true, name: '音乐站' },
{ id: 22, is_active: false, name: '停用音乐站' },
])
}
throw new Error(`Unexpected GET ${path}`)
})
})
it('defaults media searches to TheMovieDB when no global search source is configured', async () => {
@@ -224,6 +246,31 @@ describe('SearchBarDialog media source selection', () => {
expect(getSearchItem('音乐')).not.toHaveTextContent('搜索音乐元数据,并进入站点资源搜索、下载和订阅流程')
})
it('searches music sites by keyword without resolving a media identity', async () => {
const user = userEvent.setup()
const { router } = await renderSearchBar({ searchPermission: true })
const input = await screen.findByPlaceholderText('搜索电影、剧集以及更多...')
await user.type(input, '周杰伦')
await user.click(getSearchItem('在站点中搜索音乐资源'))
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('site/media/music'))
expect(await screen.findByText('音乐站')).toBeInTheDocument()
expect(screen.queryByText('停用音乐站')).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '搜索' }))
await waitFor(() => {
expect(router.currentRoute.value.path).toBe('/resource')
expect(router.currentRoute.value.query).toEqual({
area: 'title',
keyword: '周杰伦',
result_type: 'torrent',
sites: '11',
type: '音乐',
})
})
})
it('searches actors with the selected supported source', async () => {
const user = userEvent.setup()
const { router } = await renderSearchBar()

View File

@@ -2975,8 +2975,10 @@ export default {
subscribeShareSearch: 'Related subscription shares',
siteResources: 'Site Resources',
searchInSites: 'Search for torrent resources in sites',
searchMusicInSites: 'Search for music resources in sites',
searchSubtitlesInSites: 'Search for subtitle resources in sites',
relatedResources: 'Related Resources',
relatedMusicResources: 'Related Music Resources',
relatedSubtitles: 'Related Subtitles',
searchTip: 'You can search for movies, TV shows, actors, resources, etc.',
emptySearchHint: 'Enter keywords to search',

View File

@@ -2924,8 +2924,10 @@ export default {
subscribeShareSearch: '相关的订阅分享',
siteResources: '站点资源',
searchInSites: '在站点中搜索种子资源',
searchMusicInSites: '在站点中搜索音乐资源',
searchSubtitlesInSites: '在站点中搜索字幕资源',
relatedResources: '相关资源',
relatedMusicResources: '相关音乐资源',
relatedSubtitles: '相关字幕',
searchTip: '可搜索电影、电视剧、演员、资源等',
emptySearchHint: '输入关键字开始搜索',

View File

@@ -2923,8 +2923,10 @@ export default {
subscribeShareSearch: '相關的訂閱分享',
siteResources: '站點資源',
searchInSites: '在站點中搜索種子資源',
searchMusicInSites: '在站點中搜索音樂資源',
searchSubtitlesInSites: '在站點中搜索字幕資源',
relatedResources: '相關資源',
relatedMusicResources: '相關音樂資源',
relatedSubtitles: '相關字幕',
searchTip: '可搜索電影、電視劇、演員、資源等',
emptySearchHint: '輸入關鍵字開始搜索',

View File

@@ -336,6 +336,15 @@ const searchRouteCases: SearchRouteCase[] = [
year: '2025',
},
},
{
apiEndpoint: 'search/title',
apiParams: { keyword: '周杰伦', mtype: '音乐', sites: '11,22' },
displayTitle: '音乐资源结果',
expectedPath: '/api/v1/search/title/stream',
query: { keyword: '周杰伦', result_type: 'torrent', sites: '11,22', type: '音乐' },
result: createTorrent({ title: '音乐资源结果' }),
streamParams: { keyword: '周杰伦', mtype: '音乐', sites: '11,22' },
},
{
apiEndpoint: `search/media/${musicBrainzAlbumId}`,
apiParams: {