#!/usr/bin/env python3
"""Static server for the podcasts.

Features:
- Correct MIME types (.m4a -> audio/x-m4a, .xml -> application/rss+xml, etc.)
- HTTP Range support (streaming/seeking + partial GETs from podcast clients)
- Concurrency limit on large media transfers: a burst of simultaneous
  downloads would otherwise saturate the home uplink, slow every transfer to
  a crawl, and make podcast players time out and fail ALL of them at once.
  We serve a few at full speed and tell the rest to retry shortly (503 +
  Retry-After), which podcast apps honor automatically.
- Quiet handling of client disconnects (no BrokenPipe tracebacks in the log).
"""
import os, threading, http.server, socketserver

ROOT = os.path.expanduser("~/Podcasts")  # shared root; one subfolder per show
PORT = 8090
MAX_CONCURRENT_MEDIA = 2   # audio downloads served simultaneously (Funnel shares
                           # its aggregate throughput, so keep this low so each
                           # active download stays fast and within client timeouts)
ACQUIRE_WAIT = 0.5         # seconds to wait for a free slot before saying "busy"
RETRY_AFTER = 5            # seconds we ask a busy client to back off and retry

EXT_MIME = {
    ".m4a": "audio/x-m4a",
    ".mp3": "audio/mpeg",
    ".xml": "application/rss+xml; charset=utf-8",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
}
MEDIA_EXTS = (".m4a", ".mp3")
_media_slots = threading.Semaphore(MAX_CONCURRENT_MEDIA)


class Handler(http.server.SimpleHTTPRequestHandler):
    timeout = 600  # don't let a stalled transfer hold a slot forever

    def __init__(self, *a, **k):
        super().__init__(*a, directory=ROOT, **k)

    def guess_type(self, path):
        ext = os.path.splitext(path)[1].lower()
        return EXT_MIME.get(ext, super().guess_type(path))

    def end_headers(self):
        self.send_header("Accept-Ranges", "bytes")
        self.send_header("Access-Control-Allow-Origin", "*")
        super().end_headers()

    def _is_media(self):
        return self.path.split("?")[0].lower().endswith(MEDIA_EXTS)

    def _send_busy(self):
        self.send_response(503)
        self.send_header("Retry-After", str(RETRY_AFTER))
        self.send_header("Content-Length", "0")
        self.end_headers()

    def do_GET(self):
        if self._is_media():
            if not _media_slots.acquire(timeout=ACQUIRE_WAIT):
                self._send_busy()
                return
            try:
                self._serve()
            finally:
                _media_slots.release()
        else:
            self._serve()

    def do_HEAD(self):
        try:
            super().do_HEAD()
        except (BrokenPipeError, ConnectionResetError):
            pass

    def _serve(self):
        rng = self.headers.get("Range")
        path = self.translate_path(self.path)
        try:
            if not rng or not os.path.isfile(path):
                super().do_GET()
                return
            size = os.path.getsize(path)
            _, _, rspec = rng.partition("=")
            start_s, _, end_s = rspec.partition("-")
            start = int(start_s) if start_s else 0
            end = int(end_s) if end_s else size - 1
            end = min(end, size - 1)
            if start > end or start >= size:
                self.send_response(416)
                self.send_header("Content-Range", f"bytes */{size}")
                self.end_headers()
                return
            length = end - start + 1
            self.send_response(206)
            self.send_header("Content-Type", self.guess_type(path))
            self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
            self.send_header("Content-Length", str(length))
            self.end_headers()
            with open(path, "rb") as f:
                f.seek(start)
                remaining = length
                while remaining > 0:
                    chunk = f.read(min(65536, remaining))
                    if not chunk:
                        break
                    self.wfile.write(chunk)
                    remaining -= len(chunk)
        except (BrokenPipeError, ConnectionResetError):
            pass  # client or Funnel disconnected mid-transfer; normal, ignore
        except Exception as e:
            try:
                self.send_error(500, str(e))
            except Exception:
                pass


class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
    daemon_threads = True
    request_queue_size = 128


if __name__ == "__main__":
    os.chdir(ROOT)
    with ThreadingServer(("127.0.0.1", PORT), Handler) as httpd:
        print(f"Serving {ROOT} on 127.0.0.1:{PORT} "
              f"(max {MAX_CONCURRENT_MEDIA} concurrent media downloads)")
        httpd.serve_forever()
