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 #+title: Readme
* Tasques * 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 * Idees
** Jams ** Jams
*** Properes jams *** Properes jams
**** Data i lloc de les properes jams
**** Info dels temes que es tocaràn (a la slow jam) **** 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 ** Temes
*** Navegació *** Navegació
**** Cerca de temes **** Cerca de temes
***** Per nom, tonalitat, compàs, tipus, (shazam!?!?!) ***** Per nom, tonalitat, compàs, tipus, (shazam!?!?!)
***** Per tocats recentment ***** Per tocats recentment
**** Informació
***** Nom
***** Tonalitat i compàs
***** Vegades que s'ha tocat
* Referències * Referències
Fonts d'inspiració i altres materials de referència per fer la web de folkugat Fonts d'inspiració i altres materials de referència per fer la web de folkugat
** Simple Site ** 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 pytest
setuptools setuptools
ipython ipython
# To compile requirements
pip-tools
]; ];
projectDependencies = with pythonPackages; [ projectDependencies = with pythonPackages; [

View File

@@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse
from folkugat_web.api import router from folkugat_web.api import router
from folkugat_web.fragments import tema, temes from folkugat_web.fragments import tema, temes
from folkugat_web.services import auth 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 links as links_service
from folkugat_web.services.temes import lyrics as lyrics_service from folkugat_web.services.temes import lyrics as lyrics_service
from folkugat_web.services.temes import properties as properties_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}") @router.delete("/api/tema/{tema_id}")
def delete_tema(_: auth.RequireLogin, tema_id: int): def delete_tema(_: auth.RequireLogin, tema_id: int):
temes_w.delete_tema(tema_id=tema_id) temes_w.delete_tema(tema_id=tema_id)
files_service.clean_orphan_files()
return HTMLResponse(headers={ return HTMLResponse(headers={
'HX-Redirect': '/temes' 'HX-Redirect': '/temes'
}) })

View File

@@ -6,8 +6,10 @@ from fastapi.responses import HTMLResponse
from folkugat_web.api import router from folkugat_web.api import router
from folkugat_web.fragments.tema import links as links_fragments from folkugat_web.fragments.tema import links as links_fragments
from folkugat_web.model import temes as model 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 links as links_service
from folkugat_web.services.temes import query as temes_q
@router.get("/api/tema/{tema_id}/link/{link_id}") @router.get("/api/tema/{tema_id}/link/{link_id}")
@@ -30,7 +32,7 @@ async def set_link(
upload_file: Annotated[UploadFile | None, File()] = None, upload_file: Annotated[UploadFile | None, File()] = None,
): ):
if upload_file: 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 '') link_type = links_service.guess_link_type(url or '')
new_link = model.Link( new_link = model.Link(
@@ -66,6 +68,7 @@ def delete_link(
link_id: int, link_id: int,
): ):
links_service.delete_link(link_id=link_id, tema_id=tema_id) links_service.delete_link(link_id=link_id, tema_id=tema_id)
files_service.clean_orphan_files()
return HTMLResponse( return HTMLResponse(
headers={ headers={
"HX-Trigger": f"reload-tema-{tema_id}-score" "HX-Trigger": f"reload-tema-{tema_id}-score"
@@ -102,10 +105,14 @@ def get_score(
logged_in: auth.LoggedIn, logged_in: auth.LoggedIn,
tema_id: int, 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( return links_fragments.score(
request=request, request=request,
logged_in=logged_in, 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"> <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" class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3"
width="100" width="100"
alt="Folkugat"/> alt="Folkugat"/>

View File

@@ -1,6 +1,6 @@
<!-- PDF Viewer --> <!-- PDF Viewer -->
<script type="module"> <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 = { const options = {
url: '{{ pdf_url }}', url: '{{ pdf_url }}',
// PDF Canvas (where the pdf will be displayed) // PDF Canvas (where the pdf will be displayed)
@@ -24,13 +24,13 @@
<!-- </a> --> <!-- </a> -->
<!-- </div> --> <!-- </div> -->
<div class="flex flex-row flex-nowrap items-center justify-center w-full"> <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> <i class="fa fa-chevron-left" aria-hidden="true"></i>
</button> </button>
<canvas class="flex-auto m-2 w-1/2 max-w-3xl" <canvas class="flex-auto m-2 w-1/2 max-w-3xl"
id="the-canvas"> id="the-canvas">
</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> <i class="fa fa-chevron-right" aria-hidden="true"></i>
</button> </button>
</div> </div>

View File

@@ -14,15 +14,15 @@
referrerpolicy="no-referrer" /> referrerpolicy="no-referrer" />
<!-- Taiwind CSS --> <!-- 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 --> <!-- 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! --> <!-- Favicon! -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', path='favicon/apple-touch-icon.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('static', path='favicon/favicon-32x32.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('static', path='favicon/favicon-16x16.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"> <link rel="manifest" href="/site.webmanifest">
</head> </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.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 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): def score(request: Request, logged_in: bool, tema: model.Tema):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/score.html", "fragments/tema/score.html",
{ {

View File

@@ -90,4 +90,10 @@ class Tema:
return link.link_type is LinkType.IMAGE return link.link_type is LinkType.IMAGE
def score(self) -> Link | None: 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 mimetypes
import os
import re import re
import uuid import uuid
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
import magic import magic
from fastapi import HTTPException, UploadFile from fastapi import HTTPException, UploadFile
from folkugat_web.config import db 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: 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]: def list_files(tema_id: str) -> list[str]:
filedir = db.DB_FILES_DIR / str(tema_id) filedir = db.DB_FILES_DIR / str(tema_id)
return [get_db_file_path(f) for f in filedir.iterdir()] 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 import Connection
from folkugat_web.dal.sql._connection import get_connection from folkugat_web.dal.sql._connection import get_connection
from folkugat_web.dal.sql.playlists import query, write from folkugat_web.dal.sql.playlists import query, write
from folkugat_web.log import logger
from folkugat_web.model import playlists 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 from folkugat_web.services.temes import query as temes_query
def add_temes_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist: def add_temes_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist:
for set_ in playlist.sets: for set_ in playlist.sets:
add_temes_to_set(set_) _ = add_temes_to_set(set_)
return playlist return playlist
def add_temes_to_set(set_: playlists.Set) -> playlists.Set: def add_temes_to_set(set_: playlists.Set) -> playlists.Set:
for tema_in_set in set_.temes: 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_ return set_
def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaInSet: def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaInSet:
if tema_in_set.tema_id is not None: if tema_in_set.tema_id is not None:
tema_in_set.tema = temes_query.get_tema_by_id(tema_in_set.tema_id) 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 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]: def busca_temes(query: str, hidden: bool = False, limit: int = 10, offset: int = 0) -> list[model.Tema]:
t0 = time.time() t0 = time.time()
with get_connection() as con: with get_connection() as con:
result = ( result = (
FnChain.transform(temes_q.get_tema_id_to_ngrams(con).items()) | 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 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