forked from kenzuya/unshackle
252 lines
11 KiB
Python
252 lines
11 KiB
Python
import logging
|
|
import subprocess
|
|
|
|
import click
|
|
from aiohttp import web
|
|
|
|
from unshackle.core import binaries
|
|
from unshackle.core.api import cors_middleware, setup_routes, setup_swagger
|
|
from unshackle.core.config import config
|
|
from unshackle.core.constants import context_settings
|
|
|
|
|
|
@click.command(
|
|
short_help="Serve your Local Widevine/PlayReady Devices and REST API for Remote Access.",
|
|
context_settings=context_settings,
|
|
)
|
|
@click.option("-h", "--host", type=str, default="0.0.0.0", help="Host to serve from.")
|
|
@click.option("-p", "--port", type=int, default=8786, help="Port to serve from.")
|
|
@click.option("--caddy", is_flag=True, default=False, help="Also serve with Caddy.")
|
|
@click.option(
|
|
"--api-only", is_flag=True, default=False, help="Serve only the REST API, not pywidevine/pyplayready CDM."
|
|
)
|
|
@click.option("--no-widevine", is_flag=True, default=False, help="Disable Widevine CDM endpoints.")
|
|
@click.option("--no-playready", is_flag=True, default=False, help="Disable PlayReady CDM endpoints.")
|
|
@click.option("--no-key", is_flag=True, default=False, help="Disable API key authentication (allows all requests).")
|
|
@click.option(
|
|
"--debug-api",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Include technical debug information (tracebacks, stderr) in API error responses.",
|
|
)
|
|
@click.option(
|
|
"--debug",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Enable debug logging for API operations.",
|
|
)
|
|
def serve(
|
|
host: str,
|
|
port: int,
|
|
caddy: bool,
|
|
api_only: bool,
|
|
no_widevine: bool,
|
|
no_playready: bool,
|
|
no_key: bool,
|
|
debug_api: bool,
|
|
debug: bool,
|
|
) -> None:
|
|
"""
|
|
Serve your Local Widevine and PlayReady Devices and REST API for Remote Access.
|
|
|
|
\b
|
|
CDM ENDPOINTS:
|
|
- Widevine: /{device}/open, /{device}/close/{session_id}, etc.
|
|
- PlayReady: /playready/{device}/open, /playready/{device}/close/{session_id}, etc.
|
|
|
|
\b
|
|
You may serve with Caddy at the same time with --caddy. You can use Caddy
|
|
as a reverse-proxy to serve with HTTPS. The config used will be the Caddyfile
|
|
next to the unshackle config.
|
|
|
|
\b
|
|
DEVICE CONFIGURATION:
|
|
WVD files are auto-loaded from the WVDs directory, PRD files from the PRDs directory.
|
|
Configure user access in unshackle.yaml:
|
|
|
|
\b
|
|
serve:
|
|
api_secret: "your-api-secret"
|
|
users:
|
|
your-secret-key:
|
|
devices: ["device_name"] # Widevine devices
|
|
playready_devices: ["device_name"] # PlayReady devices
|
|
username: user
|
|
"""
|
|
from pyplayready.remote import serve as pyplayready_serve
|
|
from pywidevine import serve as pywidevine_serve
|
|
|
|
log = logging.getLogger("serve")
|
|
|
|
if debug:
|
|
logging.basicConfig(level=logging.DEBUG, format="%(name)s - %(levelname)s - %(message)s")
|
|
log.info("Debug logging enabled for API operations")
|
|
else:
|
|
logging.getLogger("api").setLevel(logging.WARNING)
|
|
logging.getLogger("api.remote").setLevel(logging.WARNING)
|
|
|
|
if not no_key:
|
|
api_secret = config.serve.get("api_secret")
|
|
if not api_secret:
|
|
raise click.ClickException(
|
|
"API secret key is not configured. Please add 'api_secret' to the 'serve' section in your config."
|
|
)
|
|
else:
|
|
api_secret = None
|
|
log.warning("Running with --no-key: Authentication is DISABLED for all API endpoints!")
|
|
|
|
if debug_api:
|
|
log.warning("Running with --debug-api: Error responses will include technical debug information!")
|
|
|
|
if api_only and (no_widevine or no_playready):
|
|
raise click.ClickException("Cannot use --api-only with --no-widevine or --no-playready.")
|
|
|
|
if caddy:
|
|
if not binaries.Caddy:
|
|
raise click.ClickException('Caddy executable "caddy" not found but is required for --caddy.')
|
|
caddy_p = subprocess.Popen(
|
|
[binaries.Caddy, "run", "--config", str(config.directories.user_configs / "Caddyfile")]
|
|
)
|
|
else:
|
|
caddy_p = None
|
|
|
|
try:
|
|
if not config.serve.get("devices"):
|
|
config.serve["devices"] = []
|
|
config.serve["devices"].extend(list(config.directories.wvds.glob("*.wvd")))
|
|
|
|
if not config.serve.get("playready_devices"):
|
|
config.serve["playready_devices"] = []
|
|
config.serve["playready_devices"].extend(list(config.directories.prds.glob("*.prd")))
|
|
|
|
if api_only:
|
|
log.info("Starting REST API server (pywidevine/pyplayready CDM disabled)")
|
|
if no_key:
|
|
app = web.Application(middlewares=[cors_middleware])
|
|
app["config"] = {"users": {}}
|
|
else:
|
|
app = web.Application(middlewares=[cors_middleware, pywidevine_serve.authentication])
|
|
app["config"] = {"users": {api_secret: {"devices": [], "username": "api_user"}}}
|
|
app["debug_api"] = debug_api
|
|
setup_routes(app)
|
|
setup_swagger(app)
|
|
log.info(f"REST API endpoints available at http://{host}:{port}/api/")
|
|
log.info(f"Swagger UI available at http://{host}:{port}/api/docs/")
|
|
log.info("(Press CTRL+C to quit)")
|
|
web.run_app(app, host=host, port=port, print=None)
|
|
else:
|
|
serve_widevine = not no_widevine
|
|
serve_playready = not no_playready
|
|
|
|
serve_config = dict(config.serve)
|
|
wvd_devices = serve_config.get("devices", []) if serve_widevine else []
|
|
prd_devices = serve_config.get("playready_devices", []) if serve_playready else []
|
|
|
|
cdm_parts = []
|
|
if serve_widevine:
|
|
cdm_parts.append("pywidevine CDM")
|
|
if serve_playready:
|
|
cdm_parts.append("pyplayready CDM")
|
|
log.info(f"Starting integrated server ({' + '.join(cdm_parts)} + REST API)")
|
|
|
|
wvd_device_names = [d.stem if hasattr(d, "stem") else str(d) for d in wvd_devices]
|
|
prd_device_names = [d.stem if hasattr(d, "stem") else str(d) for d in prd_devices]
|
|
|
|
if not serve_config.get("users") or not isinstance(serve_config["users"], dict):
|
|
serve_config["users"] = {}
|
|
|
|
if not no_key and api_secret not in serve_config["users"]:
|
|
serve_config["users"][api_secret] = {
|
|
"devices": wvd_device_names,
|
|
"playready_devices": prd_device_names,
|
|
"username": "api_user",
|
|
}
|
|
|
|
for user_key, user_config in serve_config["users"].items():
|
|
if "playready_devices" not in user_config:
|
|
# Require explicit PlayReady device access per user (default: no access).
|
|
user_config["playready_devices"] = []
|
|
log.warning(
|
|
f'User "{user_key}" has no "playready_devices" configured; PlayReady access disabled for this user. '
|
|
f"Available PlayReady devices: {prd_device_names}"
|
|
)
|
|
|
|
def create_serve_authentication(serve_playready_flag: bool):
|
|
@web.middleware
|
|
async def serve_authentication(request: web.Request, handler) -> web.Response:
|
|
if serve_playready_flag and request.path in ("/playready", "/playready/"):
|
|
response = await handler(request)
|
|
else:
|
|
response = await pywidevine_serve.authentication(request, handler)
|
|
|
|
if serve_playready_flag and request.path.startswith("/playready"):
|
|
from pyplayready import __version__ as pyplayready_version
|
|
response.headers["Server"] = f"https://git.gay/ready-dl/pyplayready serve v{pyplayready_version}"
|
|
|
|
return response
|
|
return serve_authentication
|
|
|
|
if no_key:
|
|
app = web.Application(middlewares=[cors_middleware])
|
|
else:
|
|
serve_auth = create_serve_authentication(serve_playready and bool(prd_devices))
|
|
app = web.Application(middlewares=[cors_middleware, serve_auth])
|
|
|
|
app["config"] = serve_config
|
|
app["debug_api"] = debug_api
|
|
|
|
if serve_widevine:
|
|
app.on_startup.append(pywidevine_serve._startup)
|
|
app.on_cleanup.append(pywidevine_serve._cleanup)
|
|
app.add_routes(pywidevine_serve.routes)
|
|
|
|
if serve_playready and prd_devices:
|
|
if no_key:
|
|
playready_app = web.Application()
|
|
else:
|
|
playready_app = web.Application(middlewares=[pyplayready_serve.authentication])
|
|
|
|
# PlayReady subapp config maps playready_devices to "devices" for pyplayready compatibility
|
|
playready_config = {
|
|
"devices": prd_devices,
|
|
"users": {
|
|
user_key: {
|
|
"devices": user_cfg.get("playready_devices", []),
|
|
"username": user_cfg.get("username", "user"),
|
|
}
|
|
for user_key, user_cfg in serve_config["users"].items()
|
|
}
|
|
if not no_key
|
|
else {},
|
|
}
|
|
playready_app["config"] = playready_config
|
|
playready_app.on_startup.append(pyplayready_serve._startup)
|
|
playready_app.on_cleanup.append(pyplayready_serve._cleanup)
|
|
playready_app.add_routes(pyplayready_serve.routes)
|
|
|
|
async def playready_ping(_: web.Request) -> web.Response:
|
|
from pyplayready import __version__ as pyplayready_version
|
|
response = web.json_response({"message": "OK"})
|
|
response.headers["Server"] = f"https://git.gay/ready-dl/pyplayready serve v{pyplayready_version}"
|
|
return response
|
|
|
|
app.router.add_route("*", "/playready", playready_ping)
|
|
|
|
app.add_subapp("/playready", playready_app)
|
|
log.info(f"PlayReady CDM endpoints available at http://{host}:{port}/playready/")
|
|
elif serve_playready:
|
|
log.info("No PlayReady devices found, skipping PlayReady CDM endpoints")
|
|
|
|
setup_routes(app)
|
|
setup_swagger(app)
|
|
|
|
if serve_widevine:
|
|
log.info(f"Widevine CDM endpoints available at http://{host}:{port}/{{device}}/open")
|
|
log.info(f"REST API endpoints available at http://{host}:{port}/api/")
|
|
log.info(f"Swagger UI available at http://{host}:{port}/api/docs/")
|
|
log.info("(Press CTRL+C to quit)")
|
|
web.run_app(app, host=host, port=port, print=None)
|
|
finally:
|
|
if caddy_p:
|
|
caddy_p.kill()
|