Vorbereitung Release 0.2.0

This commit is contained in:
2026-01-29 23:50:41 +01:00
parent 729842a71c
commit 44fac3f471
398 changed files with 40415 additions and 258 deletions
+3 -1
View File
@@ -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"])
-48
View File
@@ -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
+8 -2
View File
@@ -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")
+41
View File
@@ -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")
-69
View File
@@ -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)
+120
View File
@@ -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 -1
View File
@@ -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
+14 -3
View File
@@ -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
+4
View File
@@ -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)]
+28
View File
@@ -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]
+6
View File
@@ -0,0 +1,6 @@
from pydantic import BaseModel
class VolumeResponse(BaseModel):
id: str
name: str
+18
View File
@@ -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
+16
View File
@@ -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
+4 -4
View File
@@ -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
+172
View File
@@ -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> -->
+3
View File
@@ -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>
+16 -10
View File
@@ -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})