Compare commits

..

20 Commits

Author SHA1 Message Date
jxxghp
229b7b0c12 chore: bump version to 2.8.5 in package.json 2025-11-20 19:37:58 +08:00
jxxghp
4b7b5ff8a4 fix #397 2025-11-20 19:37:33 +08:00
jxxghp
4906bde746 chore: bump version to 2.8.4 in package.json and refactor AccountSettingSystem.vue to streamline AI agent settings 2025-11-20 19:25:28 +08:00
jxxghp
a87a1a8988 Merge pull request #403 from madrays/v2 2025-11-20 19:11:51 +08:00
madrays
e05f45e681 增加自动拉取可用ai模型的易用性功能 2025-11-20 19:01:25 +08:00
jxxghp
b4acacea81 chore: bump version to 2.8.3 in package.json 2025-11-18 12:49:06 +08:00
jxxghp
fa9645b05b Merge pull request #402 from cddjr/trimemedia 2025-11-17 14:21:39 +08:00
景大侠
1ed4052814 fix #401 2025-11-17 14:08:51 +08:00
jxxghp
7dc814461f 更新 package.json 2025-11-16 06:31:32 +08:00
jxxghp
9154ec0e8c Merge pull request #400 from wikrin/cursor-move 2025-11-16 06:30:57 +08:00
jxxghp
3a2ea60583 Merge pull request #399 from wikrin/release_dates 2025-11-16 06:30:31 +08:00
Attente
b36bff3a1e feat(dashboard): 移除 Vue 渲染模式下的固定拖拽图标
更新`docs/module-federation-guide.md` 文档,使用 `v-hover` 实现仅在鼠标悬停时显示拖拽图标。
2025-11-15 18:03:29 +08:00
Attente
b3d8cbf280 feat: 为媒体信息添加数字/实体发行日期支持 2025-11-13 23:52:54 +08:00
jxxghp
38fb02d112 Merge pull request #398 from cddjr/trimemedia 2025-11-05 23:16:28 +08:00
景大侠
2597f893cd rename 2025-11-05 15:26:34 +08:00
景大侠
ebdd036654 避免飞牛媒体库的图片地址携带敏感数据 2025-11-05 15:15:36 +08:00
景大侠
5032f0e6a9 fix 飞牛影视无法显示图片
图片接口增加Cookies参数
2025-11-04 11:32:41 +08:00
jxxghp
ad963d718d refactor: Remove unused AI agent subheader from account settings 2025-11-01 10:39:25 +08:00
jxxghp
69d314bce3 feat: Add AI agent settings and localization support for LLM configuration 2025-10-31 11:46:45 +08:00
jxxghp
4a7425a947 feat: Add download count formatting function and update card components to use it 2025-10-18 20:13:41 +08:00
17 changed files with 334 additions and 23 deletions

View File

@@ -245,13 +245,21 @@ const props = defineProps({
<template> <template>
<div class="dashboard-widget"> <div class="dashboard-widget">
<!-- 仪表板内容 --> <v-hover>
<v-card> <!-- 仪表板内容 -->
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title> <template #default="{ isHovering, props: hoverProps }">
<v-card-text> <v-card v-bind="hoverProps">
<!-- 组件内容 --> <v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
</v-card-text> <v-card-text>
</v-card> <!-- 组件内容 -->
</v-card-text>
<!-- 只在悬停时显示拖拽图标 -->
<div v-show="isHovering" class="absolute right-5 top-5">
<v-icon class="cursor-move">mdi-drag</v-icon>
</div>
</v-card>
</template>
</v-hover>
</div> </div>
</template> </template>
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.8.1", "version": "2.8.5",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",

View File

@@ -23,6 +23,13 @@ export function kFormatter(num: number) {
: Math.abs(num).toFixed(0).replace(regex, ',') : Math.abs(num).toFixed(0).replace(regex, ',')
} }
// 格式化下载量显示超过1000显示为x.xk格式
export function formatDownloadCount(num: number): string {
if (!num || num < 1000) return num?.toLocaleString() || '0'
return `${(num / 1000).toFixed(1)}k`
}
/** /**
* Format and return date in Humanize format * Format and return date in Humanize format
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format * Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format

View File

@@ -314,6 +314,8 @@ export interface MediaInfo {
production_countries?: any[] production_countries?: any[]
// 语种 // 语种
spoken_languages?: string[] spoken_languages?: string[]
// 数字/实体发行日期
release_dates?: MediaRelease[]
// 状态 // 状态
status?: string status?: string
// 标签 // 标签
@@ -368,6 +370,18 @@ export interface TmdbSeason {
vote_average?: number vote_average?: number
} }
// 发行信息
export interface MediaRelease {
// 发行日期
date: string
// 发行地区
iso_code: string
// 备注
note?: string
// 发行类型
type: number
}
// TMDB集信息 // TMDB集信息
export interface TmdbEpisode { export interface TmdbEpisode {
// 上映日期 // 上映日期
@@ -992,6 +1006,8 @@ export interface MediaServerPlayItem {
percent?: number percent?: number
// 媒体服务器类型 // 媒体服务器类型
server_type?: string server_type?: string
// 图片是否需要Cookies
use_cookies?: boolean
} }
// 媒体服务器媒体库 // 媒体服务器媒体库
@@ -1014,6 +1030,8 @@ export interface MediaServerLibrary {
link?: string link?: string
// 媒体服务器类型 // 媒体服务器类型
server_type?: string server_type?: string
// 图片是否需要Cookies
use_cookies?: boolean
} }
// 消息通知 // 消息通知

View File

@@ -26,7 +26,12 @@ async function goPlay() {
// 计算图片地址 // 计算图片地址
const getImgUrl = computed(() => { const getImgUrl = computed(() => {
const image = props.media?.image || '' const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}` let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
const use_cookies = props.media?.use_cookies
if (use_cookies) {
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
return url
}) })
</script> </script>

View File

@@ -52,20 +52,28 @@ async function goPlay() {
} }
// 生成图片代理路径 // 生成图片代理路径
function getImgUrl(url: string) { function getImgUrl(url: string, use_cookies?: boolean) {
if (!url) return getDefaultImage() if (!url) return getDefaultImage()
else return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}` let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
if (use_cookies) {
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
return imgurl
} }
// 根据多张图片生成媒体库封面 // 根据多张图片生成媒体库封面
async function drawImages(imageList: string[]) { async function drawImages(imageList: string[], use_cookies?: boolean) {
// 图片 // 图片
const IMAGES = imageList const IMAGES = imageList
if (IMAGES.length === 0) return getDefaultImage() if (IMAGES.length === 0) return getDefaultImage()
// 为所有图片添加system/img前缀 // 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++) for (let i = 0; i < IMAGES.length; i++) {
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}` IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
if (use_cookies) {
IMAGES[i] += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
}
// canvas // canvas
const canvas = canvasRef.value const canvas = canvasRef.value
@@ -137,8 +145,8 @@ async function drawImages(imageList: string[]) {
onMounted(async () => { onMounted(async () => {
if (props.media?.image_list && props.media?.image_list.length > 0) if (props.media?.image_list && props.media?.image_list.length > 0)
imgUrl.value = await drawImages(props.media?.image_list || []) imgUrl.value = await drawImages(props.media?.image_list || [], props.media?.use_cookies)
else imgUrl.value = getImgUrl(props.media?.image || '') else imgUrl.value = getImgUrl(props.media?.image || '', props.media?.use_cookies)
}) })
</script> </script>

View File

@@ -6,6 +6,7 @@ import type { Plugin } from '@/api/types'
import { getLogoUrl } from '@/utils/imageUtils' import { getLogoUrl } from '@/utils/imageUtils'
import { getDominantColor } from '@/@core/utils/image' import { getDominantColor } from '@/@core/utils/image'
import { isNullOrEmptyObject } from '@/@core/utils' import { isNullOrEmptyObject } from '@/@core/utils'
import { formatDownloadCount } from '@/@core/utils/formatters'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue' import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -244,7 +245,7 @@ const dropdownItems = ref([
</div> </div>
<div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center"> <div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center">
<VIcon size="small" icon="mdi-download" /> <VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span> <span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
</div> </div>
</div> </div>
<div class="absolute bottom-0 right-0"> <div class="absolute bottom-0 right-0">
@@ -327,7 +328,7 @@ const dropdownItems = ref([
}}</VBtn> }}</VBtn>
<div class="text-xs mt-2" v-if="props.count"> <div class="text-xs mt-2" v-if="props.count">
<VIcon icon="mdi-fire" />{{ <VIcon icon="mdi-fire" />{{
t('plugin.totalDownloads', { count: props.count?.toLocaleString() }) t('plugin.totalDownloads', { count: formatDownloadCount(props.count) })
}} }}
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import type { Plugin } from '@/api/types'
import { isNullOrEmptyObject } from '@core/utils' import { isNullOrEmptyObject } from '@core/utils'
import { getLogoUrl } from '@/utils/imageUtils' import { getLogoUrl } from '@/utils/imageUtils'
import { getDominantColor } from '@/@core/utils/image' import { getDominantColor } from '@/@core/utils/image'
import { formatDownloadCount } from '@/@core/utils/formatters'
import VersionHistory from '@/components/misc/VersionHistory.vue' import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue' import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue' import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
@@ -492,7 +493,7 @@ watch(
</div> </div>
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle"> <span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle">
<VIcon size="small" icon="mdi-download" /> <VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span> <span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
</span> </span>
</div> </div>
<div class="absolute bottom-0 right-0"> <div class="absolute bottom-0 right-0">

View File

@@ -28,7 +28,12 @@ function getChipColor(type: string) {
const getImgUrl = computed(() => { const getImgUrl = computed(() => {
if (imageLoadError.value) return noImage if (imageLoadError.value) return noImage
const image = props.media?.image || '' const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}` let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
const use_cookies = props.media?.use_cookies
if (use_cookies) {
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
return url
}) })
// 跳转播放 // 跳转播放

View File

@@ -6,10 +6,20 @@ import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } fr
import { formatFileSize } from '@/@core/utils/formatters' import { formatFileSize } from '@/@core/utils/formatters'
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs' import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import { numberValidator } from '@/@validators'
import { useGlobalSettingsStore } from '@/stores'
// 多语言支持 // 多语言支持
const { t } = useI18n() const { t } = useI18n()
// 从 provide 中获取全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 当前识别类型
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb')
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
title: String, title: String,
@@ -38,6 +48,18 @@ const directories = ref<TransferDirectoryConf[]>([])
// 是否正在加载 // 是否正在加载
const loading = ref(false) const loading = ref(false)
// 是否显示高级选项
const showAdvancedOptions = ref(false)
// TMDB ID
const tmdbid = ref<number | undefined>(undefined)
// 豆瓣ID
const doubanId = ref<string | undefined>(undefined)
// TMDB选择对话框
const mediaSelectorDialog = ref(false)
// 计算按钮图标 // 计算按钮图标
const icon = computed(() => (loading.value ? 'mdi-progress-download' : 'mdi-download')) const icon = computed(() => (loading.value ? 'mdi-progress-download' : 'mdi-download'))
@@ -96,6 +118,14 @@ async function addDownload() {
payload.media_in = props.media payload.media_in = props.media
} }
// 添加媒体ID辅助识别
if (tmdbid.value) {
payload.tmdbid = tmdbid.value
}
if (doubanId.value) {
payload.doubanid = doubanId.value
}
const endpoint = props.media ? 'download/' : 'download/add' const endpoint = props.media ? 'download/' : 'download/add'
result = await api.post(endpoint, payload) result = await api.post(endpoint, payload)
@@ -202,6 +232,56 @@ onMounted(() => {
/> />
</VCol> </VCol>
</VRow> </VRow>
<VRow class="px-5 mt-2">
<VCol cols="12">
<VBtn
variant="text"
size="small"
:prepend-icon="showAdvancedOptions ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click="showAdvancedOptions = !showAdvancedOptions"
>
{{
showAdvancedOptions
? t('dialog.addDownload.hideAdvancedOptions')
: t('dialog.addDownload.showAdvancedOptions')
}}
</VBtn>
</VCol>
</VRow>
<VRow v-show="showAdvancedOptions" class="px-5">
<VCol cols="12">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
:label="t('dialog.reorganize.tmdbId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
size="small"
variant="underlined"
density="comfortable"
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
v-else
v-model="doubanId"
:label="t('dialog.reorganize.doubanId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
size="small"
variant="underlined"
density="comfortable"
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
</VRow>
</VCardText> </VCardText>
<VCardText class="text-center"> <VCardText class="text-center">
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5"> <VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
@@ -209,5 +289,15 @@ onMounted(() => {
</VBtn> </VBtn>
</VCardText> </VCardText>
</VCard> </VCard>
<!-- 媒体ID选择器 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
</VDialog>
</VDialog> </VDialog>
</template> </template>

View File

@@ -91,10 +91,6 @@ onUnmounted(() => {
<!-- Vue 渲染模式 --> <!-- Vue 渲染模式 -->
<div v-if="pluginRenderMode === 'vue'"> <div v-if="pluginRenderMode === 'vue'">
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" /> <component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" />
<!-- Vue 模式下也可以显示拖拽句柄 -->
<div class="absolute right-5 top-5">
<VIcon class="cursor-move">mdi-drag</VIcon>
</div>
</div> </div>
<!-- Vuetify 渲染模式 --> <!-- Vuetify 渲染模式 -->
<VHover v-else-if="pluginRenderMode === 'vuetify'"> <VHover v-else-if="pluginRenderMode === 'vuetify'">

View File

@@ -187,6 +187,7 @@ export function useSetupWizard() {
'emby': 'EmbyModule', 'emby': 'EmbyModule',
'jellyfin': 'JellyfinModule', 'jellyfin': 'JellyfinModule',
'plex': 'PlexModule', 'plex': 'PlexModule',
'trimemedia': 'TrimeMediaModule',
}, },
// 通知映射 // 通知映射
notification: { notification: {

View File

@@ -788,6 +788,8 @@ export default {
originalTitle: 'Original Title', originalTitle: 'Original Title',
status: 'Status', status: 'Status',
releaseDate: 'Release Date', releaseDate: 'Release Date',
digitalRelease: 'Digital Release',
physicalRelease: 'Physical Release',
originalLanguage: 'Original Language', originalLanguage: 'Original Language',
productionCountries: 'Production Countries', productionCountries: 'Production Countries',
productionCompanies: 'Production Companies', productionCompanies: 'Production Companies',
@@ -1242,6 +1244,18 @@ export default {
'Used to increase the rate limit threshold when plugins access Github APIit is recommended to configure, otherwise plugins may not work properly', 'Used to increase the rate limit threshold when plugins access Github APIit is recommended to configure, otherwise plugins may not work properly',
ocrHost: 'OCR Server', ocrHost: 'OCR Server',
ocrHostHint: 'Used for site check-in, updating site cookies and other captcha recognition', ocrHostHint: 'Used for site check-in, updating site cookies and other captcha recognition',
aiAgent: 'Enable AI Assistant',
aiAgentEnable: 'Enable AI Assistant',
aiAgentEnableHint: 'Enable AI assistant functionality, requires LLM configuration',
llmProvider: 'LLM Provider',
llmProviderHint: 'Select the LLM service provider to use',
llmModel: 'LLM Model Name',
llmModelHint: 'Specify the LLM model to use, such as gpt-3.5-turbo, deepseek-chat, etc.',
llmApiKey: 'LLM API Key',
llmApiKeyHint: 'API key from the LLM service provider for authentication',
llmApiKeyPlaceholder: 'Please enter API key',
llmBaseUrl: 'LLM Base URL',
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
advancedSettings: 'Advanced Settings', advancedSettings: 'Advanced Settings',
advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases', advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases',
downloaders: 'Downloaders', downloaders: 'Downloaders',
@@ -1900,6 +1914,8 @@ export default {
startDownload: 'Start Download', startDownload: 'Start Download',
downloadSuccess: '{site} {title} downloaded successfully!', downloadSuccess: '{site} {title} downloaded successfully!',
downloadFailed: '{site} {title} download failed: {message}!', downloadFailed: '{site} {title} download failed: {message}!',
showAdvancedOptions: 'Show Advanced Options',
hideAdvancedOptions: 'Hide Advanced Options',
}, },
subscribeShare: { subscribeShare: {
shareSubscription: 'Share Subscription', shareSubscription: 'Share Subscription',

View File

@@ -786,6 +786,8 @@ export default {
originalTitle: '原始标题', originalTitle: '原始标题',
status: '状态', status: '状态',
releaseDate: '上映日期', releaseDate: '上映日期',
digitalRelease: '数字发行',
physicalRelease: '实体发行',
originalLanguage: '原始语言', originalLanguage: '原始语言',
productionCountries: '出品国家', productionCountries: '出品国家',
productionCompanies: '制作公司', productionCompanies: '制作公司',
@@ -1238,6 +1240,18 @@ export default {
githubTokenHint: '用于提高插件等访问Github API时的限流阈值建议配置否则插件可能无法正常使用', githubTokenHint: '用于提高插件等访问Github API时的限流阈值建议配置否则插件可能无法正常使用',
ocrHost: '验证码识别服务器', ocrHost: '验证码识别服务器',
ocrHostHint: '用于站点签到、更新站点Cookie等识别验证码', ocrHostHint: '用于站点签到、更新站点Cookie等识别验证码',
aiAgent: '启用智能助手',
aiAgentEnable: '启用智能助手',
aiAgentEnableHint: '启用后可使用智能助手功能需要配置LLM相关参数',
llmProvider: 'LLM提供商',
llmProviderHint: '选择使用的LLM服务提供商',
llmModel: 'LLM模型名称',
llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等',
llmApiKey: 'LLM API密钥',
llmApiKeyHint: 'LLM服务提供商的API密钥用于身份验证',
llmApiKeyPlaceholder: '请输入API密钥',
llmBaseUrl: 'LLM基础URL',
llmBaseUrlHint: 'LLM API的基础URL地址用于自定义API端点',
advancedSettings: '高级设置', advancedSettings: '高级设置',
advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整', advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整',
downloaders: '下载器', downloaders: '下载器',
@@ -1876,6 +1890,8 @@ export default {
startDownload: '开始下载', startDownload: '开始下载',
downloadSuccess: '{site} {title} 下载成功!', downloadSuccess: '{site} {title} 下载成功!',
downloadFailed: '{site} {title} 下载失败:{message}', downloadFailed: '{site} {title} 下载失败:{message}',
showAdvancedOptions: '显示高级选项',
hideAdvancedOptions: '隐藏高级选项',
}, },
subscribeShare: { subscribeShare: {
shareSubscription: '分享订阅', shareSubscription: '分享订阅',

View File

@@ -773,6 +773,8 @@ export default {
originalTitle: '原始標題', originalTitle: '原始標題',
status: '狀態', status: '狀態',
releaseDate: '上映日期', releaseDate: '上映日期',
digitalRelease: '數位發行',
physicalRelease: '實體發行',
originalLanguage: '原始語言', originalLanguage: '原始語言',
productionCountries: '出品國家', productionCountries: '出品國家',
productionCompanies: '製作公司', productionCompanies: '製作公司',
@@ -1226,6 +1228,18 @@ export default {
githubTokenHint: '用於提高插件等訪問Github API時的限流閾值建議配置否則插件可能無法正常使用', githubTokenHint: '用於提高插件等訪問Github API時的限流閾值建議配置否則插件可能無法正常使用',
ocrHost: '驗證碼識別服務器', ocrHost: '驗證碼識別服務器',
ocrHostHint: '用於站點簽到、更新站點Cookie等識別驗證碼', ocrHostHint: '用於站點簽到、更新站點Cookie等識別驗證碼',
aiAgent: '啟用智能助手',
aiAgentEnable: '啟用智能助手',
aiAgentEnableHint: '啟用後可使用智能助手功能需要配置LLM相關參數',
llmProvider: 'LLM提供商',
llmProviderHint: '選擇使用的LLM服務提供商',
llmModel: 'LLM模型名稱',
llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等',
llmApiKey: 'LLM API密鑰',
llmApiKeyHint: 'LLM服務提供商的API密鑰用於身份驗證',
llmApiKeyPlaceholder: '請輸入API密鑰',
llmBaseUrl: 'LLM基礎URL',
llmBaseUrlHint: 'LLM API的基礎URL地址用於自定義API端點',
advancedSettings: '高級設置', advancedSettings: '高級設置',
advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整', advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整',
downloaders: '下載器', downloaders: '下載器',
@@ -1862,6 +1876,8 @@ export default {
startDownload: '開始下載', startDownload: '開始下載',
downloadSuccess: '{site} {title} 下載成功!', downloadSuccess: '{site} {title} 下載成功!',
downloadFailed: '{site} {title} 下載失敗:{message}', downloadFailed: '{site} {title} 下載失敗:{message}',
showAdvancedOptions: '顯示高級選項',
hideAdvancedOptions: '隱藏高級選項',
}, },
subscribeShare: { subscribeShare: {
shareSubscription: '分享訂閱', shareSubscription: '分享訂閱',

View File

@@ -428,6 +428,17 @@ const getProductionCompanies = computed(() => {
return mediaDetail.value.production_companies?.map(company => company.name) return mediaDetail.value.production_companies?.map(company => company.name)
}) })
// 获取最早实体/数字发行日期
const getEarliestReleaseDate = computed(() => {
const filteredDates = mediaDetail.value.release_dates?.filter(date => [4, 5].includes(date.type))
if (!filteredDates || filteredDates.length === 0)
return null
return filteredDates.reduce((earliest, current) =>
new Date(current.date) < new Date(earliest.date) ? current : earliest,
)
})
// 计算存在状态的颜色 // 计算存在状态的颜色
function getExistColor(season: number) { function getExistColor(season: number) {
const state = seasonsNotExisted.value[season] const state = seasonsNotExisted.value[season]
@@ -840,6 +851,17 @@ onBeforeMount(() => {
</span> </span>
</span> </span>
</div> </div>
<div v-if="mediaDetail.type === '电影' && getEarliestReleaseDate" class="media-fact">
<span>{{ t(getEarliestReleaseDate.type === 4 ? 'media.info.digitalRelease' : 'media.info.physicalRelease') }}</span>
<span class="media-fact-value">
<span class="flex items-center justify-end">
<span class="inline-flex items-center justify-center h-4 w-4 text-[0.6rem] font-bold text-current border border-current leading-none">
{{ getEarliestReleaseDate.iso_code }}
</span>
<span class="ml-1.5">{{ getEarliestReleaseDate.date.slice(0, 10) }}</span>
</span>
</span>
</div>
<div v-if="mediaDetail.original_language" class="media-fact"> <div v-if="mediaDetail.original_language" class="media-fact">
<span>{{ t('media.info.originalLanguage') }}</span> <span>{{ t('media.info.originalLanguage') }}</span>
<span class="media-fact-value">{{ mediaDetail.original_language }}</span> <span class="media-fact-value">{{ mediaDetail.original_language }}</span>

View File

@@ -31,6 +31,11 @@ const SystemSettings = ref<any>({
GITHUB_TOKEN: null, GITHUB_TOKEN: null,
OCR_HOST: null, OCR_HOST: null,
CUSTOMIZE_WALLPAPER_API_URL: null, CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false,
LLM_PROVIDER: 'deepseek',
LLM_MODEL: 'deepseek-chat',
LLM_API_KEY: null,
LLM_BASE_URL: 'https://api.deepseek.com',
}, },
// 高级系统设置 // 高级系统设置
Advanced: { Advanced: {
@@ -114,6 +119,10 @@ const progressDialog = ref(false)
// 高级设置对话框 // 高级设置对话框
const advancedDialog = ref(false) const advancedDialog = ref(false)
// LLM 模型列表
const llmModels = ref<string[]>([])
const loadingModels = ref(false)
const activeTab = ref('system') const activeTab = ref('system')
// 元数据语言 // 元数据语言
@@ -149,6 +158,30 @@ const logLevelItems = [
// 安全域名添加变量 // 安全域名添加变量
const newSecurityDomain = ref('') const newSecurityDomain = ref('')
// 加载LLM模型列表
async function loadLlmModels() {
loadingModels.value = true
try {
const result: { [key: string]: any } = await api.get('system/llm-models', {
params: {
provider: SystemSettings.value.Basic.LLM_PROVIDER,
api_key: SystemSettings.value.Basic.LLM_API_KEY,
base_url: SystemSettings.value.Basic.LLM_BASE_URL,
},
})
if (result.success) {
llmModels.value = result.data
if (llmModels.value.length > 0) SystemSettings.value.Basic.LLM_MODEL = llmModels.value[0]
} else {
$toast.error(result.message)
}
} catch (error) {
console.log(error)
}
loadingModels.value = false
}
// 添加安全域名 // 添加安全域名
function addSecurityDomain() { function addSecurityDomain() {
if ( if (
@@ -607,6 +640,74 @@ onDeactivated(() => {
/> />
</VCol> </VCol>
</VRow> </VRow>
<VDivider class="my-4" />
<VRow>
<VCol cols="12">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
:label="t('setting.system.aiAgentEnable')"
:hint="t('setting.system.aiAgentEnableHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.LLM_PROVIDER"
:label="t('setting.system.llmProvider')"
:hint="t('setting.system.llmProviderHint')"
persistent-hint
:items="[
{ title: 'OpenAI', value: 'openai' },
{ title: 'Google', value: 'google' },
{ title: 'DeepSeek', value: 'deepseek' },
]"
prepend-inner-icon="mdi-robot"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_BASE_URL"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
placeholder="https://api.deepseek.com"
persistent-hint
prepend-inner-icon="mdi-link"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_API_KEY"
:label="t('setting.system.llmApiKey')"
:hint="t('setting.system.llmApiKeyHint')"
:placeholder="t('setting.system.llmApiKeyPlaceholder')"
persistent-hint
type="password"
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VCombobox
v-model="SystemSettings.Basic.LLM_MODEL"
:label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelHint')"
persistent-hint
:items="llmModels"
:loading="loadingModels"
prepend-inner-icon="mdi-brain"
>
<template #append-inner>
<VBtn
variant="text"
icon="mdi-refresh"
size="small"
@click="loadLlmModels"
:disabled="!SystemSettings.Basic.LLM_API_KEY"
/>
</template>
</VCombobox>
</VCol>
</VRow>
</VForm> </VForm>
</VCardText> </VCardText>
<VCardText> <VCardText>