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
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
F_CONFIG = os.path.join(HERE, "config.json") # OpenAudible login (chmod 600)
|
DATA = os.environ.get("DATA_DIR", HERE) # where accounts/progress live (a volume in Docker)
|
||||||
F_USERS = os.path.join(HERE, "users.json") # name -> salted scrypt hash
|
F_CONFIG = os.path.join(DATA, "config.json") # OpenAudible login (chmod 600)
|
||||||
F_SESSIONS = os.path.join(HERE, "sessions.json") # token hash -> {user, exp}
|
F_USERS = os.path.join(DATA, "users.json") # name -> salted scrypt hash
|
||||||
F_PROGRESS = os.path.join(HERE, "progress_users.json")
|
F_SESSIONS = os.path.join(DATA, "sessions.json") # token hash -> {user, exp}
|
||||||
F_LEGACY = os.path.join(HERE, "progress.json") # single-user file from the first version
|
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"
|
COOKIE = "oa_session"
|
||||||
SESSION_DAYS = 90
|
SESSION_DAYS = 90
|
||||||
|
|
||||||
@@ -79,6 +80,12 @@ def check_pw(password, rec):
|
|||||||
return hmac.compare_digest(h, rec["hash"])
|
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):
|
def sha(token):
|
||||||
return hashlib.sha256(token.encode()).hexdigest()
|
return hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
@@ -272,10 +279,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def proxy(self):
|
def proxy(self):
|
||||||
cfg = read(F_CONFIG, {})
|
oa_user, oa_pass = oa_login()
|
||||||
req = urllib.request.Request(self.upstream + self.path)
|
req = urllib.request.Request(self.upstream + self.path)
|
||||||
if cfg.get("user"):
|
if oa_user:
|
||||||
cred = base64.b64encode(f"{cfg['user']}:{cfg.get('password', '')}".encode()).decode()
|
cred = base64.b64encode(f"{oa_user}:{oa_pass}".encode()).decode()
|
||||||
req.add_header("Authorization", "Basic " + cred)
|
req.add_header("Authorization", "Basic " + cred)
|
||||||
for h in PASS_HEADERS:
|
for h in PASS_HEADERS:
|
||||||
if self.headers.get(h):
|
if self.headers.get(h):
|
||||||
@@ -306,14 +313,14 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def serve(argv):
|
def serve(argv):
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--upstream", default="http://172.16.16.106:8000")
|
ap.add_argument("--upstream", default=os.environ.get("OPENAUDIBLE_URL", "http://172.16.16.106:8000"))
|
||||||
ap.add_argument("--port", type=int, default=8080)
|
ap.add_argument("--port", type=int, default=int(os.environ.get("PORT", 8080)))
|
||||||
ap.add_argument("--host", default="0.0.0.0")
|
ap.add_argument("--host", default="0.0.0.0")
|
||||||
a = ap.parse_args(argv)
|
a = ap.parse_args(argv)
|
||||||
if not read(F_USERS, {}):
|
if not read(F_USERS, {}):
|
||||||
sys.exit("No accounts yet. Run: python3 server.py adduser YOURNAME")
|
sys.exit("No accounts yet. Run: python3 server.py adduser YOURNAME")
|
||||||
if not read(F_CONFIG, {}).get("user"):
|
if not oa_login()[0]:
|
||||||
print("Note: no OpenAudible login saved. Run `python3 server.py setup` first.")
|
print("Note: no OpenAudible login set. Run `python3 server.py setup` or set OPENAUDIBLE_USER/OPENAUDIBLE_PASSWORD.")
|
||||||
Handler.upstream = a.upstream.rstrip("/")
|
Handler.upstream = a.upstream.rstrip("/")
|
||||||
print(f"Player: http://localhost:{a.port} (OpenAudible at {Handler.upstream})")
|
print(f"Player: http://localhost:{a.port} (OpenAudible at {Handler.upstream})")
|
||||||
ThreadingHTTPServer((a.host, a.port), Handler).serve_forever()
|
ThreadingHTTPServer((a.host, a.port), Handler).serve_forever()
|
||||||
|
|||||||
Reference in new issue
Block a user