From f6016f57366ca028452ebe5871b3086f2e7aca15 Mon Sep 17 00:00:00 2001 From: "matthias@matsewe.de" Date: Wed, 22 May 2024 19:44:24 +0200 Subject: [PATCH] restructure --- .gitignore | 4 +-- app/crud.py | 67 ++++++++++++++++++++++++++++++++++++++++++++ app/database.py | 25 +++++++++++++++++ app/dependencies.py | 6 ---- app/main.py | 15 ++++++---- app/models.py | 53 ++++++++++++++++++++--------------- app/routers/admin.py | 33 +++++++++++----------- app/routers/songs.py | 46 ++++++++++++------------------ app/schemas.py | 18 ++++++++++++ app/sql_models.py | 30 -------------------- static/site.css | 13 +++++++-- 11 files changed, 195 insertions(+), 115 deletions(-) create mode 100644 app/crud.py create mode 100644 app/database.py delete mode 100644 app/dependencies.py create mode 100644 app/schemas.py delete mode 100644 app/sql_models.py diff --git a/.gitignore b/.gitignore index f805aad..7a5cff2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ __pycache__ .venv -.vscode -service_account.json -db.sqlite \ No newline at end of file +.vscode \ No newline at end of file diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 0000000..1bcec02 --- /dev/null +++ b/app/crud.py @@ -0,0 +1,67 @@ +import app.models as models +from sqlalchemy import func + +def get_songs_and_vote_for_user(db, user_id) -> list[models.Song]: + votes = db.query(models.Vote).filter(models.Vote.user_id == user_id).subquery() + + songs_and_votes = db.query( + models.Song, votes.c.vote + ).filter( + models.Song.singable == True + ).join(votes, isouter=True).filter().all() + + return songs_and_votes + + +def get_all_songs_and_votes(db) -> dict[int, dict[int, int]]: + _v = db.query(models.Vote.song_id, models.Vote.vote, func.count(models.Vote.song_id)).group_by(models.Vote.song_id, models.Vote.vote).all() + + votes = {} + + for v in _v: + if v[0] not in votes: + votes[v[0]] = {-1 : 0, 0 : 0, 1 : 0} + votes[v[0]][v[1]] = v[2] + + return votes + +def create_song(db, + og_artist, + aca_artist, + title, + url, + yt_id, + spfy_id, + thumbnail, + is_aca, + arng_url, + categories, + main_category, + singable + ): + s = models.Song(og_artist=og_artist, + aca_artist=aca_artist, + title=title, + url=url, + yt_id=yt_id, + spfy_id=spfy_id, + thumbnail=thumbnail, + is_aca=is_aca, + arng_url=arng_url, + categories=categories, + main_category=main_category, + singable=singable) + + db.add(s) + db.commit() + + +def create_or_update_vote(db, song_id, user_id, vote): + vote_entry = db.query(models.Vote).filter( + (models.Vote.user_id == user_id) & (models.Vote.song_id == song_id)).first() + if vote_entry: + vote_entry.vote = str(vote) # type: ignore + else: + vote_entry = models.Vote(song_id=song_id, user_id=user_id, vote=vote) + db.add(vote_entry) + db.commit() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..cde0f3a --- /dev/null +++ b/app/database.py @@ -0,0 +1,25 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase +from sqlalchemy.types import PickleType + +import os + +SQLALCHEMY_DATABASE_URL = "sqlite:///" + os.environ.get("DATABASE_URL", "/data/db.sqlite") + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +async def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +class Base(DeclarativeBase): + type_annotation_map = { + dict[str, bool]: PickleType + } \ No newline at end of file diff --git a/app/dependencies.py b/app/dependencies.py deleted file mode 100644 index 26afbb9..0000000 --- a/app/dependencies.py +++ /dev/null @@ -1,6 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -engine = create_engine('sqlite:///db.sqlite') -Session = sessionmaker(engine) -session = Session() \ No newline at end of file diff --git a/app/main.py b/app/main.py index 93a0d3e..598e7ec 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,13 @@ -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Depends from app.routers import admin, user, songs from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from app.dependencies import engine -from app.sql_models import Base -from app.routers.songs import get_songs +from app.database import engine, Base, get_db +from app.crud import get_songs_and_vote_for_user +from sqlalchemy.orm import Session +from typing import Annotated +from app.schemas import Song Base.metadata.create_all(engine) @@ -20,13 +22,14 @@ app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") @app.get("/", response_class=HTMLResponse) -async def root(request: Request, session_id : str = ""): +async def root(request: Request, session_id : str = "", db: Annotated[Session, Depends(get_db)] = None): if session_id == "": return templates.TemplateResponse( request=request, name="landing.html" ) else: - songs = await get_songs(session_id) + songs = [Song(**s.__dict__, vote=v) for s, v in get_songs_and_vote_for_user(db, session_id)] + songs_by_category = {} all_categories = set() for song in songs: diff --git a/app/models.py b/app/models.py index 1705a26..02e671c 100644 --- a/app/models.py +++ b/app/models.py @@ -1,26 +1,35 @@ +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +from sqlalchemy import Integer, ForeignKey +from sqlalchemy.sql import func from typing import Optional -from pydantic import BaseModel +from datetime import datetime -class GoogleFile(BaseModel): - file_id: str - file_name: str -class Genre(BaseModel): - genre_id: Optional[int] - genre_name: str +class Song(Base): + __tablename__ = 'songs' + id: Mapped[int] = mapped_column(primary_key=True) + og_artist: Mapped[Optional[str]] + aca_artist: Mapped[Optional[str]] + title: Mapped[Optional[str]] + url: Mapped[Optional[str]] + yt_id: Mapped[Optional[str]] + spfy_id: Mapped[Optional[str]] + thumbnail: Mapped[Optional[str]] + is_aca: Mapped[Optional[bool]] + arng_url: Mapped[Optional[str]] + categories: Mapped[Optional[dict[str, bool]]] + main_category: Mapped[Optional[str]] + singable: Mapped[Optional[bool]] -class Song(BaseModel): - id: int - og_artist: Optional[str] - aca_artist: Optional[str] - title: Optional[str] - url: Optional[str] - yt_id: Optional[str] - spfy_id: Optional[str] - thumbnail: Optional[str] - is_aca: bool - arng_url: Optional[str] - categories: dict[str, bool] - main_category: str - singable: bool - vote: Optional[int] \ No newline at end of file + +class Vote(Base): + __tablename__ = 'votes' + id: Mapped[int] = mapped_column(primary_key=True) + song_id: Mapped[int] = mapped_column(Integer, ForeignKey("songs.id")) + user_id: Mapped[int] + vote: Mapped[Optional[int]] + time_created: Mapped[datetime] = mapped_column(server_default=func.now()) + time_updated: Mapped[Optional[datetime] + ] = mapped_column(onupdate=func.now()) diff --git a/app/routers/admin.py b/app/routers/admin.py index 3c3be3e..c23ac4c 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -1,17 +1,14 @@ -from fastapi import APIRouter, Security -from app.sql_models import SqlSong -from app.dependencies import session -from app.dependencies import engine -from app.routers.user import get_current_user import pandas as pd import numpy as np import re import requests import os -from app.sql_models import Base - - +from fastapi import APIRouter, Security, Depends +from sqlalchemy.orm import Session +from app.database import get_db, engine, Base +from app.routers.user import get_current_user +from app.crud import create_song router = APIRouter( prefix="/admin", @@ -28,10 +25,11 @@ def get_main_category(categories) -> int: else: return np.argmax(categories != None, axis=0) + def get_youtube_id(url): if url is None: return None - + youtube_regex = ( r'(https?://)?(www\.)?' '(youtube|youtu|youtube-nocookie)\.(com|be)/' @@ -43,10 +41,11 @@ def get_youtube_id(url): return None + def get_thumbnail(url): if url is None: return "/static/cover.jpg" - + m = get_youtube_id(url) if m: thumbnail_url = "https://img.youtube.com/vi/" + m + "/mqdefault.jpg" @@ -56,6 +55,7 @@ def get_thumbnail(url): else: return "/static/cover.jpg" + def get_spotify_id(url): if url is None: return None @@ -64,8 +64,9 @@ def get_spotify_id(url): else: return None + @router.post("/load_list") -async def create_upload_file(): +async def create_upload_file(db: Session = Depends(get_db)): Base.metadata.drop_all(engine) Base.metadata.create_all(engine) @@ -83,12 +84,13 @@ async def create_upload_file(): spfy_id = get_spotify_id(row[3]) categories = {n: v for n, v in zip( - category_names, row[6:19] != None)} - + category_names, row[6:19] != None)} + if not np.any(list(categories.values())): continue - s = SqlSong(og_artist=row[0], + create_song(db, + og_artist=row[0], aca_artist=row[1], title=row[2], url=row[3], @@ -101,6 +103,3 @@ async def create_upload_file(): main_category=category_names[get_main_category(row[6:19])], singable=row[19] != "nein" ) - - session.add(s) - session.commit() diff --git a/app/routers/songs.py b/app/routers/songs.py index e0758d4..e5c3eb9 100644 --- a/app/routers/songs.py +++ b/app/routers/songs.py @@ -1,39 +1,29 @@ -from fastapi import APIRouter -from app.models import Song -from app.sql_models import SqlSong, SqlVote -from app.dependencies import session +from typing import Annotated +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +import app.models as models +from app.database import get_db +from app.schemas import Song +from app.crud import get_songs_and_vote_for_user, create_or_update_vote, get_all_songs_and_votes router = APIRouter( prefix="/songs", - #dependencies=[Security(get_current_user, scopes=["public"])], + # dependencies=[Security(get_current_user, scopes=["public"])], responses={404: {"description": "Not found"}}, ) -async def songify(s, votes): - return Song(**s.__dict__, vote=votes.get(s.id, None)) @router.get("/") -async def get_songs(user_id : str = "") -> list[Song]: - sqlsongs = session.query(SqlSong).filter(SqlSong.singable == True).all() - votes = session.query(SqlVote).filter(SqlVote.user_id == user_id).all() - votes = {v.song_id : v.vote for v in votes} +async def get_songs(user_id: str = "", db: Annotated[Session, Depends(get_db)] = None) -> list[Song]: + return [Song(**s.__dict__, vote=v) for s, v in get_songs_and_vote_for_user(db, user_id)] - songs = [] - for s in sqlsongs: - try: - songs.append(Song(**s.__dict__, vote=votes.get(s.id, None))) - except: - print(s.__dict__) - pass - return songs - #return [Song(**s.__dict__, vote=votes.get(s.id, None)) for s in sqlsongs] # type: ignore @router.post("/{song_id}/vote") -async def vote(song_id : str, user_id : str, vote : int): - vote_entry = session.query(SqlVote).filter((SqlVote.user_id == user_id) & (SqlVote.song_id == song_id)).first() - if vote_entry: - vote_entry.vote = str(vote) # type: ignore - else: - vote_entry = SqlVote(song_id=song_id, user_id=user_id, vote=vote) - session.add(vote_entry) - session.commit() +async def vote(song_id: str, user_id: str, vote: int, db: Annotated[Session, Depends(get_db)]): + create_or_update_vote(db, song_id, user_id, vote) + + +@router.get("/evaluation") +async def get_evaluation(db: Annotated[Session, Depends(get_db)] = None) -> dict[int, dict[int, int]]: + return get_all_songs_and_votes(db) \ No newline at end of file diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..8da0965 --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,18 @@ +from typing import Optional +from pydantic import BaseModel + +class Song(BaseModel): + id: int + og_artist: Optional[str] + aca_artist: Optional[str] + title: Optional[str] + url: Optional[str] + yt_id: Optional[str] + spfy_id: Optional[str] + thumbnail: Optional[str] + is_aca: Optional[bool] + arng_url: Optional[str] + categories: Optional[dict[str, bool]] + main_category: Optional[str] + singable: Optional[bool] + vote: Optional[int] \ No newline at end of file diff --git a/app/sql_models.py b/app/sql_models.py deleted file mode 100644 index 578c16c..0000000 --- a/app/sql_models.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlalchemy import Column, String, Integer, Boolean, PickleType -from sqlalchemy.orm import declarative_base - -Base = declarative_base() - -class SqlSong(Base): - __tablename__ = 'songs' - id = Column(Integer, primary_key=True) - og_artist = Column(String) - aca_artist = Column(String) - title = Column(String) - url = Column(String) - yt_id = Column(String) - spfy_id = Column(String) - thumbnail = Column(String) - is_aca = Column(Boolean) - arng_url = Column(String) - - categories = Column(PickleType) - - main_category = Column(String) - - singable = Column(Boolean) - -class SqlVote(Base): - __tablename__ = 'votes' - id = Column(Integer, primary_key=True) - song_id = Column(Integer) - user_id = Column(String) - vote = Column(Integer, nullable=True) \ No newline at end of file diff --git a/static/site.css b/static/site.css index edfc744..43c6012 100644 --- a/static/site.css +++ b/static/site.css @@ -14,7 +14,13 @@ } -@media only screen and (min-resolution: 200dpi) { +@media only screen and (min-resolution: 2dppx) { + body { + font-size: 3.2vmin; + } +} + +@media screen and (-webkit-min-device-pixel-ratio: 2) { body { font-size: 3.2vmin; } @@ -199,7 +205,8 @@ color: white; max-width: 10em; overflow: clip; - background-color: hsl(calc(var(--hue) * 360), 100%, 40%); /*color-mix(in srgb, var(--main-color) 60%, transparent);*/ + background-color: hsl(calc(var(--hue) * 360), 100%, 40%); + /*color-mix(in srgb, var(--main-color) 60%, transparent);*/ } .vote-buttons { @@ -232,7 +239,7 @@ h1 { border-bottom: 0.3rem solid hsl(calc(var(--hue) * 360), 100%, 40%); } -body > h1 { +body>h1 { background-color: color-mix(in srgb, hsl(0, 0%, 40%) 50%, transparent); border-bottom: 0.3rem solid hsl(0, 0%, 40%); }