add Bangumi

This commit is contained in:
jxxghp
2024-03-18 19:03:13 +08:00
parent bc93de8ff2
commit ec4ab8762c
9 changed files with 224 additions and 29 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "1.7.3", "version": "1.7.4",
"private": true, "private": true,
"bin": "dist/service.js", "bin": "dist/service.js",
"scripts": { "scripts": {
+28
View File
@@ -181,6 +181,9 @@ export interface MediaInfo {
// 豆瓣ID // 豆瓣ID
douban_id?: string douban_id?: string
// Bangumi ID
bangumi_id?: string
// 媒体原语种 // 媒体原语种
original_language?: string original_language?: string
@@ -282,6 +285,9 @@ export interface MediaInfo {
// 下一集 // 下一集
next_episode_to_air?: object next_episode_to_air?: object
// 别名
names?: string[]
} }
// TMDB季信息 // TMDB季信息
@@ -422,6 +428,28 @@ export interface DoubanPerson {
} }
// Bangumi人物信息
export interface BangumiPerson {
// ID
id?: number
// 名称
name?: string
// 类型
type?: number
// 角色
career?: string[]
// images large/normal
images?: { [key: string]: string }
// 关系
relation?: string
}
// 站点 // 站点
export interface Site { export interface Site {
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import personIcon from '@images/misc/person-icon.png'
import type { BangumiPerson } from '@/api/types'
const personProps = defineProps({
person: Object as PropType<BangumiPerson>,
width: String,
height: String,
})
// 当前人物
const personInfo = ref(personProps.person)
// 人物图片是否加载
const isImageLoaded = ref(false)
// 人物图片地址
function getPersonImage() {
if (!personInfo.value?.images)
return personIcon
return personInfo.value?.images?.medium
}
// 使用、拼装人物角色
function getPersonCharacter() {
if (!personInfo.value?.career)
return ''
return personInfo.value?.career.join('、')
}
// 打开人物详情
function goPersonDetail() {
if (!personInfo.value?.id)
return
window.open(`https://bangumi.tv/person/${personInfo.value?.id}`, '_blank')
}
</script>
<template>
<VHover v-bind="personProps">
<template #default="hover">
<VCard
v-bind="hover.props"
:height="personProps.height"
:width="personProps.width"
class="rounded-lg"
:class="{
'transition transform-cpu duration-300 scale-105': hover.isHovering,
}"
@click.stop="goPersonDetail"
>
<div
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-bottom: 150%;">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
<VAvatar
size="120"
:class="{
'ring-1 ring-gray-700': isImageLoaded,
}"
>
<VImg
v-img
:src="getPersonImage()"
cover
@load="isImageLoaded = true"
/>
</VAvatar>
</div>
<div class="w-full truncate text-center font-bold">
{{ personInfo?.name }}
</div>
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
{{ getPersonCharacter() }}
</div>
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
</div>
</div>
</div>
</VCard>
</template>
</VHover>
</template>
<style lang="scss">
.person-card {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
}
.person-card:hover {
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
}
</style>
+14 -16
View File
@@ -57,6 +57,15 @@ const seasonInfos = ref<TmdbSeason[]>([])
// 选中的订阅季 // 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([]) const seasonsSelected = ref<TmdbSeason[]>([])
// 获得mediaid
function getMediaId() {
return props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: props.media?.douban_id
? `douban:${props.media?.douban_id}`
: `bangumi:${props.media?.bangumi_id}`
}
// 订阅弹窗选择的多季 // 订阅弹窗选择的多季
function subscribeSeasons() { function subscribeSeasons() {
subscribeSeasonDialog.value = false subscribeSeasonDialog.value = false
@@ -131,6 +140,7 @@ async function addSubscribe(season = 0) {
year: props.media?.year, year: props.media?.year,
tmdbid: props.media?.tmdb_id, tmdbid: props.media?.tmdb_id,
doubanid: props.media?.douban_id, doubanid: props.media?.douban_id,
bangumiid: props.media?.bangumi_id,
season, season,
best_version, best_version,
}) })
@@ -186,9 +196,7 @@ async function removeSubscribe() {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
const mediaid = props.media?.tmdb_id const mediaid = getMediaId()
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const result: { [key: string]: any } = await api.delete( const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`, `subscribe/media/${mediaid}`,
@@ -249,9 +257,7 @@ async function handleCheckExists() {
// 调用API检查是否已订阅,电视剧需要指定季 // 调用API检查是否已订阅,电视剧需要指定季
async function checkSubscribe(season = 0) { async function checkSubscribe(season = 0) {
try { try {
const mediaid = props.media?.tmdb_id const mediaid = getMediaId()
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, { const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: { params: {
@@ -362,11 +368,7 @@ function goMediaDetail() {
router.push({ router.push({
path: '/media', path: '/media',
query: { query: {
mediaid: `${ mediaid: getMediaId(),
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
type: props.media?.type, type: props.media?.type,
}, },
}) })
@@ -377,11 +379,7 @@ function handleSearch() {
router.push({ router.push({
path: '/resource', path: '/resource',
query: { query: {
keyword: `${ keyword: getMediaId(),
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
type: props.media?.type, type: props.media?.type,
area: 'title', area: 'title',
}, },
+6
View File
@@ -16,6 +16,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
title="正在热映" title="正在热映"
/> />
<MediaCardSlideView
apipath="bangumi/calendar"
linkurl="/browse/bangumi/calendar?title=Bangumi每日放送"
title="Bangumi每日放送"
/>
<MediaCardSlideView <MediaCardSlideView
apipath="tmdb/movies" apipath="tmdb/movies"
linkurl="/browse/tmdb/movies?title=热门电影" linkurl="/browse/tmdb/movies?title=热门电影"
+1 -1
View File
@@ -75,7 +75,7 @@ async function fetchData() {
else { else {
startLoadingProgress() startLoadingProgress()
// 优先按TMDBID精确查询 // 优先按TMDBID精确查询
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) { if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
dataList.value = await api.get(`search/media/${keyword}`, { dataList.value = await api.get(`search/media/${keyword}`, {
params: { params: {
mtype: type, mtype: type,
+1 -1
View File
@@ -44,7 +44,7 @@ onMounted(fetchData)
<template #content> <template #content>
<template <template
v-for="data in dataList" v-for="data in dataList"
:key="data.tmdb_id || data.douban_id" :key="data.tmdb_id || data.douban_id || data.bangumi_id"
> >
<MediaCard <MediaCard
:media="data" :media="data"
+71 -10
View File
@@ -51,6 +51,15 @@ const subscribeRules = ref({
show_edit_dialog: false, show_edit_dialog: false,
}) })
// 获得mediaid
function getMediaId() {
return mediaDetail.value?.tmdb_id
? `tmdb:${mediaDetail.value?.tmdb_id}`
: mediaDetail.value?.douban_id
? `douban:${mediaDetail.value?.douban_id}`
: `bangumi:${mediaDetail.value?.bangumi_id}`
}
// 调用API查询详情 // 调用API查询详情
async function getMediaDetail() { async function getMediaDetail() {
if (mediaProps.mediaid && mediaProps.type) { if (mediaProps.mediaid && mediaProps.type) {
@@ -60,7 +69,7 @@ async function getMediaDetail() {
}, },
}) })
isRefreshed.value = true isRefreshed.value = true
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id) if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id)
return return
// 检查存在状态 // 检查存在状态
@@ -113,7 +122,7 @@ async function checkExists() {
// 查询当前媒体是否已订阅 // 查询当前媒体是否已订阅
async function checkSubscribe(season = 0) { async function checkSubscribe(season = 0) {
try { try {
const mediaid = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}` const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, { const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: { params: {
@@ -198,6 +207,7 @@ async function addSubscribe(season = 0) {
year: mediaDetail.value?.year, year: mediaDetail.value?.year,
tmdbid: mediaDetail.value?.tmdb_id, tmdbid: mediaDetail.value?.tmdb_id,
doubanid: mediaDetail.value?.douban_id, doubanid: mediaDetail.value?.douban_id,
bangumiid: mediaDetail.value?.bangumi_id,
season, season,
best_version, best_version,
}) })
@@ -253,9 +263,7 @@ async function removeSubscribe(season: number) {
// 开始处理 // 开始处理
startNProgress() startNProgress()
try { try {
const mediaid = mediaDetail.value?.tmdb_id const mediaid = getMediaId()
? `tmdb:${mediaDetail.value?.tmdb_id}`
: `douban:${mediaDetail.value?.douban_id}`
const result: { [key: string]: any } = await api.delete( const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`, `subscribe/media/${mediaid}`,
@@ -330,6 +338,11 @@ function getTvdbLink() {
return `https://www.thetvdb.com/series/${mediaDetail.value.tvdb_id}` return `https://www.thetvdb.com/series/${mediaDetail.value.tvdb_id}`
} }
// 拼装Bangumi地址
function getBangumiLink() {
return `https://bgm.tv/subject/${mediaDetail.value.bangumi_id}`
}
// 拼装集图片地址 // 拼装集图片地址
function getEpisodeImage(stillPath: string) { function getEpisodeImage(stillPath: string) {
if (!stillPath) if (!stillPath)
@@ -405,7 +418,7 @@ function joinArray(arr: string[]) {
// 开始搜索 // 开始搜索
function handleSearch(area: string) { function handleSearch(area: string) {
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}` const keyword = getMediaId()
router.push({ router.push({
path: '/resource', path: '/resource',
query: { query: {
@@ -453,7 +466,7 @@ onBeforeMount(() => {
color="primary" color="primary"
/> />
</div> </div>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" class="max-w-8xl mx-auto px-4"> <div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path"> <template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96"> <div class="vue-media-back absolute left-0 top-0 w-full h-96">
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover /> <VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
@@ -492,7 +505,7 @@ onBeforeMount(() => {
</span> </span>
</div> </div>
<div class="media-actions"> <div class="media-actions">
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2"> <VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" variant="tonal" color="info" class="mb-2">
<template #prepend> <template #prepend>
<VIcon icon="mdi-magnify" /> <VIcon icon="mdi-magnify" />
</template> </template>
@@ -518,7 +531,7 @@ onBeforeMount(() => {
</VList> </VList>
</VMenu> </VMenu>
</VBtn> </VBtn>
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)"> <VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
<template #prepend> <template #prepend>
<VIcon :icon="getSubscribeIcon" /> <VIcon :icon="getSubscribeIcon" />
</template> </template>
@@ -580,6 +593,12 @@ onBeforeMount(() => {
<span class="ms-1">TheTvDb</span> <span class="ms-1">TheTvDb</span>
</div> </div>
</a> </a>
<a v-if="mediaDetail.bangumi_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getBangumiLink()" target="_blank">
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
<VIcon icon="mdi-link" />
<span class="ms-1">Bangumi</span>
</div>
</a>
</div> </div>
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4"> <h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">
@@ -740,6 +759,33 @@ onBeforeMount(() => {
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="mediaDetail.bangumi_id" class="media-overview-right">
<div class="media-facts">
<div v-if="mediaDetail.vote_average" class="media-ratings">
<VRating
v-model="mediaDetail.vote_average"
density="compact"
length="10"
class="ma-2"
readonly
/>
</div>
<div v-if="mediaDetail.bangumi_id" class="media-fact">
<span>ID</span>
<span class="media-fact-value">{{ mediaDetail.bangumi_id }}</span>
</div>
<div v-if="mediaDetail.original_title" class="media-fact">
<span>原始标题</span>
<span class="media-fact-value">{{ mediaDetail.original_title }}</span>
</div>
<div v-if="mediaDetail.release_date" class="media-fact border-b-0">
<span>上映日期</span>
<span class="media-fact-value">
{{ mediaDetail.release_date }}
</span>
</div>
</div>
</div>
</div> </div>
<div v-if="mediaDetail.tmdb_id"> <div v-if="mediaDetail.tmdb_id">
<PersonCardSlideView <PersonCardSlideView
@@ -757,6 +803,14 @@ onBeforeMount(() => {
type="douban" type="douban"
/> />
</div> </div>
<div v-else-if="mediaDetail.bangumi_id">
<PersonCardSlideView
:apipath="`bangumi/credits/${mediaDetail.bangumi_id}`"
:linkurl="`/credits/bangumi/credits/${mediaDetail.bangumi_id}?title=演员阵容&type=bangumi`"
title="演员阵容"
type="bangumi"
/>
</div>
<div v-if="mediaDetail.tmdb_id"> <div v-if="mediaDetail.tmdb_id">
<MediaCardSlideView <MediaCardSlideView
:apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`" :apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -771,6 +825,13 @@ onBeforeMount(() => {
title="推荐" title="推荐"
/> />
</div> </div>
<div v-else-if="mediaDetail.bangumi_id">
<MediaCardSlideView
:apipath="`bangumi/recommend/${mediaDetail.bangumi_id}`"
:linkurl="`/browse/bangumi/recommend/${mediaDetail.bangumi_id}?title=推荐`"
title="推荐"
/>
</div>
<div v-if="mediaDetail.tmdb_id"> <div v-if="mediaDetail.tmdb_id">
<MediaCardSlideView <MediaCardSlideView
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`" :apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -781,7 +842,7 @@ onBeforeMount(() => {
</div> </div>
</div> </div>
<NoDataFound <NoDataFound
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed" v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && !mediaDetail.bangumi_id && isRefreshed"
error-code="500" error-code="500"
error-title="出错啦" error-title="出错啦"
error-description="未识别到媒体信息" error-description="未识别到媒体信息"
@@ -3,6 +3,7 @@ import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import api from '@/api' import api from '@/api'
import SlideView from '@/components/slide/SlideView.vue' import SlideView from '@/components/slide/SlideView.vue'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue' import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
import BangumiPersonCard from '@/components/cards/BangumiPersonCard.vue'
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
@@ -59,6 +60,12 @@ onMounted(fetchData)
height="15rem" height="15rem"
width="10rem" width="10rem"
/> />
<BangumiPersonCard
v-if="props.type === 'bangumi'"
:person="data"
height="15rem"
width="10rem"
/>
</template> </template>
</template> </template>
</SlideView> </SlideView>