Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
python-version: '3.12'

- name: Install dependencies
run: |
Expand All @@ -37,7 +37,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
python-version: '3.12'

- name: Install dependencies
run: |
Expand All @@ -46,7 +46,7 @@ jobs:

- name: Run tests with coverage
run: |
tox -e py311 -- --cov=src --cov-report=xml
tox -e py312 -- --cov=src --cov-report=xml

- name: Upload Coverage Report
uses: actions/upload-artifact@v4
Expand Down
29 changes: 26 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
FROM python:3.11-slim
# ---------- Stage 1: Builder (Debian-based) ----------
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

WORKDIR /app

# Copy lockfiles
COPY pyproject.toml uv.lock ./

# Install dependencies into .venv
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev

# ---------- Stage 2: Runtime (Debian-based) ----------
FROM python:3.12-slim

WORKDIR /app

# Copy the venv from the builder
COPY --from=builder /app/.venv /app/.venv

# Copy source code
COPY ./src /app/src

# Set environment variables
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONPATH=/app

# Ensure logs dir exists
RUN mkdir -p /app/logs && chmod -R 777 /app/logs
RUN pip install --no-cache-dir -r /app/src/requirements.txt

EXPOSE 8080

CMD ["python", "/app/src/__main__.py"]
CMD ["python", "src/__main__.py"]
49 changes: 38 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "musicapi"
version = "0.4.3"
version = "0.5.0"
description = "Web interface for automatic music donwload and tracking."
authors = [
{ name="fsdmu"}
Expand All @@ -10,7 +10,7 @@ maintainers = [
]
readme = "README.md"
license = { text = "GPL-3.0-or-later" }
requires-python = ">=3.11"
requires-python = ">=3.12"

dependencies = [
"aiofiles>=25.1.0",
Expand All @@ -36,28 +36,28 @@ dependencies = [
"idna>=3.11",
"ifaddr>=0.2.0",
"itsdangerous>=2.2.0",
"Jinja2>=3.1.6",
"jinja2>=3.1.6",
"markdown2>=2.5.4",
"MarkupSafe>=3.0.3",
"markupsafe>=3.0.3",
"multidict>=6.7.0",
"nicegui>=3.4.1",
"orjson>=3.11.5",
"propcache>=0.4.1",
"pydantic>=2.12.5",
"pydantic_core>=2.41.5",
"Pygments>=2.19.2",
"pydantic-core>=2.41.5",
"pygments>=2.19.2",
"python-dotenv>=1.2.1",
"python-engineio>=4.13.0",
"python-multipart>=0.0.21",
"python-socketio>=5.16.0",
"PyYAML>=6.0.3",
"pyyaml>=6.0.3",
"requests>=2.32.5",
"simple-websocket>=1.1.0",
"sniffio>=1.3.1",
"SQLAlchemy>=2.0.45",
"sqlalchemy>=2.0.45",
"starlette>=0.50.0",
"typing-inspection>=0.4.2",
"typing_extensions>=4.15.0",
"typing-extensions>=4.15.0",
"urllib3>=2.6.2",
"uvicorn>=0.40.0",
"uvloop>=0.22.1",
Expand All @@ -66,5 +66,32 @@ dependencies = [
"wsproto>=1.3.2",
"yarl>=1.22.0",
"ytmusicapi>=1.11.4",
"mysql-connector>=2.2.9"
]
"mysql-connector>=2.2.9",
"psycopg2-binary>=2.9.11",
]

[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = ["E", "W", "F", "I", "D", "UP"]
ignore = [
"E203",
"D212",
]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.uv.workspace]
members = [
"example",
]

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
108 changes: 92 additions & 16 deletions src/__main__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
"""User interface for the MusicAPi."""

import logging
import src.logging_config # noqa: F401
from nicegui import ui
import uuid

from src.ui.theme import apply_theme
from src.ui.components import SettingsDrawer, HelpDialog
from nicegui import app, ui
from starlette.requests import Request
from starlette.responses import Response

from src.logging.event_logger import log_event
from src.logging.log_database_connector import LogDatabaseConnector
from src.logging_config import setup_logging, stop_logging
from src.ui.components import HelpDialog, SettingsDrawer
from src.ui.logic import process_submission
from src.ui.theme import apply_theme

logger = logging.getLogger(__name__)
logger = logging.getLogger("app.music_api_ui")
logger.setLevel(logging.INFO)

latest_url_value = ""

THRESHOLD = 4


class MusicApiApp:
"""Class for the MusicAPI user interface."""

def __init__(self):
def __init__(self, session_id: str | None = None):
"""Initialize the MusicApiApp."""
apply_theme()

self.session_id = session_id

self.settings = SettingsDrawer()
self.help = HelpDialog()

Expand All @@ -31,6 +43,7 @@ def build_header(self):
ui.button(icon="help", on_click=self.help.open).props("round flat")
ui.button(icon="code", on_click=self.settings.toggle).props("round flat")

# noqa: D401
def build_main_content(self):
"""Build the main content for the MusicAPI user interface."""
with ui.column().classes(
Expand All @@ -54,22 +67,85 @@ def build_main_content(self):
.classes("mt-2")
)

ui.button("Submit", on_click=self.handle_click).props(
"color=#CB69C1"
).classes("pink-btn w-full h-[50px] font-bold text-lg mt-4")
# ensure submit handler logs and receives session_id
ui.button(
"Submit",
on_click=lambda: self.handle_click(session_id=self.session_id),
).props("color=#CB69C1").classes(
"pink-btn w-full h-[50px] font-bold text-lg mt-4"
)

@log_event("submit.click")
async def handle_click(self, session_id: str | None = None):
"""Handle a download submission. session_id is passed to logging wrapper.

async def handle_click(self):
"""Handle a download submission."""
Args:
session_id: The session ID for logging context.

"""
await process_submission(
self.url_input, self.auto_dl.value, self.settings.audio_format.value
self.url_input,
self.auto_dl.value,
self.settings.audio_format.value,
session_id=session_id,
)


@log_event("page.view")
def _log_page_view(session_id: str | None, user_agent: str, created: bool):
"""Emit a structured page.view event via the log_event wrapper.

Args:
session_id: The session ID for logging context.
user_agent: The client's user agent string.
created: Whether the session was newly created.

Returns:
A dict containing client and session information.

"""
return {
"client": {"user_agent": user_agent},
"session": {"id": session_id, "created": created},
}


@ui.page("/")
def main_page():
"""Start user interface for the MusicAPi."""
MusicApiApp()
def main_page(request: Request, response: Response) -> None:
"""Start user interface for the MusicAPi.

Args:
request: The incoming HTTP request.
response: The HTTP response to be sent.

"""
# create or reuse session id cookie
session_id = request.cookies.get("session_id")
created = False
if not session_id:
session_id = str(uuid.uuid4())
response.set_cookie(
"session_id",
session_id,
httponly=True,
samesite="lax",
max_age=400 * 24 * 3600,
)
created = True

user_agent = request.headers.get("user-agent", "")

_log_page_view(
session_id=session_id,
user_agent=user_agent,
created=created,
)

MusicApiApp(session_id=session_id)


app.on_shutdown(stop_logging)

if __name__ in {"__main__", "__mp_main__"}:
ui.run(host="0.0.0.0", port=8080, title="MusicAPI")
setup_logging(LogDatabaseConnector())
ui.run(host="0.0.0.0", port=8080, title="MusicAPI", uvicorn_logging_level="warning")
8 changes: 4 additions & 4 deletions src/auto_download_artists.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
"""Automatically download albums from artists marked for auto-download."""

import logging
import src.logging_config # initialize logging # noqa: F401

from src.youtube_handler.me_tube_connector import MeTubeConnector
import src.logging_config # initialize logging # noqa: F401
from src.database_connector import DatabaseConnector
from src.youtube_handler.me_tube_connector import MeTubeConnector
from src.youtube_handler.youtube_album_fetcher import YoutubeAlbumFetcher

logger = logging.getLogger(__name__)
logger = logging.getLogger("app.auto_download_artists")
logger.setLevel(logging.INFO)


def main():
"""Automatically download albums from artists marked for auto-download."""
"""Automatically downloads albums from artists marked for auto-download."""
db = DatabaseConnector()
mt = MeTubeConnector(base_url=None)

Expand Down
Loading