Skip to content

Commit

Permalink
feat: anime not finish
Browse files Browse the repository at this point in the history
  • Loading branch information
phillychi3 committed Jan 3, 2025
1 parent a6b79e2 commit 66a1e41
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 16 deletions.
21 changes: 12 additions & 9 deletions backend/src/core/syncfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
AnimeSeries,
AnimeTag,
)
from core.logger import logging
from core.logger import logger


def sync_text_file(metadata: dict, db: Session):
Expand Down Expand Up @@ -43,7 +43,8 @@ def sync_text_file(metadata: dict, db: Session):
def sync_video_file(metadata: dict, db: Session):
"""同步影片檔案到資料庫"""
try:
logging.debug(f"Syncing video file: {metadata}")
logger.debug(f"Syncing video file: {metadata}")
logger.info(f"Syncing video file: {metadata}")
video_file = VideoFile(
filename=metadata.get("filename"),
filepath=metadata.get("file_path"),
Expand All @@ -56,13 +57,13 @@ def sync_video_file(metadata: dict, db: Session):
)

anime = None
if metadata.get("isanime") and metadata.get("anime"):
if metadata.get("isanime"):
anime = db.exec(
select(AnimeSeries).where(AnimeSeries.title == metadata["anime"])
select(AnimeSeries).where(AnimeSeries.title == metadata["title"])
).first()
if not anime:
anime = AnimeSeries(
title=metadata["anime"],
title=metadata["title"],
description=metadata.get("description"),
season_number=metadata.get("season_number", 1),
release_date=metadata.get("date"),
Expand All @@ -77,7 +78,9 @@ def sync_video_file(metadata: dict, db: Session):
anime.tags.append(anime_tag)

video = Video(
title=metadata.get("title") or metadata["filename"],
title=metadata.get("title") or metadata["filename"]
if metadata.get("isanime")
else metadata.get("titme") + " - " + metadata.get("season_number", 1),
duration=metadata.get("duration", 0),
description=metadata.get("description"),
subtitles=metadata.get("subtitles", []),
Expand Down Expand Up @@ -189,12 +192,12 @@ def sync_dir_file(dir_path: Path) -> list:
for file in files:
file_path = Path(root) / file
if file_path in exist_files:
logging.debug(f"File {file_path} already exists")
logger.debug(f"File {file_path} already exists")
continue
try:
files.append(FileParser().parse_file(file_path))
except Exception as e:
logging.error(f"Error parsing file {file_path} : {str(e)}")
logger.error(f"Error parsing file {file_path} : {str(e)}")
pass
for file in files:
if file.get("file_type") == FileType.MUSIC:
Expand All @@ -218,7 +221,7 @@ def sync_one_file(file_path: Path):
try:
file = FileParser().parse_file(file_path)
except Exception as e:
logging.error(f"Error parsing file {file_path}: {str(e)}")
logger.error(f"Error parsing file {file_path}: {str(e)}")
return
if file.get("file_type") == FileType.MUSIC:
sync_music_file(metadata=file, db=db)
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from routers.user import user_router
from routers.setting import setting_router
from routers.playlist import playlist_router
from routers.video import video_router
from core.logger import logger
from starlette.middleware.cors import CORSMiddleware

Expand Down Expand Up @@ -41,6 +42,7 @@ def ping():
app.include_router(user_router)
app.include_router(setting_router)
app.include_router(playlist_router)
app.include_router(video_router)
logger.info("Server started")

cors = [
Expand Down
96 changes: 89 additions & 7 deletions backend/src/routers/video.py
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,
)
31 changes: 31 additions & 0 deletions frontend/web/src/routes/app/anime/+page.svelte
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>
10 changes: 10 additions & 0 deletions frontend/web/src/routes/app/anime/+page.ts
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;
89 changes: 89 additions & 0 deletions frontend/web/src/routes/app/anime/[animeid]/+page.svelte
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>
10 changes: 10 additions & 0 deletions frontend/web/src/routes/app/anime/[animeid]/+page.ts
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

0 comments on commit 66a1e41

Please sign in to comment.