Add audiobooks

This commit is contained in:
2024-06-15 11:53:53 +04:00
parent fe424182df
commit 563560c5e3
20 changed files with 420 additions and 8 deletions

View File

@@ -1,2 +1,4 @@
from .games import * from .games import *
from .movies import *
from .audiobooks import *
from .users import * from .users import *

View File

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

View File

@@ -32,6 +32,7 @@ async def edit_game(db: AsyncSession,
for key, value in vars(game_info).items(): for key, value in vars(game_info).items():
if (value and value is not None and getattr(game, key) != value): if (value and value is not None and getattr(game, key) != value):
setattr(game, key, value) setattr(game, key, value)
setattr(game, "update_date", strftime("%Y-%m-%d %H:%M:%S"))
await db.commit() await db.commit()
return game return game

45
database/crud/movies.py Normal file
View File

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

View File

@@ -1,2 +1,4 @@
from .games import Game as Game from .games import Game as Game
from .movies import Movie as Movie
from .audiobooks import Audiobook as Audiobook
from .users import User as User from .users import User as User

View File

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

View File

@@ -1,5 +1,4 @@
from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from ..database import Base from ..database import Base

28
database/models/movies.py Normal file
View File

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

View File

@@ -1,2 +1,4 @@
from .games import * from .games import *
from .movies import *
from .audiobooks import *
from .users import * from .users import *

View File

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

View File

@@ -1,10 +1,9 @@
from typing import Optional from typing import Optional
from fastapi import Body
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
class GameCardBase(BaseModel): class GameCardBase(BaseModel):
title: str = Field(examples=["DwarfFortress", "RimWorld"]) title: str = Field(examples=["DwarfFortress"])
cover: Optional[str] = \ cover: Optional[str] = \
Field(default=None, examples=["cover_filename.jpg"]) Field(default=None, examples=["cover_filename.jpg"])
description: Optional[str] = \ description: Optional[str] = \

View File

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

View File

@@ -85,3 +85,25 @@ async def save_image(cover: UploadFile, type: Literal["cover", "screenshot"]):
buf, format=cover.content_type.upper().replace("IMAGE/", "")) buf, format=cover.content_type.upper().replace("IMAGE/", ""))
await preview_file.write(buf.getbuffer()) await preview_file.write(buf.getbuffer())
return hash_filename 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

View File

@@ -15,6 +15,8 @@ app = FastAPI(
) )
app.include_router(startup_router) app.include_router(startup_router)
app.include_router(games_router) app.include_router(games_router)
app.include_router(movies_router)
app.include_router(audiobooks_router)
app.include_router(files_router) app.include_router(files_router)
app.include_router(auth_router) app.include_router(auth_router)
app.mount("/content", StaticFiles(directory="content"), name="content") app.mount("/content", StaticFiles(directory="content"), name="content")

View File

@@ -1,4 +1,6 @@
from .games import games_router as games_router 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 .files import files_router as files_router
from .startup import startup_router as startup_router from .startup import startup_router as startup_router
from .auth import auth_router as auth_router from .auth import auth_router as auth_router

61
routes/audiobooks.py Normal file
View File

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

View File

@@ -12,7 +12,7 @@ async def upload_torrent(torrent: UploadFile):
return await save_torrent_file(torrent) return await save_torrent_file(torrent)
except Exception as ex: except Exception as ex:
print(ex) print(ex)
raise HTTPException(500) raise HTTPException(500, detail=str(ex))
@files_router.post("/cover", response_model=str) @files_router.post("/cover", response_model=str)
@@ -21,4 +21,13 @@ async def upload_cover(cover: UploadFile):
return await save_image(cover, "cover") return await save_image(cover, "cover")
except Exception as ex: except Exception as ex:
print(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))

View File

@@ -51,7 +51,6 @@ async def delete_game(game_id: int,
user: db.User = Depends(get_user), user: db.User = Depends(get_user),
db_session: AsyncSession = Depends(db.get_session)): db_session: AsyncSession = Depends(db.get_session)):
game_db = await db.get_game(db_session, game_id) game_db = await db.get_game(db_session, game_id)
print(game_db)
if (game_db is None): if (game_db is None):
raise HTTPException(status.HTTP_404_NOT_FOUND, raise HTTPException(status.HTTP_404_NOT_FOUND,
detail=f"Game with id={game_id} not found") detail=f"Game with id={game_id} not found")

61
routes/movies.py Normal file
View File

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

View File

@@ -12,7 +12,8 @@ def create_folders():
Path() / "content" / "images" / "cover" / "preview", Path() / "content" / "images" / "cover" / "preview",
Path() / "content" / "images" / "screenshot" / "full_size", Path() / "content" / "images" / "screenshot" / "full_size",
Path() / "content" / "images" / "screenshot" / "preview", Path() / "content" / "images" / "screenshot" / "preview",
Path() / "content" / "torrent" Path() / "content" / "torrent",
Path() / "content" / "audio"
] ]
for path in need_paths: for path in need_paths:
path.mkdir(parents=True, exist_ok=True) path.mkdir(parents=True, exist_ok=True)