-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a6b79e2
commit 66a1e41
Showing
7 changed files
with
243 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,112 @@ | ||
from typing import Annotated | ||
from typing import Annotated, Optional | ||
from fastapi import APIRouter, Depends | ||
from pydantic import BaseModel | ||
from sqlmodel import Session, select | ||
from sqlalchemy.orm import selectinload | ||
|
||
from db import get_db | ||
from models import AnimeSeries | ||
from models import AnimeSeries, VideoFile | ||
from fastapi import HTTPException | ||
|
||
video_router = APIRouter(prefix="/video", tags=["video"]) | ||
|
||
SessionDep = Annotated[Session, Depends(get_db)] | ||
|
||
|
||
@video_router.get("/anime", response_model=AnimeSeries) | ||
class TagResponse(BaseModel): | ||
id: Optional[int] | ||
name: str | ||
description: Optional[str] | ||
|
||
|
||
class EpisodeResponse(BaseModel): | ||
id: Optional[int] | ||
episode_number: Optional[int] | ||
title: Optional[str] | ||
file: Optional[VideoFile] | ||
description: Optional[str] | ||
subtitles: list[str] | ||
audio_tracks: list[str] | ||
thumbnail: Optional[str] | ||
series_id: Optional[int] | ||
episode_number: Optional[int] | ||
|
||
|
||
class AnimeResponse(BaseModel): | ||
id: Optional[int] | ||
title: str | ||
original_title: Optional[str] | ||
release_date: str | ||
author: Optional[str] | ||
studio: Optional[str] | ||
description: Optional[str] | ||
season_number: Optional[int] | ||
total_episodes: Optional[int] | ||
cover_image: Optional[str] | ||
created_at: Optional[str] | ||
updated_at: Optional[str] | ||
tags: list[TagResponse] = [] | ||
episodes: list[EpisodeResponse] = [] | ||
|
||
class Config: | ||
from_attributes = True | ||
|
||
|
||
@video_router.get("/anime", response_model=list[AnimeSeries]) | ||
async def get_anime(session: SessionDep): | ||
"""獲取動畫列表""" | ||
animes = session.exec(AnimeSeries).all() | ||
statement = select(AnimeSeries) | ||
animes = session.exec(statement).all() | ||
return animes | ||
|
||
|
||
@video_router.get("/anime/{anime_id}", response_model=AnimeSeries) | ||
@video_router.get("/anime/{anime_id}", response_model=AnimeResponse) | ||
async def get_anime_by_id(anime_id: int, session: SessionDep): | ||
"""獲取動畫資訊""" | ||
stmt = ( | ||
select(AnimeSeries) | ||
.where(AnimeSeries.id == anime_id) | ||
.options(selectinload(AnimeSeries.tags), selectinload(AnimeSeries.episodes)) | ||
) | ||
anime = session.exec(stmt).first() | ||
return anime | ||
|
||
result = session.exec(stmt).first() | ||
|
||
tags = [ | ||
TagResponse(id=tag.id, name=tag.name, description=tag.description) | ||
for tag in result.tags | ||
] | ||
|
||
# 手動轉換 Video 到 EpisodeResponse | ||
episodes = [ | ||
EpisodeResponse( | ||
id=ep.id, | ||
episode_number=ep.episode_number, | ||
title=ep.title, | ||
file=ep.file, | ||
description=ep.description, | ||
subtitles=ep.subtitles, | ||
audio_tracks=ep.audio_tracks, | ||
thumbnail=ep.thumbnail, | ||
series_id=ep.series_id, | ||
) | ||
for ep in result.episodes | ||
] | ||
|
||
if not result: | ||
raise HTTPException(status_code=404, detail=f"找不到 ID 為 {anime_id} 的動畫") | ||
return AnimeResponse( | ||
id=result.id, | ||
title=result.title, | ||
original_title=result.original_title, | ||
release_date=result.release_date, | ||
author=result.author, | ||
studio=result.studio, | ||
description=result.description, | ||
season_number=result.season_number, | ||
total_episodes=result.total_episodes, | ||
cover_image=result.cover_image, | ||
created_at=str(result.created_at) if result.created_at else None, | ||
updated_at=str(result.updated_at) if result.updated_at else None, | ||
tags=tags, | ||
episodes=episodes, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<script lang="ts"> | ||
import type { PageData } from './$types' | ||
import { APIUrl } from '$lib/api' | ||
let { data }: { data: PageData } = $props() | ||
function getCoverUrl(coverId: string | null) { | ||
if (!coverId) return '/placeholder.png' | ||
return `${APIUrl}/file/image?image_id=${coverId}&image_size=300` | ||
} | ||
</script> | ||
|
||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"> | ||
{#each data.animes as anime} | ||
<div class="card bg-base-200 shadow-xl transition-transform hover:scale-105"> | ||
<figure class="h-48"> | ||
{#if anime.cover_image} | ||
<img | ||
src={getCoverUrl(anime.cover_image)} | ||
alt={anime.title} | ||
class="h-full w-full object-cover" | ||
/> | ||
{/if} | ||
</figure> | ||
<div class="card-body"> | ||
<h2 class="card-title text-lg">{anime.title}</h2> | ||
<p class="line-clamp-2 text-sm opacity-75">{anime.description}</p> | ||
</div> | ||
</div> | ||
{/each} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { APIUrl } from '$lib/api'; | ||
import type { PageLoad } from './$types'; | ||
|
||
export const load = (async ({ fetch }) => { | ||
const response = await fetch(`${APIUrl}/video/anime`); | ||
const animes = await response.json(); | ||
return { | ||
animes | ||
}; | ||
}) satisfies PageLoad; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<script lang="ts"> | ||
import type { PageData } from './$types' | ||
let { data }: { data: PageData } = $props() | ||
import { APIUrl } from '$lib/api' | ||
const { animes } = data | ||
function getCoverUrl(coverId: string | null) { | ||
if (!coverId) return '/placeholder.png' | ||
return `${APIUrl}/file/image?image_id=${coverId}&image_size=500` | ||
} | ||
</script> | ||
|
||
<div class="container mx-auto px-4 py-8"> | ||
<div class="flex flex-col gap-8 md:flex-row"> | ||
<div class="w-full md:w-1/4"> | ||
{#if animes.cover_image} | ||
<img src={animes.cover_image} alt={animes.title} class="w-full rounded-lg shadow-lg" /> | ||
{:else} | ||
<div class="flex aspect-[3/4] w-full items-center justify-center rounded-lg bg-gray-200"> | ||
<span class="text-gray-400">無封面</span> | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
<div class="flex-1"> | ||
<h1 class="mb-4 text-3xl font-bold">{animes.title}</h1> | ||
{#if animes.original_title} | ||
<h2 class="mb-4 text-xl text-gray-600">{animes.original_title}</h2> | ||
{/if} | ||
|
||
<div class="mb-4"> | ||
<p class="text-gray-700">{animes.description || '暫無簡介'}</p> | ||
</div> | ||
|
||
<div class="mb-4 grid grid-cols-2 gap-4"> | ||
<div> | ||
<span class="font-semibold">放送日期:</span> | ||
<span>{animes.release_date}</span> | ||
</div> | ||
{#if animes.studio} | ||
<div> | ||
<span class="font-semibold">製作公司:</span> | ||
<span>{animes.studio}</span> | ||
</div> | ||
{/if} | ||
{#if animes.author} | ||
<div> | ||
<span class="font-semibold">原作:</span> | ||
<span>{animes.author}</span> | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
|
||
<div class="mb-4 flex flex-wrap gap-2"> | ||
{#each animes.tags as tag} | ||
<span class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"> | ||
{tag.name} | ||
</span> | ||
{/each} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
|
||
<div class="mt-8"> | ||
<h2 class="mb-4 text-2xl font-bold">劇集列表</h2> | ||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4 lg:grid-cols-6"> | ||
{#each animes.episodes as episode} | ||
<div class="overflow-hidden rounded-lg bg-white shadow"> | ||
{#if episode.thumbnail} | ||
<img | ||
src={getCoverUrl(episode.thumbnail)} | ||
alt={episode.title} | ||
class="aspect-video w-full object-cover" | ||
/> | ||
{:else} | ||
<div class="aspect-video w-full bg-gray-200"></div> | ||
{/if} | ||
<div class="p-4"> | ||
<h3 class="font-semibold">第 {episode.episode_number} 集</h3> | ||
{#if episode.title} | ||
<p class="text-sm text-gray-600">{episode.title}</p> | ||
{/if} | ||
</div> | ||
</div> | ||
{/each} | ||
</div> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { APIUrl } from '$lib/api' | ||
import type { PageLoad } from './$types' | ||
|
||
export const load = (async ({ fetch, params }) => { | ||
const response = await fetch(`${APIUrl}/video/anime/${params.animeid}`) | ||
const animes = await response.json() | ||
return { | ||
animes | ||
} | ||
}) satisfies PageLoad |