Add Dockerfile, compose file and Gitea Actions workflow
build / check (push) Has been cancelled
build / image (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 15:29:44 -04:00
1 parent 8341c25846
commit b0480c8e56
5 files changed
+104 -12

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
.git
.gitea
__pycache__
config.json
users.json
sessions.json
progress*.json
*.tmp
README.md
+47
View File
@@ -0,0 +1,47 @@
name: build
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
env:
REGISTRY: git.necloud.us
IMAGE: git.necloud.us/nate/audiobook-player
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Syntax check
run: python3 -m py_compile server.py
- name: Check the page script parses
run: |
python3 - <<'PY'
import re
open("/tmp/page.js", "w").write(re.search(r"<script>(.*)</script>", open("index.html").read(), re.S).group(1))
PY
node --check /tmp/page.js
image:
needs: check
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to the Gitea container registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ gitea.actor }}" --password-stdin
- name: Build
run: docker build -t "$IMAGE:${{ gitea.sha }}" -t "$IMAGE:latest" .
- name: Smoke test (server refuses to start without accounts, but must import and print usage)
run: docker run --rm "$IMAGE:latest" python server.py users
- name: Push
run: |
docker push "$IMAGE:${{ gitea.sha }}"
docker push "$IMAGE:latest"
if [ "${{ gitea.ref_type }}" = "tag" ]; then
docker tag "$IMAGE:latest" "$IMAGE:${{ gitea.ref_name }}"
docker push "$IMAGE:${{ gitea.ref_name }}"
fi
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 DATA_DIR=/data PORT=8080
RUN useradd --system --uid 10001 --home /app app && mkdir /data && chown app /data
WORKDIR /app
COPY server.py index.html ./
USER app
VOLUME /data
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request,os;urllib.request.urlopen('http://127.0.0.1:%s/login'%os.environ['PORT'])" || exit 1
CMD ["python", "server.py", "serve"]
+17
View File
@@ -0,0 +1,17 @@
services:
audiobook-player:
image: git.necloud.us/nate/audiobook-player:latest
# build: . # uncomment to build locally instead of pulling
container_name: audiobook-player
restart: unless-stopped
ports:
- "8080:8080"
environment:
OPENAUDIBLE_URL: http://172.16.16.106:8000
OPENAUDIBLE_USER: ${OPENAUDIBLE_USER} # put these in a .env file next to this one
OPENAUDIBLE_PASSWORD: ${OPENAUDIBLE_PASSWORD}
volumes:
- player-data:/data # accounts, sessions, listening progress
volumes:
player-data:
+19 -12
View File
@@ -29,11 +29,12 @@ from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HERE = os.path.dirname(os.path.abspath(__file__))
F_CONFIG = os.path.join(HERE, "config.json") # OpenAudible login (chmod 600)
F_USERS = os.path.join(HERE, "users.json") # name -> salted scrypt hash
F_SESSIONS = os.path.join(HERE, "sessions.json") # token hash -> {user, exp}
F_PROGRESS = os.path.join(HERE, "progress_users.json")
F_LEGACY = os.path.join(HERE, "progress.json") # single-user file from the first version
DATA = os.environ.get("DATA_DIR", HERE) # where accounts/progress live (a volume in Docker)
F_CONFIG = os.path.join(DATA, "config.json") # OpenAudible login (chmod 600)
F_USERS = os.path.join(DATA, "users.json") # name -> salted scrypt hash
F_SESSIONS = os.path.join(DATA, "sessions.json") # token hash -> {user, exp}
F_PROGRESS = os.path.join(DATA, "progress_users.json")
F_LEGACY = os.path.join(DATA, "progress.json") # single-user file from the first version
COOKIE = "oa_session"
SESSION_DAYS = 90
@@ -79,6 +80,12 @@ def check_pw(password, rec):
return hmac.compare_digest(h, rec["hash"])
def oa_login():
"""OpenAudible credentials: env vars win over config.json (handy for containers)."""
cfg = read(F_CONFIG, {})
return os.environ.get("OPENAUDIBLE_USER") or cfg.get("user"), os.environ.get("OPENAUDIBLE_PASSWORD") or cfg.get("password", "")
def sha(token):
return hashlib.sha256(token.encode()).hexdigest()
@@ -272,10 +279,10 @@ class Handler(BaseHTTPRequestHandler):
self.end_headers()
def proxy(self):
cfg = read(F_CONFIG, {})
oa_user, oa_pass = oa_login()
req = urllib.request.Request(self.upstream + self.path)
if cfg.get("user"):
cred = base64.b64encode(f"{cfg['user']}:{cfg.get('password', '')}".encode()).decode()
if oa_user:
cred = base64.b64encode(f"{oa_user}:{oa_pass}".encode()).decode()
req.add_header("Authorization", "Basic " + cred)
for h in PASS_HEADERS:
if self.headers.get(h):
@@ -306,14 +313,14 @@ class Handler(BaseHTTPRequestHandler):
def serve(argv):
ap = argparse.ArgumentParser()
ap.add_argument("--upstream", default="http://172.16.16.106:8000")
ap.add_argument("--port", type=int, default=8080)
ap.add_argument("--upstream", default=os.environ.get("OPENAUDIBLE_URL", "http://172.16.16.106:8000"))
ap.add_argument("--port", type=int, default=int(os.environ.get("PORT", 8080)))
ap.add_argument("--host", default="0.0.0.0")
a = ap.parse_args(argv)
if not read(F_USERS, {}):
sys.exit("No accounts yet. Run: python3 server.py adduser YOURNAME")
if not read(F_CONFIG, {}).get("user"):
print("Note: no OpenAudible login saved. Run `python3 server.py setup` first.")
if not oa_login()[0]:
print("Note: no OpenAudible login set. Run `python3 server.py setup` or set OPENAUDIBLE_USER/OPENAUDIBLE_PASSWORD.")
Handler.upstream = a.upstream.rstrip("/")
print(f"Player: http://localhost:{a.port} (OpenAudible at {Handler.upstream})")
ThreadingHTTPServer((a.host, a.port), Handler).serve_forever()