Vorbereitung Release 0.2.0
This commit is contained in:
@@ -4,6 +4,8 @@ from src.apis.version1 import comic, media, tysc, admin
|
||||
|
||||
api_router = APIRouter(prefix="/api")
|
||||
api_router.include_router(comic.router, prefix="/comics", tags=["comics"])
|
||||
api_router.include_router(media.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(mediafile.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(mediaactor.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(mediaactorfile.router, prefix="/media", tags=["media"])
|
||||
api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"])
|
||||
api_router.include_router(admin.router, prefix="/login", tags=["login"])
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
from typing import Annotated
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import status
|
||||
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.security import OAuth2
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.db.session import get_db
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||
def __init__(
|
||||
self,
|
||||
tokenUrl: str,
|
||||
scheme_name: Optional[str] = None,
|
||||
scopes: Optional[Dict[str, str]] = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.cookies.get(
|
||||
"access_token"
|
||||
) # changed to accept access token from httpOnly Cookie
|
||||
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
@@ -21,22 +21,27 @@ def get_all_comics(db: SessionDep) -> List[ComicResponse]:
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
|
||||
def get_comic(comic_id: AnyStr, db: SessionDep) -> ComicDetailsResponse:
|
||||
comic = db.get(Comic, comic_id)
|
||||
if comic is None:
|
||||
raise HTTPException(status_code=404, detail="Comic could not be found")
|
||||
logger.info(f"create ComicDetailsResponse for {comic}")
|
||||
response: ComicDetailsResponse = get_comic_details(comic)
|
||||
logger.info(f"ComicDetailsResponse: {response}")
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/artists", response_model=List[ArtistResponse])
|
||||
def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
|
||||
results: List[ArtistResponse] = []
|
||||
artists = db.query(Artist).all()
|
||||
for artist in artists:
|
||||
results.append(ArtistResponse(id=artist.id, name=artist.name))
|
||||
results.append(ArtistResponse(id=artist.id, name=str(artist.name))) # type: ignore
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
|
||||
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
|
||||
artist = db.get(Artist, artist_id)
|
||||
@@ -45,6 +50,7 @@ def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse:
|
||||
response: ArtistDetailResponse = get_artist_details(artist)
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/artists", status_code=status.HTTP_201_CREATED)
|
||||
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse:
|
||||
artist: Artist = Artist()
|
||||
@@ -54,7 +60,7 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
|
||||
db.commit()
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Artist already added")
|
||||
response = ArtistResponse(id=artist.id, name=artist.name)
|
||||
response = ArtistResponse(id=artist.id, name=str(artist.name))
|
||||
return response
|
||||
|
||||
@router.get("/issues", response_model=List[IssueDetailsResponse])
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from src.schema.admin import HealthCheck
|
||||
|
||||
health_router = APIRouter()
|
||||
|
||||
|
||||
@health_router.get(
|
||||
"/health",
|
||||
tags=["healthcheck"],
|
||||
summary="Perform a health check",
|
||||
response_description="Return HTTP status code 200 (OK)",
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
def health_check() -> HealthCheck:
|
||||
"""
|
||||
## Perform a health check
|
||||
Endpoint to perform a healthcheck on. This endpoint can primarily be used Docker
|
||||
to ensure a robust container orchestration and management is in place. Other
|
||||
services which rely on proper functioning of the API service will not deploy if this
|
||||
endpoint returns any other HTTP status code except 200 (OK).
|
||||
:return:
|
||||
HealthCheck: Returns a JSON response with the health status
|
||||
"""
|
||||
return HealthCheck(status="ok")
|
||||
@@ -0,0 +1,41 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.log_conf import logger
|
||||
from src.core.security import authenticate_user, create_access_token
|
||||
from src.schema.admin import Token
|
||||
|
||||
login_router = APIRouter()
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: str | None = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
@login_router.post(
|
||||
"/login",
|
||||
tags=["login"],
|
||||
summary="Login and get token",
|
||||
response_description="Return HTTP status code 200 (OK)",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def login(request: LoginRequest) -> Token:
|
||||
logger.info(f"login with {request.email}")
|
||||
user = authenticate_user(request.email, request.password) # type: ignore
|
||||
scopes = ["admin", "read"]
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email, "scope": " ".join(scopes)},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
@@ -1,69 +0,0 @@
|
||||
from typing import List, AnyStr
|
||||
|
||||
from fastapi import APIRouter, status, HTTPException, Depends
|
||||
from sqlalchemy import select, Sequence
|
||||
from src.core.log_conf import logger
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.repository.media import create_new_mediafile
|
||||
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
|
||||
from src.db.models.media import MediaFile
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/update-titles")
|
||||
def update_titles(db: SessionDep) -> list[MediaFileResponse]:
|
||||
results: list[MediaFileResponse] = []
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||
for mediafile in files:
|
||||
mediafile.update_title()
|
||||
db.add(mediafile)
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
db.commit()
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/files", response_model=List[MediaFileResponse])
|
||||
#def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]:
|
||||
results: list[MediaFileResponse] = []
|
||||
files: Sequence[MediaFile]
|
||||
if review:
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||
elif download:
|
||||
files = db.query(MediaFile).filter(MediaFile.should_download == True).all()
|
||||
else:
|
||||
files = db.scalars(select(MediaFile)).all()
|
||||
for mediafile in files:
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def get_file(file_id: AnyStr, db: SessionDep) -> MediaFileResponse:
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
response = get_file_details(mediafile)
|
||||
return response
|
||||
|
||||
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def update_file(file_id: AnyStr, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse:
|
||||
mediaFile = db.get(MediaFile, file_id)
|
||||
if not mediaFile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
set_file(info, mediaFile)
|
||||
db.add(mediaFile)
|
||||
db.commit()
|
||||
return info
|
||||
|
||||
|
||||
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
||||
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse:
|
||||
logger.info(f"add url {new_link.url}")
|
||||
try:
|
||||
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_file_details(mediaFile)
|
||||
return response
|
||||
@@ -0,0 +1,48 @@
|
||||
from fastapi import APIRouter, status, HTTPException
|
||||
from sqlalchemy import select
|
||||
from src.core.log_conf import logger
|
||||
from src.db.repository.media import create_new_mediaactor, delete_mediaactor
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.media.actor import Actor, MediaActorResponse, get_actor_details
|
||||
from src.db.models.media import MediaActor
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/actors", response_model=list[MediaActorResponse])
|
||||
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||
def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]: # type: ignore
|
||||
results: list[MediaActorResponse] = []
|
||||
actors = db.scalars(select(MediaActor)).all()
|
||||
for mediaactor in actors:
|
||||
response = MediaActorResponse(id=mediaactor.id, name=str(mediaactor.name), url=str(mediaactor.url))
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/actors/{actor_id}", response_model=MediaActorResponse)
|
||||
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse: # type: ignore
|
||||
media_actor = db.get(MediaActor, actor_id)
|
||||
if not media_actor:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
response = get_actor_details(media_actor)
|
||||
return response
|
||||
|
||||
@router.delete("/actors/{actor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_actor(actor_id: str, db: SessionDep): # type: ignore
|
||||
media_actor = db.get(MediaActor, actor_id)
|
||||
if not media_actor:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
logger.info(f"delete MediaActor: {actor_id}")
|
||||
actor_files = media_actor.media_actor_files
|
||||
logger.info(f"MediaActorFiles links {len(actor_files)}")
|
||||
if len(actor_files) == 0:
|
||||
delete_mediaactor(db, media_actor.id)
|
||||
|
||||
@router.post("/actors", status_code=status.HTTP_201_CREATED)
|
||||
def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse: # type: ignore
|
||||
logger.info(f"add actor {new_actor.url}")
|
||||
try:
|
||||
mediaActor: MediaActor = create_new_mediaactor(new_actor, db)
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_actor_details(mediaActor)
|
||||
return response
|
||||
@@ -0,0 +1,33 @@
|
||||
from fastapi import APIRouter, status, HTTPException
|
||||
from sqlalchemy import select
|
||||
from src.db.models.media import MediaActorFile
|
||||
from src.db.repository.media import delete_mediaactorfile
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.media.actorfile import MediaActorFileResponse, get_actorfile_details
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/actorfiles", response_model=list[MediaActorFileResponse])
|
||||
def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]: # type: ignore
|
||||
results: list[MediaActorFileResponse] = []
|
||||
actorfiles = db.scalars(select(MediaActorFile)).all()
|
||||
for mediaactorfile in actorfiles:
|
||||
response = MediaActorFileResponse(id=mediaactorfile.id, actor_id=str(mediaactorfile.media_actor_id), file_id=str(mediaactorfile.media_file_id))
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/actorfiles/{actorfile_id}", response_model=MediaActorFileResponse)
|
||||
def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse: # type: ignore
|
||||
media_actorfile = db.get(MediaActorFile, actorfile_id)
|
||||
if not media_actorfile:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
response = get_actorfile_details(media_actorfile)
|
||||
return response
|
||||
|
||||
@router.delete("/actorfiles/{actorfile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_actorfile(actorfile_id: str, db: SessionDep): # type: ignore
|
||||
media_actorfile = db.get(MediaActorFile, actorfile_id)
|
||||
if not media_actorfile:
|
||||
raise HTTPException(status_code=404, detail="MediaActor could not be found")
|
||||
delete_mediaactorfile(db, media_actorfile.id)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from fastapi import APIRouter, status, HTTPException, Depends
|
||||
from sqlalchemy import select, Sequence
|
||||
from src.core.log_conf import logger
|
||||
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile, delete_mediafile
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.media.actor import MediaActorResponse
|
||||
from src.schema.media.actorfile import MediaActorFileResponse
|
||||
from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file
|
||||
from src.db.models.media import MediaFile
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/update-titles")
|
||||
def update_titles(db: SessionDep) -> list[MediaFileResponse]: # type: ignore
|
||||
results: list[MediaFileResponse] = []
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all()
|
||||
for mediafile in files:
|
||||
mediafile.update_title()
|
||||
db.add(mediafile)
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
db.commit()
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/files", response_model=list[MediaFileResponse])
|
||||
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]:
|
||||
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaFileResponse]: # type: ignore
|
||||
results: list[MediaFileResponse] = []
|
||||
files: Sequence[MediaFile]
|
||||
if review:
|
||||
files = db.query(MediaFile).filter(MediaFile.review == True).all() # type: ignore
|
||||
elif download:
|
||||
files = db.query(MediaFile).filter(MediaFile.should_download == True).all() # type: ignore
|
||||
else:
|
||||
files = db.scalars(select(MediaFile)).all() # type: ignore
|
||||
for mediafile in files: # type: ignore
|
||||
response = get_file_details(mediafile)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.get("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse: # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
response = get_file_details(mediafile)
|
||||
return response
|
||||
|
||||
@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_file(file_id: str, db: SessionDep): # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
logger.info(f"delete MediaFile: {file_id}")
|
||||
actor_files = mediafile.media_actor_files
|
||||
logger.info(f"MediaActorFiles links {len(actor_files)}")
|
||||
if len(actor_files) == 0:
|
||||
delete_mediafile(db, mediafile.id)
|
||||
|
||||
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
|
||||
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]: # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
actor_files = mediafile.media_actor_files
|
||||
logger.info(f"already known actors: {actor_files}")
|
||||
results: list[MediaActorResponse] = []
|
||||
for actor_file in actor_files:
|
||||
response = MediaActorResponse(id=actor_file.media_actor.id, name=actor_file.media_actor.name, url=actor_file.media_actor.url)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.put("/files/{file_id}/actors", response_model=list[MediaActorFileResponse])
|
||||
def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResponse]) -> list[MediaActorFileResponse]: # type: ignore
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
actor_files = mediafile.media_actor_files
|
||||
logger.info(f"already known actors: {actor_files}")
|
||||
for actor in actors:
|
||||
already_associated = False
|
||||
for actor_file in actor_files:
|
||||
if actor.id == actor_file.media_actor_id:
|
||||
logger.info("alreay associated - do nothing")
|
||||
already_associated = True
|
||||
break
|
||||
if not already_associated:
|
||||
create_new_mediaactorfile(db, actor.id, mediafile.id)
|
||||
db.refresh(mediafile)
|
||||
actor_files = mediafile.media_actor_files
|
||||
results: list[MediaActorFileResponse] = []
|
||||
for actor_file in actor_files:
|
||||
response = MediaActorFileResponse(id=actor_file.id, actor_id=actor_file.media_actor_id, file_id=actor_file.media_file_id)
|
||||
results.append(response)
|
||||
return results
|
||||
|
||||
@router.put("/files/{file_id}", response_model=MediaFileResponse)
|
||||
def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse: # type: ignore
|
||||
mediaFile = db.get(MediaFile, file_id)
|
||||
if not mediaFile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be found")
|
||||
set_file(info, mediaFile)
|
||||
db.add(mediaFile)
|
||||
db.commit()
|
||||
mediafile = db.get(MediaFile, file_id)
|
||||
if not mediafile:
|
||||
raise HTTPException(status_code=404, detail="MediaFile could not be updated")
|
||||
response = get_file_details(mediafile)
|
||||
return response
|
||||
|
||||
@router.post("/files", status_code=status.HTTP_201_CREATED)
|
||||
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: # type: ignore
|
||||
logger.info(f"add url {new_link.url}")
|
||||
try:
|
||||
mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
|
||||
except:
|
||||
raise HTTPException(status_code=409, detail="Link duplicate")
|
||||
response = get_file_details(mediaFile)
|
||||
return response
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.apis.utils import SessionDep
|
||||
from src.db.session import SessionDep
|
||||
from src.schema.tysc.sport import SportResponse
|
||||
from src.db.models.tysc import Sport
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
def update_title(self) -> None:
|
||||
logging.info(f"update title for {self.url}")
|
||||
try:
|
||||
r = requests.get(self.url)
|
||||
r = requests.get(str(self.url))
|
||||
soup = BeautifulSoup(r.content, "html.parser")
|
||||
title = soup.title.string
|
||||
title = soup.title.get_text() # type: ignore
|
||||
self.title = title
|
||||
self.review = False
|
||||
except:
|
||||
@@ -37,7 +37,7 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin):
|
||||
|
||||
def download_file(self, download_dir: str, dl_tool: str):
|
||||
logging.info(f"download file for {self.url} to {download_dir}")
|
||||
result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True)
|
||||
result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) # type: ignore
|
||||
if result.returncode == 0:
|
||||
output = result.stdout
|
||||
output = re.sub(' +', ' ', output)
|
||||
@@ -72,6 +72,12 @@ class MediaActor(Base, BaseMixin):
|
||||
__tablename__ = 'media_actor'
|
||||
name = Column(String)
|
||||
media_actor_files = relationship("MediaActorFile")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'MediaActor({self.id} {self.name} {self.url})'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.url}({self.id})'
|
||||
|
||||
|
||||
class MediaActorFile(Base, BaseMixin):
|
||||
@@ -81,6 +87,11 @@ class MediaActorFile(Base, BaseMixin):
|
||||
media_file_id = Column(String, ForeignKey("media_file.id"), nullable=True)
|
||||
media_file = relationship("MediaFile", back_populates="media_actor_files")
|
||||
|
||||
def __repr__(self):
|
||||
return f'MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.id} {self.media_actor_id} {self.media_file_id}'
|
||||
|
||||
class MediaArticle(Base, BaseMixin):
|
||||
__tablename__ = 'media_article'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import List
|
||||
|
||||
from src.db.models.comic import Publisher
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
from src.schema.comics.publisher_details import PublisherDetailsResponse
|
||||
|
||||
|
||||
def get_publisher_details(publisher: Publisher) -> PublisherDetailsResponse:
|
||||
imprints: List[PublisherResponse] = []
|
||||
for imprint in publisher.imprints:
|
||||
imprints.append(PublisherResponse(id=imprint.id, name=str(imprint.name)))
|
||||
comics: List[ComicResponse] = []
|
||||
for comic in publisher.comics:
|
||||
comics.append(
|
||||
ComicResponse(id=comic.id, title=comic.title, completed=comic.completed)
|
||||
)
|
||||
parent_publisher: PublisherResponse | None = None
|
||||
if publisher.parent_publisher:
|
||||
parent_publisher = PublisherResponse(id=publisher.parent_publisher.id, name=str(publisher.parent_publisher.name))
|
||||
response: PublisherDetailsResponse = PublisherDetailsResponse(
|
||||
id=publisher.id, name=str(publisher.name), parent_publisher=parent_publisher, imprints=imprints, comics=comics
|
||||
)
|
||||
return response
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Generator
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
@@ -10,6 +11,9 @@ engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
||||
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
|
||||
def get_db() -> Generator:
|
||||
with SessionLocal() as db:
|
||||
yield db
|
||||
|
||||
SessionDep: type[Session] = Annotated[Session, Depends(get_db)]
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging.config
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from src.apis.base import api_router
|
||||
@@ -11,7 +12,19 @@ from src.db.session import engine
|
||||
from src.db.utils import check_db_connected, check_db_disconnected
|
||||
from src.webapps.base import api_router as web_app_router
|
||||
from src.core.config import settings
|
||||
from src.core.log_conf import logger
|
||||
from src.db.models.base import Base
|
||||
from src.db.session import engine
|
||||
from src.db.utils import check_db_connected, check_db_disconnected
|
||||
from src.webapps.base import api_router as web_app_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await check_db_connected()
|
||||
yield
|
||||
await check_db_disconnected()
|
||||
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -23,10 +36,24 @@ async def lifespan(app: FastAPI):
|
||||
def include_router(app: FastAPI):
|
||||
app.include_router(api_router)
|
||||
app.include_router(web_app_router)
|
||||
app.include_router(health_router)
|
||||
app.include_router(login_router)
|
||||
|
||||
|
||||
def configure_static(app: FastAPI):
|
||||
app.mount("/static", StaticFiles(directory="src/static"), name="static")
|
||||
|
||||
|
||||
def add_middle_ware(app: FastAPI):
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -35,6 +62,7 @@ def start_application(log):
|
||||
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan)
|
||||
include_router(app)
|
||||
configure_static(app)
|
||||
add_middle_ware(app)
|
||||
create_tables()
|
||||
return app
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.issue_details import IssueDetailsResponse
|
||||
from src.schema.comics.worktype import WorktypeResponse
|
||||
|
||||
|
||||
class ArtistWorktypeComicResponse(BaseModel):
|
||||
worktype: WorktypeResponse
|
||||
comics: List[ComicResponse]
|
||||
|
||||
class ArtistWorktypeIssueResponse(BaseModel):
|
||||
worktype: WorktypeResponse
|
||||
issues: List[IssueDetailsResponse]
|
||||
|
||||
class ArtistDetailResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
weblink: str
|
||||
comic_works: List[ArtistWorktypeComicResponse]
|
||||
issue_works: List[ArtistWorktypeIssueResponse]
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.artist import ArtistResponse
|
||||
from src.schema.comics.issue import IssueResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
from src.schema.comics.volume import VolumeResponse
|
||||
from src.schema.comics.worktype import WorktypeResponse
|
||||
|
||||
|
||||
class ComicWorktypeArtistResponse(BaseModel):
|
||||
worktype: WorktypeResponse
|
||||
artists: List[ArtistResponse]
|
||||
|
||||
|
||||
class ComicDetailsResponse(BaseModel):
|
||||
id: str
|
||||
created: str
|
||||
title: str
|
||||
completed : bool
|
||||
current_order : bool
|
||||
weblink: str
|
||||
publisher: PublisherResponse
|
||||
issues: List[IssueResponse]
|
||||
volumes: List[VolumeResponse]
|
||||
works: List[ComicWorktypeArtistResponse]
|
||||
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.volume import VolumeResponse
|
||||
|
||||
|
||||
class IssueDetailsResponse(BaseModel):
|
||||
id: str
|
||||
issue_number: str
|
||||
in_stock: bool
|
||||
is_read: bool
|
||||
comic: ComicResponse
|
||||
volume: VolumeResponse | None
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PublisherResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -0,0 +1,14 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.schema.comics.comic import ComicResponse
|
||||
from src.schema.comics.publisher import PublisherResponse
|
||||
|
||||
|
||||
class PublisherDetailsResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
parent_publisher: PublisherResponse | None
|
||||
imprints: List[PublisherResponse]
|
||||
comics: List[ComicResponse]
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class VolumeResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from src.db.models.media import MediaActor
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MediaActorResponse(BaseModel):
|
||||
id: str
|
||||
name: str | None
|
||||
url: str
|
||||
|
||||
class Actor(BaseModel):
|
||||
name: str | None
|
||||
url: str
|
||||
|
||||
def get_actor_details(media_actor: MediaActor) -> MediaActorResponse:
|
||||
reponse: MediaActorResponse = MediaActorResponse(id=media_actor.id, name=str(media_actor.name), url=str(media_actor.url))
|
||||
return reponse
|
||||
@@ -0,0 +1,16 @@
|
||||
from datetime import datetime
|
||||
|
||||
from src.db.models.media import MediaActorFile, MediaFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MediaActorFileResponse(BaseModel):
|
||||
id: str
|
||||
file_id: str
|
||||
actor_id: str
|
||||
|
||||
def get_actorfile_details(media_actorfile: MediaActorFile) -> MediaActorFileResponse:
|
||||
response: MediaActorFileResponse = MediaActorFileResponse(id=media_actorfile.id,
|
||||
file_id=str(media_actorfile.media_file_id),
|
||||
actor_id=str(media_actorfile.media_actor_id))
|
||||
return response
|
||||
@@ -9,14 +9,14 @@ class MediaFileResponse(BaseModel):
|
||||
title: str | None = None
|
||||
file_name: str | None = None
|
||||
cloud_link: str | None = None
|
||||
url: str
|
||||
url: str | None = None
|
||||
review: bool = False
|
||||
should_download: bool = False
|
||||
|
||||
class Link(BaseModel):
|
||||
url: str
|
||||
|
||||
def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
|
||||
def get_file_details(mediafile: MediaFile) -> MediaFileResponse:
|
||||
response = MediaFileResponse(id=mediafile.id,
|
||||
title=mediafile.title,
|
||||
file_name=mediafile.file_name,
|
||||
@@ -30,8 +30,8 @@ def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None:
|
||||
|
||||
def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None:
|
||||
mediafile.file_name = model.file_name
|
||||
mediafile.cloud_link = model.cloud_link
|
||||
mediafile.url = model.url
|
||||
mediafile.cloud_link = model.cloud_link # type: ignore
|
||||
mediafile.url = model.url # type: ignore
|
||||
mediafile.title = model.title
|
||||
mediafile.last_modified_date = datetime.now()
|
||||
mediafile.review = model.review
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial;
|
||||
padding: 10px;
|
||||
background: lightgrey;
|
||||
}
|
||||
|
||||
/* Header/Blog Title */
|
||||
.header {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
/* Style the top navigation bar */
|
||||
.topnav {
|
||||
overflow: hidden;
|
||||
background-color: darkgrey;
|
||||
}
|
||||
|
||||
/* Style the topnav links */
|
||||
.topnav a {
|
||||
float: left;
|
||||
display: block;
|
||||
color: #f2f2f2;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Change color on hover */
|
||||
.topnav a:hover {
|
||||
background-color: #ddd;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.subnav {
|
||||
overflow: hidden;
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.subnav a {
|
||||
float: left;
|
||||
display: block;
|
||||
color: #f2f2f2;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.form-inline label {
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.form-inline input {
|
||||
padding-right: 50px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.form-inline button {
|
||||
padding: 10px 20px;
|
||||
background-color: dodgerblue;
|
||||
border: 1px solid #ddd;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.form-inline button:hover {
|
||||
background-color: royalblue;
|
||||
}
|
||||
|
||||
.pill-nav a {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-color: dodgerblue;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* Change the color of links on mouse-over */
|
||||
.pill-nav a:hover {
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Add a color to the active/current link */
|
||||
.pill-nav a.active {
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.maincolumn {
|
||||
float:left;
|
||||
width: 100%;
|
||||
}
|
||||
/* Create two unequal columns that floats next to each other */
|
||||
/* Left column */
|
||||
.leftcolumn {
|
||||
float: left;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
/* Right column */
|
||||
.rightcolumn {
|
||||
float: left;
|
||||
width: 25%;
|
||||
background-color: #f1f1f1;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* Fake image */
|
||||
.fakeimg {
|
||||
background-color: #aaa;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Add a card effect for articles */
|
||||
.card {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Clear floats after the columns */
|
||||
.row::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #ddd;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Responsive layout - when the screen is less than 800px wide, make the two columns stack on top of each other instead of next to each other */
|
||||
@media screen and (max-width: 800px) {
|
||||
.leftcolumn, .rightcolumn {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive layout - when the screen is less than 400px wide, make the navigation links stack on top of each other instead of next to each other */
|
||||
@media screen and (max-width: 400px) {
|
||||
.topnav a {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,18 @@
|
||||
<title>Comic List</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Comics..</Comics></h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block subnav %}
|
||||
<div class="subnav">
|
||||
<a href="/comic/artists/">Artists</a>
|
||||
<a href="/comic/publishers/">Publishers</a>
|
||||
<a href="/comic/worktypes">WorkTypes</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<h2>Footer</h2>
|
||||
@@ -1,4 +1,12 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
|
||||
<div class="topnav">
|
||||
<a href="/">Kontor</a>
|
||||
<a href="/comic/comics">Comics</a>
|
||||
<a href="/media/files">Media</a>
|
||||
<a style="float:right" href="/login/">Login</a></li>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <nav class="navbar navbar-expand-lg navbar-light bg-light px-5">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Kontor</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
@@ -63,4 +71,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav> -->
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<title>Kontor</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Kontor</h1>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
<th scope="row">Actor Name</th>
|
||||
<td colspan="2">{{actor.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">URL</th>
|
||||
<td colspan="2">{{actor.url}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Works</th>
|
||||
<td colspan="2">
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
<title>MediaFiles List</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>MediaFiles..</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block subnav %}
|
||||
{% include "media/media_nav.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with msg=msg %}
|
||||
{% include "components/alerts.html" %}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="subnav">
|
||||
<a href="/media/files/">MediaFiles</a>
|
||||
<a href="/media/actors/">MediaActors</a>
|
||||
<a href="/media/videos/">MediaVideos</a>
|
||||
</div>
|
||||
@@ -9,17 +9,23 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% include "components/navbar.html" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<body>
|
||||
<div class="header">
|
||||
{% block header %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% include "components/navbar.html" %}
|
||||
{% block subnav %}
|
||||
{% endblock %}
|
||||
<!-- <div class="topnav">
|
||||
<a href="#">Link</a>
|
||||
<a href="#">Link</a>
|
||||
<a href="#">Link</a>
|
||||
<a href="#" style="float:right">Link</a>
|
||||
</div> -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||
<script src="{{ url_for('static', path='js/autocomplete.js') }}"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from fastapi import Request
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AddArtistForm:
|
||||
def __init__(self, request: Request, artist_id: str, artist_name: str, artist_link: str):
|
||||
self.request = request
|
||||
self.errors: List = []
|
||||
self.id: str = artist_id
|
||||
self.name: Optional[str] = artist_name
|
||||
self.link: Optional[str] = artist_link
|
||||
|
||||
|
||||
async def load_data(self):
|
||||
form = await self.request.form()
|
||||
print(f"{form.keys()}")
|
||||
self.name = form.get("artist.name")
|
||||
self.link = form.get("artist.link")
|
||||
|
||||
def is_valid(self):
|
||||
if not self.errors:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name=}, {self.link=}"
|
||||
@@ -0,0 +1,21 @@
|
||||
from typing import AnyStr
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from src.db.models.media import MediaActor
|
||||
from src.db.session import SessionDep
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
router = APIRouter(include_in_schema=False, prefix="/media")
|
||||
|
||||
|
||||
@router.get("/actors")
|
||||
def get_actors(db: SessionDep, request: Request, msg: str | None = None):
|
||||
actors = db.query(MediaActor).all()
|
||||
return templates.TemplateResponse("media/actors.html", {"request": request, "msg": msg, "actors": actors})
|
||||
|
||||
@router.get("/actors/{actor_id}")
|
||||
def artist_detail(actor_id: AnyStr, request: Request, db: SessionDep):
|
||||
actor = db.get(MediaActor, actor_id)
|
||||
return templates.TemplateResponse("media/actor_detail.html", {"request": request, "actor": actor})
|
||||
|
||||
Reference in New Issue
Block a user