From 563560c5e3bd41c961bbe3dbda84d6a51bbf9711 Mon Sep 17 00:00:00 2001 From: StepanovPlaton Date: Sat, 15 Jun 2024 11:53:53 +0400 Subject: [PATCH] Add audiobooks --- database/crud/__init__.py | 2 ++ database/crud/audiobooks.py | 45 +++++++++++++++++++++++++ database/crud/games.py | 1 + database/crud/movies.py | 45 +++++++++++++++++++++++++ database/models/__init__.py | 4 ++- database/models/audiobooks.py | 26 +++++++++++++++ database/models/games.py | 1 - database/models/movies.py | 28 ++++++++++++++++ database/schemas/__init__.py | 2 ++ database/schemas/audiobooks.py | 51 ++++++++++++++++++++++++++++ database/schemas/games.py | 3 +- database/schemas/movies.py | 55 ++++++++++++++++++++++++++++++ file_handler.py | 22 ++++++++++++ main.py | 2 ++ routes/__init__.py | 2 ++ routes/audiobooks.py | 61 ++++++++++++++++++++++++++++++++++ routes/files.py | 13 ++++++-- routes/games.py | 1 - routes/movies.py | 61 ++++++++++++++++++++++++++++++++++ routes/startup.py | 3 +- 20 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 database/crud/audiobooks.py create mode 100644 database/crud/movies.py create mode 100644 database/models/audiobooks.py create mode 100644 database/models/movies.py create mode 100644 database/schemas/audiobooks.py create mode 100644 database/schemas/movies.py create mode 100644 routes/audiobooks.py create mode 100644 routes/movies.py diff --git a/database/crud/__init__.py b/database/crud/__init__.py index ffc3a23..4d11ab3 100644 --- a/database/crud/__init__.py +++ b/database/crud/__init__.py @@ -1,2 +1,4 @@ from .games import * +from .movies import * +from .audiobooks import * from .users import * diff --git a/database/crud/audiobooks.py b/database/crud/audiobooks.py new file mode 100644 index 0000000..0185f16 --- /dev/null +++ b/database/crud/audiobooks.py @@ -0,0 +1,45 @@ +from time import strftime +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from .. import models as mdl +from .. import schemas as sch +from ..database import add_transaction + + +async def get_audiobooks(db: AsyncSession): + return (await db.execute(select(mdl.Audiobook))).scalars().all() + + +async def get_audiobook(db: AsyncSession, audiobook_id: int): + return await db.get(mdl.Audiobook, audiobook_id) + + +async def add_audiobook(db: AsyncSession, + audiobook_info: sch.AudiobookCreate, + user_id: int): + audiobook = mdl.Audiobook(**audiobook_info.model_dump(), + update_date=strftime("%Y-%m-%d %H:%M:%S"), + upload_date=strftime("%Y-%m-%d %H:%M:%S"), + owner_id=user_id) + return await add_transaction(db, audiobook) + + +async def edit_audiobook(db: AsyncSession, + audiobook_id: int, + audiobook_info: sch.AudiobookCreate): + audiobook = await db.get(mdl.Audiobook, audiobook_id) + for key, value in vars(audiobook_info).items(): + if (value and value is not None and getattr(audiobook, key) != value): + setattr(audiobook, key, value) + setattr(audiobook, "update_date", strftime("%Y-%m-%d %H:%M:%S")) + await db.commit() + return audiobook + + +async def delete_audiobook(db: AsyncSession, + audiobook_id: int): + audiobook = await get_audiobook(db, audiobook_id) + await db.delete(audiobook) + await db.commit() + return audiobook diff --git a/database/crud/games.py b/database/crud/games.py index be2568f..f209a03 100644 --- a/database/crud/games.py +++ b/database/crud/games.py @@ -32,6 +32,7 @@ async def edit_game(db: AsyncSession, for key, value in vars(game_info).items(): if (value and value is not None and getattr(game, key) != value): setattr(game, key, value) + setattr(game, "update_date", strftime("%Y-%m-%d %H:%M:%S")) await db.commit() return game diff --git a/database/crud/movies.py b/database/crud/movies.py new file mode 100644 index 0000000..6008d23 --- /dev/null +++ b/database/crud/movies.py @@ -0,0 +1,45 @@ +from time import strftime +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from .. import models as mdl +from .. import schemas as sch +from ..database import add_transaction + + +async def get_movies(db: AsyncSession): + return (await db.execute(select(mdl.Movie))).scalars().all() + + +async def get_movie(db: AsyncSession, movie_id: int): + return await db.get(mdl.Movie, movie_id) + + +async def add_movie(db: AsyncSession, + movie_info: sch.MovieCreate, + user_id: int): + movie = mdl.Movie(**movie_info.model_dump(), + update_date=strftime("%Y-%m-%d %H:%M:%S"), + upload_date=strftime("%Y-%m-%d %H:%M:%S"), + owner_id=user_id) + return await add_transaction(db, movie) + + +async def edit_movie(db: AsyncSession, + movie_id: int, + movie_info: sch.MovieCreate): + movie = await db.get(mdl.Movie, movie_id) + for key, value in vars(movie_info).items(): + if (value and value is not None and getattr(movie, key) != value): + setattr(movie, key, value) + setattr(movie, "update_date", strftime("%Y-%m-%d %H:%M:%S")) + await db.commit() + return movie + + +async def delete_movie(db: AsyncSession, + movie_id: int): + movie = await get_movie(db, movie_id) + await db.delete(movie) + await db.commit() + return movie diff --git a/database/models/__init__.py b/database/models/__init__.py index b435c60..67226bc 100644 --- a/database/models/__init__.py +++ b/database/models/__init__.py @@ -1,2 +1,4 @@ from .games import Game as Game -from .users import User as User \ No newline at end of file +from .movies import Movie as Movie +from .audiobooks import Audiobook as Audiobook +from .users import User as User diff --git a/database/models/audiobooks.py b/database/models/audiobooks.py new file mode 100644 index 0000000..56ff5d8 --- /dev/null +++ b/database/models/audiobooks.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, ForeignKey, Integer, String + +from ..database import Base + + +class Audiobook(Base): + __tablename__ = "audiobooks" + + id = Column(Integer, primary_key=True) + + title = Column(String, nullable=False, unique=True) + cover = Column(String) + description = Column(String) + author = Column(String) + + torrent_file = Column(String, nullable=False) + upload_date = Column(String, nullable=False) + fragment = Column(String) + update_date = Column(String, nullable=False) + language = Column(String) + release_date = Column(String) + download_size = Column(String) + duration = Column(String) + reader = Column(String) + + owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/database/models/games.py b/database/models/games.py index 169b28d..5c766fd 100644 --- a/database/models/games.py +++ b/database/models/games.py @@ -1,5 +1,4 @@ from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship from ..database import Base diff --git a/database/models/movies.py b/database/models/movies.py new file mode 100644 index 0000000..c447d99 --- /dev/null +++ b/database/models/movies.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, ForeignKey, Integer, String + +from ..database import Base + + +class Movie(Base): + __tablename__ = "movies" + + id = Column(Integer, primary_key=True) + + title = Column(String, nullable=False, unique=True) + cover = Column(String) + age = Column(String) + description = Column(String) + + torrent_file = Column(String, nullable=False) + upload_date = Column(String, nullable=False) + trailer = Column(String) + update_date = Column(String, nullable=False) + language = Column(String) + subtitles = Column(String) + release_date = Column(String) + download_size = Column(String) + director = Column(String) + duration = Column(String) + country = Column(String) + + owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/database/schemas/__init__.py b/database/schemas/__init__.py index ffc3a23..4d11ab3 100644 --- a/database/schemas/__init__.py +++ b/database/schemas/__init__.py @@ -1,2 +1,4 @@ from .games import * +from .movies import * +from .audiobooks import * from .users import * diff --git a/database/schemas/audiobooks.py b/database/schemas/audiobooks.py new file mode 100644 index 0000000..eacb896 --- /dev/null +++ b/database/schemas/audiobooks.py @@ -0,0 +1,51 @@ +from typing import Optional +from pydantic import BaseModel, ConfigDict, Field + + +class AudiobookCardBase(BaseModel): + title: str = Field(examples=["Марсианин"]) + cover: Optional[str] = \ + Field(default=None, examples=["cover_filename.jpg"]) + description: Optional[str] = \ + Field(default=None, + examples=["Главный герой оказался в сложнейшей ситуации." + " Его жизнь висела на волоске и зависела от" + " нескольких совершенно нелогичных факторов." + " Дело в том, что его бросили на Марсе в крайне" + " затруднительном для дальнейшей жизни положении." + " Рассчитывать стоит лишь на себя и на чудо," + " ведь ультрасовременный скафандр оказался прошит" + " антенной, а до прибытия следующей экспедиции" + " остается целая вечность."]) + author: Optional[str] = \ + Field(default=None, examples=["Вейр Энди"]) + + +class AudiobookCard(AudiobookCardBase): + id: int = Field(examples=[1]) + + +class AudiobookBase(AudiobookCardBase): + torrent_file: str = Field(examples=["torrent_filename.torrent"]) + fragment: Optional[str] = \ + Field(default=None, examples=[ + "fragment.mp3"]) + + language: Optional[str] = Field(default=None, examples=["рус"]) + release_date: Optional[str] = Field(default=None, examples=["2015"]) + download_size: Optional[str] = Field(default=None, examples=["300Mb"]) + duration: Optional[str] = Field(default=None, examples=["12:38"]) + reader: Optional[str] = Field(default=None, examples=["Дмитрий Хазанович"]) + + +class AudiobookCreate(AudiobookBase): + pass + + +class Audiobook(AudiobookBase): + id: int = Field(examples=[1]) + update_date: str = Field(examples=["2024-06-14 12:00:00"]) + upload_date: str = Field(examples=["2024-06-14 12:00:00"]) + owner_id: int = Field(examples=[1]) + + model_config = ConfigDict(from_attributes=True) diff --git a/database/schemas/games.py b/database/schemas/games.py index d262cc3..206d6b9 100644 --- a/database/schemas/games.py +++ b/database/schemas/games.py @@ -1,10 +1,9 @@ from typing import Optional -from fastapi import Body from pydantic import BaseModel, ConfigDict, Field class GameCardBase(BaseModel): - title: str = Field(examples=["DwarfFortress", "RimWorld"]) + title: str = Field(examples=["DwarfFortress"]) cover: Optional[str] = \ Field(default=None, examples=["cover_filename.jpg"]) description: Optional[str] = \ diff --git a/database/schemas/movies.py b/database/schemas/movies.py new file mode 100644 index 0000000..50d4d09 --- /dev/null +++ b/database/schemas/movies.py @@ -0,0 +1,55 @@ +from typing import Optional +from pydantic import BaseModel, ConfigDict, Field + + +class MovieCardBase(BaseModel): + title: str = Field(examples=["Интерстеллар"]) + cover: Optional[str] = \ + Field(default=None, examples=["cover_filename.jpg"]) + description: Optional[str] = \ + Field(default=None, + examples=["Когда засуха, пыльные бури и вымирание" + " растений приводят человечество к" + " продовольственному кризису, коллектив" + " исследователей и учёных отправляется" + " сквозь червоточину (которая предположительно" + " соединяет области пространства-времени" + " через большое расстояние) в путешествие," + " чтобы превзойти прежние ограничения для" + " космических путешествий человека и найти" + " планету с подходящими для человечества условиями."]) + age: Optional[str] = \ + Field(default=None, examples=["18+"]) + + +class MovieCard(MovieCardBase): + id: int = Field(examples=[1]) + + +class MovieBase(MovieCardBase): + torrent_file: str = Field(examples=["torrent_filename.torrent"]) + trailer: Optional[str] = \ + Field(default=None, examples=[ + "https://www.youtube.com/watch?v=6ybBuTETr3U"]) + + language: Optional[str] = Field(default=None, examples=["рус"]) + subtitles: Optional[str] = Field(default=None, examples=["Отсутствуют"]) + release_date: Optional[str] = Field(default=None, examples=["2014"]) + download_size: Optional[str] = Field(default=None, examples=["32Gb"]) + director: Optional[str] = Field(default=None, examples=["Кристофер Нолан"]) + duration: Optional[str] = Field(default=None, examples=["02:37:58"]) + country: Optional[str] = \ + Field(default=None, examples=["США, Великобритания, Канада"]) + + +class MovieCreate(MovieBase): + pass + + +class Movie(MovieBase): + id: int = Field(examples=[1]) + update_date: str = Field(examples=["2024-06-11 12:00:00"]) + upload_date: str = Field(examples=["2024-06-11 12:00:00"]) + owner_id: int = Field(examples=[1]) + + model_config = ConfigDict(from_attributes=True) diff --git a/file_handler.py b/file_handler.py index 9cb67b9..9e36523 100644 --- a/file_handler.py +++ b/file_handler.py @@ -85,3 +85,25 @@ async def save_image(cover: UploadFile, type: Literal["cover", "screenshot"]): buf, format=cover.content_type.upper().replace("IMAGE/", "")) await preview_file.write(buf.getbuffer()) return hash_filename + + +async def save_audio_fragment(fragment: UploadFile): + if (fragment.filename is None): + raise ValueError("Filename not found") + if (fragment.content_type is None): + raise ValueError("File content type unknown") + + hash_filename = create_hash_name(fragment.filename) + file_extension = mimetypes.guess_extension(fragment.content_type) + if (file_extension is None): + raise NameError("File extension not found") + else: + hash_filename += file_extension + + async with aiofiles.open(Path() / "content" / "audio" + / hash_filename, 'wb') as file: + fragment_data = await fragment.read() + if (isinstance(fragment_data, str)): + raise ValueError("Invalid audio file") + await file.write(fragment_data) + return hash_filename diff --git a/main.py b/main.py index f3c1ddd..2f5b996 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,8 @@ app = FastAPI( ) app.include_router(startup_router) app.include_router(games_router) +app.include_router(movies_router) +app.include_router(audiobooks_router) app.include_router(files_router) app.include_router(auth_router) app.mount("/content", StaticFiles(directory="content"), name="content") diff --git a/routes/__init__.py b/routes/__init__.py index d2331c4..4a8d60e 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -1,4 +1,6 @@ from .games import games_router as games_router +from .movies import movies_router as movies_router +from .audiobooks import audiobooks_router as audiobooks_router from .files import files_router as files_router from .startup import startup_router as startup_router from .auth import auth_router as auth_router diff --git a/routes/audiobooks.py b/routes/audiobooks.py new file mode 100644 index 0000000..d3c716f --- /dev/null +++ b/routes/audiobooks.py @@ -0,0 +1,61 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, Depends, HTTPException, status + +import database as db +from file_handler import * +from routes.auth import get_user + +audiobooks_router = APIRouter(prefix="/audiobooks", tags=["Audiobooks"]) + + +@audiobooks_router.get("", response_model=list[db.Audiobook]) +async def get_audiobooks(db_session: AsyncSession = Depends(db.get_session)): + return await db.get_audiobooks(db_session) + + +@audiobooks_router.post("", response_model=db.Audiobook) +async def add_audiobook(audiobook: db.AudiobookCreate, + user: db.User = Depends(get_user), + db_session: AsyncSession = Depends(db.get_session)): + return await db.add_audiobook(db_session, audiobook, user.id) + + +@audiobooks_router.get("/cards", response_model=list[db.AudiobookCard]) +async def get_audiobooks_cards(db_session: AsyncSession = Depends(db.get_session)): + return await db.get_audiobooks(db_session) + + +@audiobooks_router.get("/{audiobook_id}", response_model=db.Audiobook) +async def get_audiobook(audiobook_id: int, db_session: AsyncSession = Depends(db.get_session)): + return await db.get_audiobook(db_session, audiobook_id) + + +@audiobooks_router.put("/{audiobook_id}", response_model=db.Audiobook) +async def edit_audiobook(audiobook_id: int, + audiobook: db.AudiobookCreate, + user: db.User = Depends(get_user), + db_session: AsyncSession = Depends(db.get_session)): + audiobook_db = await db.get_audiobook(db_session, audiobook_id) + if (audiobook_db is None): + raise HTTPException(status.HTTP_404_NOT_FOUND, + detail=f"Audiobook with id={audiobook_id} not found") + if (user.id != audiobook_db.owner_id): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, + detail=f"Audiobook can only be edited " + "by the owner (creator)") + return await db.edit_audiobook(db_session, audiobook_id, audiobook) + + +@audiobooks_router.delete("/{audiobook_id}", response_model=db.Audiobook) +async def delete_audiobook(audiobook_id: int, + user: db.User = Depends(get_user), + db_session: AsyncSession = Depends(db.get_session)): + audiobook_db = await db.get_audiobook(db_session, audiobook_id) + if (audiobook_db is None): + raise HTTPException(status.HTTP_404_NOT_FOUND, + detail=f"Audiobook with id={audiobook_id} not found") + if (user.id != audiobook_db.owner_id): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, + detail=f"Audiobook can only be deleted " + "by the owner (creator)") + return await db.delete_audiobook(db_session, audiobook_id) diff --git a/routes/files.py b/routes/files.py index 123b849..f879b36 100644 --- a/routes/files.py +++ b/routes/files.py @@ -12,7 +12,7 @@ async def upload_torrent(torrent: UploadFile): return await save_torrent_file(torrent) except Exception as ex: print(ex) - raise HTTPException(500) + raise HTTPException(500, detail=str(ex)) @files_router.post("/cover", response_model=str) @@ -21,4 +21,13 @@ async def upload_cover(cover: UploadFile): return await save_image(cover, "cover") except Exception as ex: print(ex) - raise HTTPException(500) + raise HTTPException(500, detail=str(ex)) + + +@files_router.post("/audio", response_model=str) +async def upload_audio_fragment(fragment: UploadFile): + try: + return await save_audio_fragment(fragment) + except Exception as ex: + print(ex) + raise HTTPException(500, detail=str(ex)) diff --git a/routes/games.py b/routes/games.py index a8beb64..fde1619 100644 --- a/routes/games.py +++ b/routes/games.py @@ -51,7 +51,6 @@ async def delete_game(game_id: int, user: db.User = Depends(get_user), db_session: AsyncSession = Depends(db.get_session)): game_db = await db.get_game(db_session, game_id) - print(game_db) if (game_db is None): raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Game with id={game_id} not found") diff --git a/routes/movies.py b/routes/movies.py new file mode 100644 index 0000000..3909eb4 --- /dev/null +++ b/routes/movies.py @@ -0,0 +1,61 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, Depends, HTTPException, status + +import database as db +from file_handler import * +from routes.auth import get_user + +movies_router = APIRouter(prefix="/movies", tags=["Movies"]) + + +@movies_router.get("", response_model=list[db.Movie]) +async def get_movies(db_session: AsyncSession = Depends(db.get_session)): + return await db.get_movies(db_session) + + +@movies_router.post("", response_model=db.Movie) +async def add_movie(movie: db.MovieCreate, + user: db.User = Depends(get_user), + db_session: AsyncSession = Depends(db.get_session)): + return await db.add_movie(db_session, movie, user.id) + + +@movies_router.get("/cards", response_model=list[db.MovieCard]) +async def get_movies_cards(db_session: AsyncSession = Depends(db.get_session)): + return await db.get_movies(db_session) + + +@movies_router.get("/{movie_id}", response_model=db.Movie) +async def get_movie(movie_id: int, db_session: AsyncSession = Depends(db.get_session)): + return await db.get_movie(db_session, movie_id) + + +@movies_router.put("/{movie_id}", response_model=db.Movie) +async def edit_movie(movie_id: int, + movie: db.MovieCreate, + user: db.User = Depends(get_user), + db_session: AsyncSession = Depends(db.get_session)): + movie_db = await db.get_movie(db_session, movie_id) + if (movie_db is None): + raise HTTPException(status.HTTP_404_NOT_FOUND, + detail=f"Movie with id={movie_id} not found") + if (user.id != movie_db.owner_id): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, + detail=f"Movie can only be edited " + "by the owner (creator)") + return await db.edit_movie(db_session, movie_id, movie) + + +@movies_router.delete("/{movie_id}", response_model=db.Movie) +async def delete_movie(movie_id: int, + user: db.User = Depends(get_user), + db_session: AsyncSession = Depends(db.get_session)): + movie_db = await db.get_movie(db_session, movie_id) + if (movie_db is None): + raise HTTPException(status.HTTP_404_NOT_FOUND, + detail=f"Movie with id={movie_id} not found") + if (user.id != movie_db.owner_id): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, + detail=f"Movie can only be deleted " + "by the owner (creator)") + return await db.delete_movie(db_session, movie_id) diff --git a/routes/startup.py b/routes/startup.py index 19e5838..41ab095 100644 --- a/routes/startup.py +++ b/routes/startup.py @@ -12,7 +12,8 @@ def create_folders(): Path() / "content" / "images" / "cover" / "preview", Path() / "content" / "images" / "screenshot" / "full_size", Path() / "content" / "images" / "screenshot" / "preview", - Path() / "content" / "torrent" + Path() / "content" / "torrent", + Path() / "content" / "audio" ] for path in need_paths: path.mkdir(parents=True, exist_ok=True)