Compare commits

..
4 Commits
Author SHA1 Message Date
Andre a97ee9c41a fix: enable yt-dlp browser impersonation for TikTok
TikTok now answers requests without a real browser TLS fingerprint with a
page yt-dlp cannot parse, so every TikTok extraction failed at the metadata
step. yt-dlp can impersonate browsers, but only when curl_cffi is installed
on the interpreter it runs on, which the image never provided.
2026-09-12 11:23:24 +02:00
Andre 94965512fe fix: detect youtube shorts and live urls as video sources
Only the classic watch URL was recognised, so a shorts link fell through
to the website extractor. That path fetches the page, finds no recipe
markup in what is a JavaScript shell, and hands the raw text to the
language model, which returns an empty recipe titled "null" — stored in
the history as an apparently successful extraction rather than an error.

Shorts are among the most common formats for recipe clips, so the
failure mode was both frequent and silent. Live URLs share the pattern.
2026-08-14 16:32:27 +02:00
Andre 77c63770c3 fix: create data subdirectories on first container start
On a host that has never run the service, the mounted data volume is
empty. Ownership was being corrected for a subdirectory that did not
exist yet, which aborted the entrypoint before the application started
— exactly the situation a fresh deployment creates, and the reason the
container came up on the existing Pi but would not have elsewhere.

Creating the directory has to happen at runtime rather than during the
build, because the volume mount hides whatever the image places there.
2026-08-14 16:20:54 +02:00
Andre 790c744217 build: install ffmpeg and gosu as binaries instead of apt packages
Setting the service up on a new host took 20 to 30 minutes, spent almost
entirely in the Debian ffmpeg dependency chain: several hundred packages
of X11 and desktop libraries that dpkg unpacks and configures file by
file onto an SD card. That cost is paid again on every fresh machine and
turned moving the service into a half-hour commitment rather than a
routine step.

Prebuilt binaries remove the package manager from the build entirely. A
cold build with nothing cached now finishes in about four minutes on Pi
hardware, verified end to end including audio transcoding.

Pinning the dependency versions is unrelated to build speed. It prevents
a rebuild months from now from silently resolving versions other than
those the running container was tested with.
2026-08-14 16:20:49 +02:00
4 changed files with 39 additions and 29 deletions
+16 -16
View File
@@ -1,35 +1,35 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm FROM python:3.12-slim-bookworm
# Install system dependencies (ffmpeg required by yt-dlp for audio extraction) ARG TARGETARCH
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
curl \
gosu \
&& rm -rf /var/lib/apt/lists/*
# Install yt-dlp (standalone binary, always up-to-date) # ffmpeg + ffprobe as static binaries (yt-dlp needs both for audio extraction).
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp \ # The Debian package pulls in ~400 MB of X11/desktop dependencies that dpkg then
-o /usr/local/bin/yt-dlp && \ # unpacks file by file — on an SD card that alone dominates the build time.
chmod +x /usr/local/bin/yt-dlp COPY --from=mwader/static-ffmpeg:7.1 /ffmpeg /ffprobe /usr/local/bin/
# gosu drops privileges in the entrypoint; the binary avoids an apt transaction
ADD --chmod=755 https://github.com/tianon/gosu/releases/download/1.17/gosu-${TARGETARCH} /usr/local/bin/gosu
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN --mount=type=cache,target=/root/.cache/pip \
pip install --prefer-binary -r requirements.txt
# Placed after pip: the "latest" URL changes with every yt-dlp release and would
# otherwise invalidate the dependency layer on each build
ADD --chmod=755 https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp /usr/local/bin/yt-dlp
COPY --chmod=755 entrypoint.sh /entrypoint.sh
COPY . . COPY . .
RUN mkdir -p /app/tmp /app/data
ENV APP_HOST=0.0.0.0 ENV APP_HOST=0.0.0.0
ENV APP_PORT=8000 ENV APP_PORT=8000
ENV TMP_DIR=/app/tmp ENV TMP_DIR=/app/tmp
EXPOSE 8000 EXPOSE 8000
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
# Single worker to stay within Pi 3's memory budget # Single worker to stay within Pi 3's memory budget
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
+2
View File
@@ -3,6 +3,8 @@ import re
VIDEO_PATTERNS = [ VIDEO_PATTERNS = [
r"youtube\.com/watch", r"youtube\.com/watch",
r"youtube\.com/shorts/",
r"youtube\.com/live/",
r"youtu\.be/", r"youtu\.be/",
r"tiktok\.com/", r"tiktok\.com/",
r"instagram\.com/reel", r"instagram\.com/reel",
+3 -1
View File
@@ -4,7 +4,9 @@ set -e
APP_UID=${APP_UID:-1000} APP_UID=${APP_UID:-1000}
APP_GID=${APP_GID:-1000} APP_GID=${APP_GID:-1000}
# Fix ownership of mounted volumes so the app user can write to them # Mounted volumes start out empty on a fresh host, and a mount hides anything the
# image created at these paths — so create the directories here, not at build time
mkdir -p /app/tmp /app/data/images
chown "${APP_UID}:${APP_GID}" /app/tmp /app/data /app/data/images chown "${APP_UID}:${APP_GID}" /app/tmp /app/data /app/data/images
# Self-update yt-dlp on every container start (Instagram/TikTok break old versions). # Self-update yt-dlp on every container start (Instagram/TikTok break old versions).
+18 -12
View File
@@ -1,12 +1,18 @@
fastapi>=0.115 # Pinned to the versions verified on the Pi. Bump deliberately, not implicitly:
uvicorn>=0.30 # an unpinned rebuild months later resolves different versions than the running
pydantic>=2.9 # container and breaks in ways the diff cannot explain.
pydantic-settings>=2.5 fastapi==0.139.2
httpx>=0.27 uvicorn==0.51.0
python-multipart>=0.0.18 pydantic==2.13.4
recipe-scrapers>=15.0 pydantic-settings==2.14.2
beautifulsoup4>=4.12 httpx==0.28.1
lxml>=5.2 python-multipart==0.0.32
openai>=1.50 recipe-scrapers==15.11.0
python-dotenv>=1.0 beautifulsoup4==4.15.0
aiofiles>=23.0 lxml==6.1.1
openai==2.46.0
python-dotenv==1.2.2
aiofiles==25.1.0
# Not imported by the app: the yt-dlp binary runs on this interpreter and needs
# curl_cffi for browser impersonation, without which TikTok rejects requests
curl_cffi==0.16.3