restructure
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
__pycache__
|
||||
.venv
|
||||
.vscode
|
||||
service_account.json
|
||||
db.sqlite
|
||||
.vscode
|
||||
67
app/crud.py
Normal file
67
app/crud.py
Normal file
@@ -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()
|
||||
25
app/database.py
Normal file
25
app/database.py
Normal file
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
15
app/main.py
15
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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
18
app/schemas.py
Normal file
18
app/schemas.py
Normal file
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user