Add Dockerfile, compose file and Gitea Actions workflow
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8341c25846
commit
b0480c8e56
5 files changed
+104
-12
No files matched your search
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
.gitea
|
||||
__pycache__
|
||||
config.json
|
||||
users.json
|
||||
sessions.json
|
||||
progress*.json
|
||||
*.tmp
|
||||
README.md
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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:
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user