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

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.7.3",
"version": "1.7.4",
"private": true,
"bin": "dist/service.js",
"scripts": {

View File

@@ -181,6 +181,9 @@ export interface MediaInfo {
// 豆瓣ID
douban_id?: string
// Bangumi ID
bangumi_id?: string
// 媒体原语种
original_language?: string
@@ -282,6 +285,9 @@ export interface MediaInfo {
// 下一集
next_episode_to_air?: object
// 别名
names?: string[]
}
// 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 {

View File

@@ -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>

View File

@@ -57,6 +57,15 @@ const seasonInfos = 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() {
subscribeSeasonDialog.value = false
@@ -131,6 +140,7 @@ async function addSubscribe(season = 0) {
year: props.media?.year,
tmdbid: props.media?.tmdb_id,
doubanid: props.media?.douban_id,
bangumiid: props.media?.bangumi_id,
season,
best_version,
})
@@ -186,9 +196,7 @@ async function removeSubscribe() {
// 开始处理
startNProgress()
try {
const mediaid = props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const mediaid = getMediaId()
const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`,
@@ -249,9 +257,7 @@ async function handleCheckExists() {
// 调用API检查是否已订阅电视剧需要指定季
async function checkSubscribe(season = 0) {
try {
const mediaid = props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
const mediaid = getMediaId()
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
@@ -362,11 +368,7 @@ function goMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: `${
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
mediaid: getMediaId(),
type: props.media?.type,
},
})
@@ -377,11 +379,7 @@ function handleSearch() {
router.push({
path: '/resource',
query: {
keyword: `${
props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`
}`,
keyword: getMediaId(),
type: props.media?.type,
area: 'title',
},

View File

@@ -16,6 +16,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
title="正在热映"
/>
<MediaCardSlideView
apipath="bangumi/calendar"
linkurl="/browse/bangumi/calendar?title=Bangumi每日放送"
title="Bangumi每日放送"
/>
<MediaCardSlideView
apipath="tmdb/movies"
linkurl="/browse/tmdb/movies?title=热门电影"

View File

@@ -75,7 +75,7 @@ async function fetchData() {
else {
startLoadingProgress()
// 优先按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}`, {
params: {
mtype: type,

View File

@@ -44,7 +44,7 @@ onMounted(fetchData)
<template #content>
<template
v-for="data in dataList"
:key="data.tmdb_id || data.douban_id"
:key="data.tmdb_id || data.douban_id || data.bangumi_id"
>
<MediaCard
:media="data"

View File

@@ -51,6 +51,15 @@ const subscribeRules = ref({
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查询详情
async function getMediaDetail() {
if (mediaProps.mediaid && mediaProps.type) {
@@ -60,7 +69,7 @@ async function getMediaDetail() {
},
})
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
// 检查存在状态
@@ -113,7 +122,7 @@ async function checkExists() {
// 查询当前媒体是否已订阅
async function checkSubscribe(season = 0) {
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}`, {
params: {
@@ -198,6 +207,7 @@ async function addSubscribe(season = 0) {
year: mediaDetail.value?.year,
tmdbid: mediaDetail.value?.tmdb_id,
doubanid: mediaDetail.value?.douban_id,
bangumiid: mediaDetail.value?.bangumi_id,
season,
best_version,
})
@@ -253,9 +263,7 @@ async function removeSubscribe(season: number) {
// 开始处理
startNProgress()
try {
const mediaid = mediaDetail.value?.tmdb_id
? `tmdb:${mediaDetail.value?.tmdb_id}`
: `douban:${mediaDetail.value?.douban_id}`
const mediaid = getMediaId()
const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`,
@@ -330,6 +338,11 @@ function getTvdbLink() {
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) {
if (!stillPath)
@@ -405,7 +418,7 @@ function joinArray(arr: 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({
path: '/resource',
query: {
@@ -453,7 +466,7 @@ onBeforeMount(() => {
color="primary"
/>
</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">
<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 />
@@ -492,7 +505,7 @@ onBeforeMount(() => {
</span>
</div>
<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>
<VIcon icon="mdi-magnify" />
</template>
@@ -518,7 +531,7 @@ onBeforeMount(() => {
</VList>
</VMenu>
</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>
<VIcon :icon="getSubscribeIcon" />
</template>
@@ -580,6 +593,12 @@ onBeforeMount(() => {
<span class="ms-1">TheTvDb</span>
</div>
</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>
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">
@@ -740,6 +759,33 @@ onBeforeMount(() => {
</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 v-if="mediaDetail.tmdb_id">
<PersonCardSlideView
@@ -757,6 +803,14 @@ onBeforeMount(() => {
type="douban"
/>
</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">
<MediaCardSlideView
:apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -771,6 +825,13 @@ onBeforeMount(() => {
title="推荐"
/>
</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">
<MediaCardSlideView
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
@@ -781,7 +842,7 @@ onBeforeMount(() => {
</div>
</div>
<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-title="出错啦"
error-description="未识别到媒体信息"

View File

@@ -3,6 +3,7 @@ import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
import api from '@/api'
import SlideView from '@/components/slide/SlideView.vue'
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
import BangumiPersonCard from '@/components/cards/BangumiPersonCard.vue'
// 输入参数
const props = defineProps({
@@ -59,6 +60,12 @@ onMounted(fetchData)
height="15rem"
width="10rem"
/>
<BangumiPersonCard
v-if="props.type === 'bangumi'"
:person="data"
height="15rem"
width="10rem"
/>
</template>
</template>
</SlideView>