Deploy folkugat web

This commit is contained in:
marc
2025-03-30 20:52:47 +02:00
parent d596861a2e
commit 6962d70468
20 changed files with 182 additions and 1101 deletions

View File

@@ -1,25 +1,20 @@
#+title: Readme
* Tasques
** TODO Refactor organitzatiu
** TODO Ordenar els resultats de la cerca de temes
** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat)
** TODO Usuaris i permisos granulars
** TODO Lilypond support (o similar)
*** TODO Fer cançoners "en directe"
* Idees
** Jams
*** Properes jams
**** Data i lloc de les properes jams
**** Info dels temes que es tocaràn (a la slow jam)
*** Live jam
**** Quin tema està sonant ara
*** Historial de jams
**** Veure a cada jam passada quins temes s'han tocat
** Temes
*** Navegació
**** Cerca de temes
***** Per nom, tonalitat, compàs, tipus, (shazam!?!?!)
***** Per tocats recentment
**** Informació
***** Nom
***** Tonalitat i compàs
***** Vegades que s'ha tocat
* Referències
Fonts d'inspiració i altres materials de referència per fer la web de folkugat
** Simple Site

8
build.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
tailwindcss -i folkugat_web/assets/static/src/tw.css -o folkugat_web/assets/static/css/main.css --minify
docker build -t marc.sastre.cat/folkugat-web -f deploy/Dockerfile .
docker push marc.sastre.cat/folkugat-web:latest

11
deploy/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.11
WORKDIR /folkugat
COPY deploy/requirements.txt /folkugat/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /folkugat/requirements.txt
COPY folkugat_web /folkugat/folkugat_web
CMD ["uvicorn", "folkugat_web.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

11
deploy/requirements.in Normal file
View File

@@ -0,0 +1,11 @@
# API
fastapi
python-multipart
jinja2
uvicorn
# Files
python-magic
# Auth
pyjwt
# Search
levenshtein

48
deploy/requirements.txt Normal file
View File

@@ -0,0 +1,48 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=deploy/requirements.txt deploy/requirements.in
#
annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via starlette
click==8.1.8
# via uvicorn
fastapi==0.115.12
# via -r deploy/requirements.in
h11==0.14.0
# via uvicorn
idna==3.10
# via anyio
jinja2==3.1.6
# via -r deploy/requirements.in
levenshtein==0.27.1
# via -r deploy/requirements.in
markupsafe==3.0.2
# via jinja2
pydantic==2.10.6
# via fastapi
pydantic-core==2.27.2
# via pydantic
pyjwt==2.10.1
# via -r deploy/requirements.in
python-magic==0.4.27
# via -r deploy/requirements.in
python-multipart==0.0.20
# via -r deploy/requirements.in
rapidfuzz==3.12.2
# via levenshtein
sniffio==1.3.1
# via anyio
starlette==0.46.1
# via fastapi
typing-extensions==4.12.2
# via
# anyio
# fastapi
# pydantic
# pydantic-core
uvicorn==0.34.0
# via -r deploy/requirements.in

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
folkugat:
# image: "marc.sastre.cat/quinto-cua:latest"
build:
context: .
dockerfile: deploy/Dockerfile
restart: unless-stopped
environment:
- JWT_SECRET=12345
- ADMIN_PASSWORD=prova
- DB_DIR=/folkugat/files
volumes:
- /home/marc/tmp/folkugat:/folkugat/files
ports:
- "8001:80"
networks:
- folkugat
networks:
folkugat:

View File

@@ -22,6 +22,8 @@
pytest
setuptools
ipython
# To compile requirements
pip-tools
];
projectDependencies = with pythonPackages; [

View File

@@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse
from folkugat_web.api import router
from folkugat_web.fragments import tema, temes
from folkugat_web.services import auth
from folkugat_web.services import files as files_service
from folkugat_web.services.temes import links as links_service
from folkugat_web.services.temes import lyrics as lyrics_service
from folkugat_web.services.temes import properties as properties_service
@@ -46,6 +47,7 @@ def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
@router.delete("/api/tema/{tema_id}")
def delete_tema(_: auth.RequireLogin, tema_id: int):
temes_w.delete_tema(tema_id=tema_id)
files_service.clean_orphan_files()
return HTMLResponse(headers={
'HX-Redirect': '/temes'
})

View File

@@ -6,8 +6,10 @@ from fastapi.responses import HTMLResponse
from folkugat_web.api import router
from folkugat_web.fragments.tema import links as links_fragments
from folkugat_web.model import temes as model
from folkugat_web.services import auth, files
from folkugat_web.services import auth
from folkugat_web.services import files as files_service
from folkugat_web.services.temes import links as links_service
from folkugat_web.services.temes import query as temes_q
@router.get("/api/tema/{tema_id}/link/{link_id}")
@@ -30,7 +32,7 @@ async def set_link(
upload_file: Annotated[UploadFile | None, File()] = None,
):
if upload_file:
url = await files.store_file(tema_id=tema_id, upload_file=upload_file)
url = await files_service.store_file(tema_id=tema_id, upload_file=upload_file)
link_type = links_service.guess_link_type(url or '')
new_link = model.Link(
@@ -66,6 +68,7 @@ def delete_link(
link_id: int,
):
links_service.delete_link(link_id=link_id, tema_id=tema_id)
files_service.clean_orphan_files()
return HTMLResponse(
headers={
"HX-Trigger": f"reload-tema-{tema_id}-score"
@@ -102,10 +105,14 @@ def get_score(
logged_in: auth.LoggedIn,
tema_id: int,
):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
tema = links_service.add_links_to_tema(tema)
return links_fragments.score(
request=request,
logged_in=logged_in,
tema_id=tema_id,
tema=tema,
)

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<div class="h-4/5 min-h-[400px] flex flex-col items-center justify-center">
<img src="{{ url_for('static', path='img/folkugat.svg') }}"
<img src="{{ url_for(request, 'static', path='img/folkugat.svg') }}"
class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3"
width="100"
alt="Folkugat"/>

View File

@@ -1,6 +1,6 @@
<!-- PDF Viewer -->
<script type="module">
const pdfViewer = await import("{{ url_for('static', path='js/pdf_viewer.js') }}");
const pdfViewer = await import("{{ url_for(request, 'static', path='js/pdf_viewer.js') }}");
const options = {
url: '{{ pdf_url }}',
// PDF Canvas (where the pdf will be displayed)
@@ -24,13 +24,13 @@
<!-- </a> -->
<!-- </div> -->
<div class="flex flex-row flex-nowrap items-center justify-center w-full">
<button id="prev" class="flex-none text-xl text-beige mx-3">
<button id="prev" class="flex-none text-xl text-beige mr-2">
<i class="fa fa-chevron-left" aria-hidden="true"></i>
</button>
<canvas class="flex-auto m-2 w-1/2 max-w-3xl"
id="the-canvas">
</canvas>
<button id="next" class="flex-none text-xl text-beige mx-3">
<button id="next" class="flex-none text-xl text-beige ml-2">
<i class="fa fa-chevron-right" aria-hidden="true"></i>
</button>
</div>

View File

@@ -14,15 +14,15 @@
referrerpolicy="no-referrer" />
<!-- Taiwind CSS -->
<link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}" type="text/css" />
<link rel="stylesheet" href="{{ url_for(request, 'static', path='css/main.css') }}" type="text/css" />
<!-- HTMX -->
<script src="{{ url_for('static', path='js/htmx.min.js') }}"></script>
<script src="{{ url_for(request, 'static', path='js/htmx.min.js') }}"></script>
<!-- Favicon! -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', path='favicon/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', path='favicon/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', path='favicon/favicon-16x16.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for(request, 'static', path='favicon/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for(request, 'static', path='favicon/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for(request, 'static', path='favicon/favicon-16x16.png') }}">
<link rel="manifest" href="/site.webmanifest">
</head>

View File

@@ -0,0 +1,3 @@
import os
URL_SCHEME = os.getenv("URL_SCHEME", "http")

View File

@@ -1,7 +1,5 @@
from fastapi import HTTPException, Request
from fastapi import Request
from folkugat_web.model import temes as model
from folkugat_web.services.temes import query as temes_q
from folkugat_web.services.temes.links import guess_link_type
from folkugat_web.templates import templates
@@ -73,10 +71,7 @@ def link_icon(request: Request, logged_in: bool, link: model.Link):
)
def score(request: Request, logged_in: bool, tema_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
def score(request: Request, logged_in: bool, tema: model.Tema):
return templates.TemplateResponse(
"fragments/tema/score.html",
{

View File

@@ -90,4 +90,10 @@ class Tema:
return link.link_type is LinkType.IMAGE
def score(self) -> Link | None:
return next(filter(self._is_score, self.links), None)
result = next(filter(self._is_score, self.links), None)
return result
class TemaCols(enum.Enum):
NOM = "nom"
COPS_TOCAT = "cops_tocat"

View File

@@ -1,11 +1,15 @@
import mimetypes
import os
import re
import uuid
from collections.abc import Iterator
from pathlib import Path
import magic
from fastapi import HTTPException, UploadFile
from folkugat_web.config import db
from folkugat_web.dal.sql.temes import links as links_dal
from folkugat_web.log import logger
async def get_mimetype(upload_file: UploadFile) -> str:
@@ -56,3 +60,17 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
def list_files(tema_id: str) -> list[str]:
filedir = db.DB_FILES_DIR / str(tema_id)
return [get_db_file_path(f) for f in filedir.iterdir()]
def get_orphan_files() -> Iterator[Path]:
alive_files = {link.url for link in links_dal.get_links()}
return filter(
lambda p: p.is_file() and get_db_file_path(p) not in alive_files,
db.DB_FILES_DIR.rglob("*"),
)
def clean_orphan_files():
for path in get_orphan_files():
logger.info(f"Deleting the orphan file: {path}")
os.remove(path)

View File

@@ -1,27 +1,31 @@
from typing import Optional
from folkugat_web.dal.sql import Connection
from folkugat_web.dal.sql._connection import get_connection
from folkugat_web.dal.sql.playlists import query, write
from folkugat_web.log import logger
from folkugat_web.model import playlists
from folkugat_web.services.temes import links as links_service
from folkugat_web.services.temes import query as temes_query
def add_temes_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist:
for set_ in playlist.sets:
add_temes_to_set(set_)
_ = add_temes_to_set(set_)
return playlist
def add_temes_to_set(set_: playlists.Set) -> playlists.Set:
for tema_in_set in set_.temes:
add_tema_to_tema_in_set(tema_in_set)
_ = add_tema_to_tema_in_set(tema_in_set)
return set_
def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaInSet:
if tema_in_set.tema_id is not None:
tema_in_set.tema = temes_query.get_tema_by_id(tema_in_set.tema_id)
if not tema_in_set.tema:
logger.error("fCould not load tune in set: {tema_in_set}")
else:
_ = links_service.add_links_to_tema(tema_in_set.tema)
return tema_in_set

View File

@@ -84,7 +84,6 @@ def _apply_limit_offset(limit: int, offset: int) -> Callable[[Iterable[model.Tem
def busca_temes(query: str, hidden: bool = False, limit: int = 10, offset: int = 0) -> list[model.Tema]:
t0 = time.time()
with get_connection() as con:
result = (
FnChain.transform(temes_q.get_tema_id_to_ngrams(con).items()) |

View File

@@ -1,5 +1,20 @@
from typing import Any
from fastapi import Request
from fastapi.templating import Jinja2Templates
from folkugat_web.config import directories as config
from folkugat_web.config import api as api_config
from folkugat_web.config import directories as directories_config
templates = Jinja2Templates(directory=config.TEMPLATES_DIR)
templates = Jinja2Templates(directory=directories_config.TEMPLATES_DIR)
def url_for(request: Request, name: str, **path_params: Any) -> str:
http_url = request.url_for(name, **path_params)
if api_config.URL_SCHEME == "http":
return str(http_url)
# Replace 'http' with 'https'
return str(http_url.replace(scheme=api_config.URL_SCHEME))
templates.env.globals["url_for"] = url_for