Deploy folkugat web
This commit is contained in:
15
README.org
15
README.org
@@ -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
8
build.sh
Executable 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
11
deploy/Dockerfile
Normal 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
11
deploy/requirements.in
Normal file
@@ -0,0 +1,11 @@
|
||||
# API
|
||||
fastapi
|
||||
python-multipart
|
||||
jinja2
|
||||
uvicorn
|
||||
# Files
|
||||
python-magic
|
||||
# Auth
|
||||
pyjwt
|
||||
# Search
|
||||
levenshtein
|
||||
48
deploy/requirements.txt
Normal file
48
deploy/requirements.txt
Normal 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
20
docker-compose.yml
Normal 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:
|
||||
@@ -22,6 +22,8 @@
|
||||
pytest
|
||||
setuptools
|
||||
ipython
|
||||
# To compile requirements
|
||||
pip-tools
|
||||
];
|
||||
|
||||
projectDependencies = with pythonPackages; [
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
folkugat_web/config/api.py
Normal file
3
folkugat_web/config/api.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import os
|
||||
|
||||
URL_SCHEME = os.getenv("URL_SCHEME", "http")
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()) |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user