21 Commits

Author SHA1 Message Date
Matthias Weber
566183dc4a make somewhat anonymous, improve evaluation
All checks were successful
release-tag / release-image (push) Successful in 6m27s
2024-07-10 21:31:55 +02:00
e4b2d04c7b tidy up action 2024-07-03 18:13:16 +02:00
8109f121cb run build action online on new tag (version) 2024-07-03 18:10:08 +02:00
b1ba54194f remove jquery dependency
Some checks failed
release-tag / release-image (push) Has been cancelled
2024-07-03 17:06:25 +02:00
9ff28d99de Clean up
All checks were successful
release-tag / release-image (push) Successful in 5m8s
2024-07-03 12:16:00 +02:00
6955739841 Fix database errors
All checks were successful
release-tag / release-image (push) Successful in 6m17s
2024-07-03 11:57:01 +02:00
7f72c94b9e updaet action
All checks were successful
release-tag / release-image (push) Successful in 28m56s
2024-07-03 10:09:48 +02:00
2b29c22ea4 update action
Some checks failed
release-tag / release-image (push) Failing after 47s
2024-07-03 10:08:38 +02:00
abd90ed378 add gitea action build push image
Some checks failed
release-tag / release-image (push) Failing after 55s
2024-07-03 09:38:31 +02:00
5808b53071 remove github workflow 2024-07-03 09:30:33 +02:00
Matthias Weber
02cfa4b218 Create Dockerfile 2024-07-02 15:41:52 +02:00
Matthias Weber
cbadbcc706 Update docker-compose.yml 2024-07-02 15:40:19 +02:00
Matthias Weber
de06c1371f Rename Dockerfile to Dockerfile-dev 2024-07-02 15:39:21 +02:00
Matthias Weber
075ad83f6a Update docker-image.yml 2024-07-02 15:30:52 +02:00
Matthias Weber
d79b31fd46 Create docker-image.yml 2024-07-02 15:23:30 +02:00
matthias@matsewe.de
6cd77cca50 fix first run 2024-07-02 14:20:09 +02:00
matthias@matsewe.de
9e28915419 fix first run 2024-07-02 14:19:01 +02:00
matthias@matsewe.de
dfeb6d93c9 fix dockerfile 2024-07-02 14:13:25 +02:00
matthias@matsewe.de
39281e7e52 change sample compose to auto load 2024-07-02 13:58:49 +02:00
matthias@matsewe.de
b48a27b2a3 sample Docker/-compose file 2024-07-02 13:51:19 +02:00
Matthias Weber
dbaf0c5e4c Merge pull request #1 from matsewe/choriosity-ci
Corporate Identity, Slack OAuth, etc.
2024-07-02 13:19:12 +02:00
12 changed files with 216 additions and 137 deletions

View File

@@ -0,0 +1,55 @@
name: release-tag
on:
push:
tags:
- '*'
jobs:
release-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
env:
DOCKER_ORG: matthias
DOCKER_LATEST: latest
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
with:
config-inline: |
[registry."git.matsewe.de"]
http = true
insecure = true
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: git.matsewe.de
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: |
git.matsewe.de/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
git.matsewe.de/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
__pycache__
.venv
.vscode
.env
data

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.11
WORKDIR /code
COPY ./requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /tmp/requirements.txt
COPY ./app /code/app
COPY ./static /code/static
COPY ./templates /code/templates
RUN echo "first_run" > "/tmp/first_run"
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]

11
Dockerfile-dev Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.11
WORKDIR /code
COPY ./requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /tmp/requirements.txt
RUN echo "first_run" > "/tmp/first_run"
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80", "--reload"]

View File

@@ -1,9 +1,7 @@
import app.models as models
from sqlalchemy import func, and_
from sqlalchemy.orm.attributes import flag_modified
from starlette_context import context
from starlette_context.header_keys import HeaderKeys
from sqlalchemy.sql import text
def get_songs_and_vote_for_session(db, session_name) -> list[models.Song]:
session_entry = activate_session(db, session_name)
@@ -17,19 +15,20 @@ def get_songs_and_vote_for_session(db, session_name) -> list[models.Song]:
return songs_and_votes
def get_all_songs_and_votes(db) -> list:
res = db.execute(text("""SELECT
songs.og_artist,
songs.aca_artist,
songs.title,
songs.url,
COUNT(vote) FILTER (where vote = -1) as "nein" ,
COUNT(vote) FILTER (where vote = 0) as "neutral",
COUNT(vote) FILTER (where vote = 1) as "ja"
FROM votes INNER JOIN songs ON votes.song_id = songs.id
GROUP BY song_id ORDER BY song_id
""")).fetchall()
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
return [dict(r._mapping) for r in res]
def create_song(db,
@@ -101,33 +100,25 @@ def create_or_update_comment(db, song_id, session_name, comment):
def activate_session(db, session_name):
ip = context.data[HeaderKeys.forwarded_for]
user_agent = context.data[HeaderKeys.user_agent]
session_entry = db.query(models.Session).filter(and_(
models.Session.session_name == session_name)).first() # , models.Session.ip == ip, models.Session.user_agent == user_agent
session_entry = db.query(models.Session).filter(
models.Session.session_name == session_name).first() # , models.Session.ip == ip, models.Session.user_agent == user_agent
if session_entry:
if ip not in session_entry.ips:
session_entry.ips.append(ip)
session_entry.active = True
else:
session_entry = models.Session(
session_name=session_name, active=True, ips=[ip]) # , ip=ip, user_agent=user_agent
session_name=session_name, active=True) # , ip=ip, user_agent=user_agent
db.add(session_entry)
flag_modified(session_entry, "active")
flag_modified(session_entry, "ips")
db.commit()
return session_entry
def deactivate_session(db, session_name):
ip = context.data[HeaderKeys.forwarded_for]
user_agent = context.data[HeaderKeys.user_agent]
session_entry = db.query(models.Session).filter(and_(
models.Session.session_name == session_name)).first() # , models.Session.ip == ip, models.Session.user_agent == user_agent
session_entry = db.query(models.Session).filter(
models.Session.session_name == session_name).first() # , models.Session.ip == ip, models.Session.user_agent == user_agent
if session_entry:
session_entry.active = False
flag_modified(session_entry, "active")

View File

@@ -1,4 +1,4 @@
from fastapi import FastAPI, Request, Depends, Cookie, Security
from fastapi import FastAPI, Request, Depends, Security
from app.routers import admin, songs, session
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
@@ -6,26 +6,26 @@ from fastapi.templating import Jinja2Templates
from app.database import engine, Base, get_db, SessionLocal
from app.crud import get_songs_and_vote_for_session, get_setting
from sqlalchemy.orm import Session
from typing import Annotated
from app.schemas import Song
import json
import os
import asyncio
from jose import JWTError, jwt
from app.security import get_current_user
from starlette.middleware import Middleware
from starlette_context import context, plugins
from starlette_context import plugins
from starlette_context.middleware import RawContextMiddleware
if os.path.isfile("first_run") and (os.environ.get("RELOAD_ON_FIRST_RUN").lower() == "true"):
from hashlib import sha384
Base.metadata.create_all(engine)
if os.path.isfile("/tmp/first_run") and (os.environ.get("RELOAD_ON_FIRST_RUN", "").lower() == "true"):
print("First run ... load data")
with SessionLocal() as db:
asyncio.run(admin.create_upload_file(include_non_singable=True, db=db))
os.remove("first_run")
os.remove("/tmp/first_run")
# Base.metadata.create_all(engine)
middleware = [
Middleware(
@@ -60,7 +60,7 @@ async def vote(request: Request, session_id: str | None = None , unordered: bool
db: Session = Depends(get_db)) -> HTMLResponse:
if not session_id:
session_id = user["sub"]
session_id = sha384(str.encode(user["sub"])).hexdigest() #CryptContext(schemes=["bcrypt"], deprecated="auto").hash(user["sub"])
#print(user)

View File

@@ -32,7 +32,6 @@ class Session(Base):
id: Mapped[int] = mapped_column(primary_key=True)
session_name: Mapped[str]
active: Mapped[bool]
ips: Mapped[list[str]]
first_seen: Mapped[datetime] = mapped_column(server_default=func.now())
last_seen: Mapped[Optional[datetime]
] = mapped_column(onupdate=func.now())

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
from app.database import get_db, engine, Base
from app.security import get_current_user
from app.crud import create_song, get_setting, set_setting
from app.crud import create_song, get_setting, set_setting, get_all_songs_and_votes
router = APIRouter(
prefix="/admin",
@@ -79,11 +79,12 @@ async def create_upload_file(include_non_singable: bool = False, db: Session = D
category_names = list(song_list.iloc[0][8:17])
for i, row in song_list[1:].iterrows():
if (row[17] == "nein") and not include_non_singable:
continue
row = np.array(row)
if (row[17] == "nein") and not include_non_singable:
continue
if not row[2]: # no title
continue
@@ -126,3 +127,8 @@ async def toggle_veto_mode(db: Session = Depends(get_db)) -> bool:
else:
set_setting(db, "veto_mode", True)
return True
@router.get("/results")
async def get_evaluation(db = Depends(get_db)) -> list:
return get_all_songs_and_votes(db)

View File

@@ -1,11 +1,10 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Request
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_session, create_or_update_vote, get_all_songs_and_votes, create_or_update_comment
from app.crud import get_songs_and_vote_for_session, create_or_update_vote, create_or_update_comment
router = APIRouter(
prefix="/songs",
@@ -28,6 +27,3 @@ async def comment(song_id: str, session_id: str, comment: str, db: Annotated[Ses
create_or_update_comment(db, song_id, session_id, comment)
#create_or_update_vote(db, song_id, session_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)

View File

@@ -1,8 +1,6 @@
from typing import Annotated
from fastapi import HTTPException, Cookie, status, Request
from fastapi import HTTPException, status, Request
from fastapi.security import SecurityScopes
from jose import JWTError, jwt
from jose import JWTError
from pydantic import ValidationError
import os
@@ -11,7 +9,7 @@ import os
# openssl rand -hex 32
scopes_db = {
os.environ['ADMIN_EMAIL'] : ["admin"]
os.environ.get('ADMIN_EMAIL', "") : ["admin"]
}
credentials_exception = HTTPException(
@@ -22,6 +20,10 @@ credentials_exception = HTTPException(
async def get_current_user(
security_scopes: SecurityScopes, request: Request
):
if os.environ.get("NO_LOGIN", "").lower() == "true":
return {"sub": "test"}
try:
username: str = request.headers.get("x-auth-request-user") # type: ignore
if username is None:

21
docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
version: "3.7"
services:
liederwahl-dev:
build:
context: .
dockerfile: Dockerfile-dev
container_name: liederwahl-dev
restart: unless-stopped
volumes:
- .:/code
- liederwahl-dev:/data
ports:
- 80:80
environment:
- LIST_URL=${LIST_URL}
- NO_LOGIN=true
- RELOAD_ON_FIRST_RUN=true
volumes:
liederwahl-dev:

View File

@@ -27,37 +27,9 @@
{% endif %}
<script src="https://open.spotify.com/embed/iframe-api/v1" async></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
const session_id = "{{ session_id }}";
function activate_session() {
$.ajax({
url: "/session/" + session_id,
method: "PUT"
})
}
function deactivate_session() {
$.ajax({
url: "/session/" + session_id,
method: "DELETE"
})
}
$(window).on("load", activate_session);
$(window).on("beforeunload", deactivate_session);
$(window).on("unload", deactivate_session);
$(window).on("pagehide", deactivate_session);
$(document).on('visibilitychange', function () {
if (document.visibilityState == 'hidden') {
deactivate_session();
} else {
activate_session()
}
});
var spotify_embed_controller;
window.onSpotifyIframeApiReady = (IFrameAPI) => {
@@ -75,12 +47,15 @@
var is_playing = -1;
function stop() {
$("#song-" + is_playing + " .cover-container .overlay img").attr("src", "/static/play.svg");
if (is_playing !== -1) {
document.querySelector("#song-" + is_playing + " .cover-container .overlay img").setAttribute("src", "/static/play.svg");
}
document.querySelector("#yt-player").style.display = "none";
document.querySelector("#spotify-player").style.display = "none";
document.querySelector("#close-player").style.display = "none";
document.querySelector("#yt-player").innerHTML = "";
$("#yt-player").css("display", "none");
$("#spotify-player").css("display", "none");
$("#close-player").css("display", "none");
$("#yt-player").html("");
spotify_embed_controller.pause();
is_playing = -1;
}
@@ -93,12 +68,13 @@
is_playing = song_id;
$("#song-" + song_id + " .cover-container .overlay img").attr("src", "/static/stop.svg");
document.querySelector("#song-" + song_id + " .cover-container .overlay img").setAttribute("src", "/static/stop.svg");
document.querySelector("#yt-player").style.display = "flex";
document.querySelector("#close-player").style.display = "block";
$("#yt-player").css("display", "flex");
$("#close-player").css("display", "block");
iframe_code = '<iframe src="https://www.youtube.com/embed/' + yt_id + '?autoplay=1" title="" width="640" height="360" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>';
$("#yt-player").html(iframe_code);
document.querySelector("#yt-player").innerHTML = iframe_code;
}
}
@@ -110,10 +86,10 @@
is_playing = song_id;
$("#song-" + song_id + " .cover-container .overlay img").attr("src", "/static/stop.svg");
document.querySelector("#song-" + song_id + " .cover-container .overlay img").setAttribute("src", "/static/stop.svg");
$("#spotify-player").css("display", "flex");
$("#close-player").css("display", "block");
document.querySelector("#spotify-player").style.display = "flex";
document.querySelector("#close-player").style.display = "block";
spotify_embed_controller.loadUri("spotify:track:" + spfy_id);
spotify_embed_controller.play();
}
@@ -124,59 +100,62 @@
window.open(url, '_blank').focus();
}
function vote(song_id, vote) {
$.ajax({
url: "/songs/" + song_id + "/vote?" + $.param({ session_id: session_id, vote: vote }),
method: "POST",
success: function (data, textStatus) {
no_button = $("#song-" + song_id).find(".button-no")
yes_button = $("#song-" + song_id).find(".button-yes")
neutral_button = $("#song-" + song_id).find(".button-neutral")
no_button.removeClass("selected")
yes_button.removeClass("selected")
neutral_button.removeClass("selected")
switch (vote) {
case 0:
neutral_button.addClass("selected")
{% if veto_mode %}
$("#song-" + song_id).removeClass("not_singable")
{% endif %}
break;
case 1:
yes_button.addClass("selected")
{% if veto_mode %}
$("#song-" + song_id).removeClass("not_singable")
{% endif %}
break;
case -1:
no_button.addClass("selected")
{% if veto_mode %}
$("#song-" + song_id).addClass("not_singable")
{% endif %}
break;
default:
break;
}
async function vote(song_id, vote) {
response = await fetch(
"/songs/" + song_id + "/vote?session_id=" + encodeURIComponent(session_id) + "&vote=" + vote, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
no_button = document.querySelector("#song-" + song_id + " .button-no");
yes_button = document.querySelector("#song-" + song_id + " .button-yes");
neutral_button = document.querySelector("#song-" + song_id + " .button-neutral");
no_button.classList.remove("selected")
yes_button.classList.remove("selected")
neutral_button.classList.remove("selected")
switch (vote) {
case 0:
neutral_button.classList.add("selected")
{% if veto_mode %}
document.querySelector("#song-" + song_id).classList.remove("not_singable")
{% endif %}
break;
case 1:
yes_button.classList.add("selected")
{% if veto_mode %}
document.querySelector("#song-" + song_id).classList.remove("not_singable")
{% endif %}
break;
case -1:
no_button.classList.add("selected")
{% if veto_mode %}
document.querySelector("#song-" + song_id).classList.add("not_singable")
{% endif %}
break;
default:
break;
}
}
}
{% if veto_mode %}
function updateComment(song_id, el) {
async function updateComment(song_id, el) {
comment = el.value
$.ajax({
url: "/songs/" + song_id + "/comment?" + $.param({ session_id: session_id, comment: comment }),
method: "POST"
})
response = await fetch(
"/songs/" + song_id + "/comment?session_id=" + encodeURIComponent(session_id) + "&comment=" + encodeURIComponent(comment), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
}
{% endif %}
</script>
@@ -189,8 +168,10 @@
abgeben.
</div>
{% else %}
<h1 id="title"><img id="title-logo" src="https://choriosity.de/assets/images/logo-choriosity_weiss.svg"> Liederwahl</h1>
<div class="text">Du kannst die Liederwahl jederzeit unterbrechen und zu einem späteren Zeitpunkt weitermachen. Mit einem Klick auf das Thumbnail kannst du das Lied abspielen.
<h1 id="title"><img id="title-logo" src="https://choriosity.de/assets/images/logo-choriosity_weiss.svg"> Liederwahl
</h1>
<div class="text">Du kannst die Liederwahl jederzeit unterbrechen und zu einem späteren Zeitpunkt weitermachen. Mit
einem Klick auf das Thumbnail kannst du das Lied abspielen.
</div>
{% endif %}
<div id="songs">