Files
unshackle/dev/rest-api/remote-sessions/index.html
T
2026-10-24 02:23:34 +00:00

3303 lines
107 KiB
HTML

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Movie, TV, and Music archival software. User guide and REST API reference">
<link rel="canonical" href="https://docs.unshackle.dev/dev/rest-api/remote-sessions/">
<link rel="prev" href="../dashboard/">
<link rel="next" href="../errors/">
<link rel="icon" href="../../../assets/favicon.svg">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
<title>Remote Sessions - unshackle</title>
<link rel="stylesheet" href="../../../assets/stylesheets/main.ec1eaa64.min.css">
<link rel="stylesheet" href="../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<link rel="stylesheet" href="../../../assets/_mkdocstrings.css">
<link rel="stylesheet" href="../../../assets/extra.css">
<script>__md_scope=new URL("../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="black" data-md-color-accent="indigo">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#remote-service-sessions" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../../.." title="unshackle" class="md-header__button md-logo" aria-label="unshackle" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m800-274-80-80v-206H514l-80-80h166v-80q0-50-34.5-85T481-840q-50 0-84 34.5T363-720v9l-73-73q22-61 75-98.5T481-920q83 0 141 58.5T680-720v80h40q33 0 56.5 23.5T800-560v286Zm20 246-62-62q-11 5-20 7.5T720-80H240q-33 0-56.5-23.5T160-160v-400q0-25 14.5-46t37.5-30L28-820l56-56L876-84l-56 56ZM686-160 539-309q-11 11-25.5 17t-31.5 6q-33 0-56.5-23.5T402-366q0-17 6-31.5t17-25.5L286-560h-46v400h446ZM486-360Zm131-97Z"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
unshackle
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Remote Sessions
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="(prefers-color-scheme)" data-md-color-scheme="default" data-md-color-primary="black" data-md-color-accent="indigo" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1M8 13h8v-2H8zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4a5 5 0 0 0 5-5 5 5 0 0 0-5-5"/></svg>
</label>
<input class="md-option" data-md-color-media="(prefers-color-scheme: light)" data-md-color-scheme="default" data-md-color-primary="black" data-md-color-accent="indigo" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_2" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5m0 8a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3"/></svg>
</label>
<input class="md-option" data-md-color-media="(prefers-color-scheme: dark)" data-md-color-scheme="slate" data-md-color-primary="black" data-md-color-accent="indigo" aria-label="Switch to system preference" type="radio" name="__palette" id="__palette_2">
<label class="md-header__button md-icon" title="Switch to system preference" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5M7 15a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://github.com/unshackle-dl/unshackle" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
unshackle-dl/unshackle
</div>
</a>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../../../getting-started/installation/" class="md-tabs__link">
Getting Started
</a>
</li>
<li class="md-tabs__item">
<a href="../../../guide/downloading/" class="md-tabs__link">
User Guide
</a>
</li>
<li class="md-tabs__item">
<a href="../../../reference/configuration/" class="md-tabs__link">
Configuration
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../creating-a-service/" class="md-tabs__link">
Developer
</a>
</li>
<li class="md-tabs__item">
<a href="../../../reference/api/service/" class="md-tabs__link">
API Reference
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../.." title="unshackle" class="md-nav__button md-logo" aria-label="unshackle" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m800-274-80-80v-206H514l-80-80h166v-80q0-50-34.5-85T481-840q-50 0-84 34.5T363-720v9l-73-73q22-61 75-98.5T481-920q83 0 141 58.5T680-720v80h40q33 0 56.5 23.5T800-560v286Zm20 246-62-62q-11 5-20 7.5T720-80H240q-33 0-56.5-23.5T160-160v-400q0-25 14.5-46t37.5-30L28-820l56-56L876-84l-56 56ZM686-160 539-309q-11 11-25.5 17t-31.5 6q-33 0-56.5-23.5T402-366q0-17 6-31.5t17-25.5L286-560h-46v400h446ZM486-360Zm131-97Z"/></svg>
</a>
unshackle
</label>
<div class="md-nav__source">
<a href="https://github.com/unshackle-dl/unshackle" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
unshackle-dl/unshackle
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" >
<label class="md-nav__link" for="__nav_2" id="__nav_2_label" tabindex="0">
<span class="md-ellipsis">
Getting Started
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../getting-started/installation/" class="md-nav__link">
<span class="md-ellipsis">
Installation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../getting-started/quickstart/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../getting-started/configuration-file/" class="md-nav__link">
<span class="md-ellipsis">
The Configuration File
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_3" >
<label class="md-nav__link" for="__nav_3" id="__nav_3_label" tabindex="0">
<span class="md-ellipsis">
User Guide
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_3">
<span class="md-nav__icon md-icon"></span>
User Guide
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../guide/downloading/" class="md-nav__link">
<span class="md-ellipsis">
Downloading
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/cli-reference/" class="md-nav__link">
<span class="md-ellipsis">
CLI Reference
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/drm-and-cdm/" class="md-nav__link">
<span class="md-ellipsis">
DRM & CDM Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/proxies-and-vpn/" class="md-nav__link">
<span class="md-ellipsis">
Proxies & VPN
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/vaults/" class="md-nav__link">
<span class="md-ellipsis">
Key Vaults
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/output-and-naming/" class="md-nav__link">
<span class="md-ellipsis">
Output & Naming
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/subtitles/" class="md-nav__link">
<span class="md-ellipsis">
Subtitles
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../guide/troubleshooting/" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_4" >
<div class="md-nav__link md-nav__container">
<a href="../../../reference/configuration/" class="md-nav__link ">
<span class="md-ellipsis">
Configuration
</span>
</a>
<label class="md-nav__link " for="__nav_4" id="__nav_4_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_4">
<span class="md-nav__icon md-icon"></span>
Configuration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../reference/configuration/directories/" class="md-nav__link">
<span class="md-ellipsis">
Directories
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/services/" class="md-nav__link">
<span class="md-ellipsis">
Services & Authentication
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/download/" class="md-nav__link">
<span class="md-ellipsis">
Download & Processing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/output/" class="md-nav__link">
<span class="md-ellipsis">
Output & Naming
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/drm/" class="md-nav__link">
<span class="md-ellipsis">
DRM & CDM
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/network/" class="md-nav__link">
<span class="md-ellipsis">
Network & Proxy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/vaults/" class="md-nav__link">
<span class="md-ellipsis">
Key Vaults
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/post-scripts/" class="md-nav__link">
<span class="md-ellipsis">
Post-Scripts
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/misc/" class="md-nav__link">
<span class="md-ellipsis">
Other Settings
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/configuration/example/" class="md-nav__link">
<span class="md-ellipsis">
Annotated Example
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" checked>
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="">
<span class="md-ellipsis">
Developer
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Developer
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../creating-a-service/" class="md-nav__link">
<span class="md-ellipsis">
Creating a Service
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../service-architecture/" class="md-nav__link">
<span class="md-ellipsis">
Service Architecture
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5_3" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<span class="md-ellipsis">
REST API
</span>
</a>
<label class="md-nav__link " for="__nav_5_3" id="__nav_5_3_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_5_3_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_5_3">
<span class="md-nav__icon md-icon"></span>
REST API
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../quickstart/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../endpoints/" class="md-nav__link">
<span class="md-ellipsis">
Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../job-lifecycle/" class="md-nav__link">
<span class="md-ellipsis">
Job Lifecycle
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../dashboard/" class="md-nav__link">
<span class="md-ellipsis">
Dashboard
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Remote Sessions
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Remote Sessions
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#the-big-picture" class="md-nav__link">
<span class="md-ellipsis">
The big picture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuring-a-remote-server-client-side" class="md-nav__link">
<span class="md-ellipsis">
Configuring a remote server (client side)
</span>
</a>
<nav class="md-nav" aria-label="Configuring a remote server (client side)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#selecting-a-server" class="md-nav__link">
<span class="md-ellipsis">
Selecting a server
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#making-a-service-remote-by-default" class="md-nav__link">
<span class="md-ellipsis">
Making a service remote by default
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#what-happens-during-a-download" class="md-nav__link">
<span class="md-ellipsis">
What happens during a download
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#server-side-vs-proxied-cdm" class="md-nav__link">
<span class="md-ellipsis">
Server-side vs. proxied CDM
</span>
</a>
<nav class="md-nav" aria-label="Server-side vs. proxied CDM">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#your-own-device-identity" class="md-nav__link">
<span class="md-ellipsis">
Your own device identity
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#remote-session-lifecycle-and-expiry" class="md-nav__link">
<span class="md-ellipsis">
Remote session lifecycle and expiry
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security-properties-worth-knowing" class="md-nav__link">
<span class="md-ellipsis">
Security properties worth knowing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#developer-reference" class="md-nav__link">
<span class="md-ellipsis">
Developer reference
</span>
</a>
<nav class="md-nav" aria-label="Developer reference">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#remote-session-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Remote session endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#create-request-and-response" class="md-nav__link">
<span class="md-ellipsis">
Create: request and response
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#interactive-auth-the-inputbridge" class="md-nav__link">
<span class="md-ellipsis">
Interactive auth: the InputBridge
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sessionstore-internals" class="md-nav__link">
<span class="md-ellipsis">
SessionStore internals
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#per-session-cache-namespacing" class="md-nav__link">
<span class="md-ellipsis">
Per-session cache namespacing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#remote-session-info-response" class="md-nav__link">
<span class="md-ellipsis">
Remote session info response
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#reference-client-remoteservice" class="md-nav__link">
<span class="md-ellipsis">
Reference client: RemoteService
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../errors/" class="md-nav__link">
<span class="md-ellipsis">
Error Responses
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_6" >
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="0">
<span class="md-ellipsis">
API Reference
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
API Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../reference/api/service/" class="md-nav__link">
<span class="md-ellipsis">
Service
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/api/config/" class="md-nav__link">
<span class="md-ellipsis">
Config
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/api/tracks/" class="md-nav__link">
<span class="md-ellipsis">
Tracks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/api/titles/" class="md-nav__link">
<span class="md-ellipsis">
Titles
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../reference/api/drm/" class="md-nav__link">
<span class="md-ellipsis">
DRM
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#the-big-picture" class="md-nav__link">
<span class="md-ellipsis">
The big picture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuring-a-remote-server-client-side" class="md-nav__link">
<span class="md-ellipsis">
Configuring a remote server (client side)
</span>
</a>
<nav class="md-nav" aria-label="Configuring a remote server (client side)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#selecting-a-server" class="md-nav__link">
<span class="md-ellipsis">
Selecting a server
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#making-a-service-remote-by-default" class="md-nav__link">
<span class="md-ellipsis">
Making a service remote by default
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#what-happens-during-a-download" class="md-nav__link">
<span class="md-ellipsis">
What happens during a download
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#server-side-vs-proxied-cdm" class="md-nav__link">
<span class="md-ellipsis">
Server-side vs. proxied CDM
</span>
</a>
<nav class="md-nav" aria-label="Server-side vs. proxied CDM">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#your-own-device-identity" class="md-nav__link">
<span class="md-ellipsis">
Your own device identity
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#remote-session-lifecycle-and-expiry" class="md-nav__link">
<span class="md-ellipsis">
Remote session lifecycle and expiry
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security-properties-worth-knowing" class="md-nav__link">
<span class="md-ellipsis">
Security properties worth knowing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#developer-reference" class="md-nav__link">
<span class="md-ellipsis">
Developer reference
</span>
</a>
<nav class="md-nav" aria-label="Developer reference">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#remote-session-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Remote session endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#create-request-and-response" class="md-nav__link">
<span class="md-ellipsis">
Create: request and response
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#interactive-auth-the-inputbridge" class="md-nav__link">
<span class="md-ellipsis">
Interactive auth: the InputBridge
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#sessionstore-internals" class="md-nav__link">
<span class="md-ellipsis">
SessionStore internals
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#per-session-cache-namespacing" class="md-nav__link">
<span class="md-ellipsis">
Per-session cache namespacing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#remote-session-info-response" class="md-nav__link">
<span class="md-ellipsis">
Remote session info response
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#reference-client-remoteservice" class="md-nav__link">
<span class="md-ellipsis">
Reference client: RemoteService
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="remote-service-sessions">Remote Service Sessions<a class="headerlink" href="#remote-service-sessions" title="Permanent link">&para;</a></h1>
<p>A <strong>remote session</strong> lets one machine do the hard, service-specific work
(authenticating to a streaming service, listing titles, resolving tracks and
manifests, and, optionally, DRM licensing) while another machine runs the actual
download, decryption, and muxing locally.</p>
<p>This is the mechanism behind unshackle's client/server "remote download" workflow.
A thin local client (<code>RemoteService</code>) talks to an unshackle <code>serve</code> instance over
HTTP. The server keeps an authenticated service instance alive between requests so
the client can authenticate once and then make many follow-up calls against the
same remote session.</p>
<div class="admonition note">
<p class="admonition-title">Why you'd use this</p>
<ul>
<li>The server has service accounts, cookies, or a CDM you do not want to copy to
every machine.</li>
<li>You want to do downloads on a fast/local machine but keep credentials and
licensing centralised.</li>
<li>A service is region-locked to where the server lives, and the client is not.</li>
</ul>
</div>
<hr />
<h2 id="the-big-picture">The big picture<a class="headerlink" href="#the-big-picture" title="Permanent link">&para;</a></h2>
<div class="language-text highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>┌──────────────────────────┐ HTTP (X-Secret-Key) ┌──────────────────────────┐
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>│ Local machine (client) │ ───────────────────────────────▶ │ unshackle serve │
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a>│ │ │ │
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a>│ unshackle dl --server │ POST /api/session/create │ authenticated Service │
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a>│ │ GET /api/session/{id}/titles │ instance, kept alive in │
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a>│ • track selection │ POST /api/session/{id}/tracks │ an in-memory session │
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a>│ • download / decrypt │ POST /api/session/{id}/segments │ store (TTL-based) │
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a>│ • mux │ POST /api/session/{id}/license │ │
</span><span id="__span-0-9"><a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a>│ │ DELETE /api/session/{id} │ │
</span><span id="__span-0-10"><a id="__codelineno-0-10" name="__codelineno-0-10" href="#__codelineno-0-10"></a>└──────────────────────────┘ └──────────────────────────┘
</span></code></pre></div>
<p>The client implements the same interface <code>dl</code> expects from a normal service, but
every service-facing method proxies to the server. Everything after track
selection (downloading segments, decrypting, and muxing) runs locally.</p>
<div class="admonition note">
<p class="admonition-title">Migration: <code>remote_dl</code> is gone</p>
<p>The <code>RemoteService</code> adapter lives inside the ordinary <code>dl</code> command and replaces
the old standalone <code>remote_dl</code> command. If you have prior scripts or docs that
invoke <code>remote_dl</code>, use <code>dl</code> with a configured remote server instead. The
functionality now rides on <code>dl</code> through <code>RemoteService</code>.</p>
</div>
<p>For the auth-facing endpoints, header format, and error shapes referenced below,
see <a href="../authentication/">Authentication</a> and the endpoint reference in
<a href="../">the REST API index</a>.</p>
<hr />
<h2 id="configuring-a-remote-server-client-side">Configuring a remote server (client side)<a class="headerlink" href="#configuring-a-remote-server-client-side" title="Permanent link">&para;</a></h2>
<p>On the <strong>client</strong>, add a <code>remote_services</code> block to your <code>unshackle.yaml</code>. Each
entry names a server and provides its URL and API key.</p>
<div class="language-yaml highlight"><span class="filename">unshackle.yaml (client)</span><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="nt">remote_services</span><span class="p">:</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a><span class="w"> </span><span class="nt">my_server</span><span class="p">:</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="w"> </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s">"https://my-box.example:8786"</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a><span class="w"> </span><span class="nt">api_key</span><span class="p">:</span><span class="w"> </span><span class="s">"your-api-key"</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a><span class="w"> </span><span class="c1"># Optional: let the server's CDM do the licensing instead of a local CDM</span>
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6" href="#__codelineno-1-6"></a><span class="w"> </span><span class="nt">server_cdm</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">false</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7" href="#__codelineno-1-7"></a><span class="w"> </span><span class="c1"># Optional: per-service config overrides applied locally (title_map, cdm, etc.)</span>
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8" href="#__codelineno-1-8"></a><span class="w"> </span><span class="nt">services</span><span class="p">:</span>
</span><span id="__span-1-9"><a id="__codelineno-1-9" name="__codelineno-1-9" href="#__codelineno-1-9"></a><span class="w"> </span><span class="nt">EXAMPLE</span><span class="p">:</span>
</span><span id="__span-1-10"><a id="__codelineno-1-10" name="__codelineno-1-10" href="#__codelineno-1-10"></a><span class="w"> </span><span class="nt">title_map</span><span class="p">:</span>
</span><span id="__span-1-11"><a id="__codelineno-1-11" name="__codelineno-1-11" href="#__codelineno-1-11"></a><span class="w"> </span><span class="s">"0ABCDEF"</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">"The</span><span class="nv"> </span><span class="s">Show</span><span class="nv"> </span><span class="s">(Renamed</span><span class="nv"> </span><span class="s">Locally)"</span>
</span></code></pre></div>
<table>
<thead>
<tr>
<th>Key</th>
<th>Required</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>yes</td>
<td>Base URL of the <code>unshackle serve</code> instance (no trailing slash needed)</td>
</tr>
<tr>
<td><code>api_key</code></td>
<td>no</td>
<td>Sent as the <code>X-Secret-Key</code> header on every request</td>
</tr>
<tr>
<td><code>server_cdm</code></td>
<td>no</td>
<td>When <code>true</code>, DRM keys are resolved by the server's CDM rather than a local device (default <code>false</code>)</td>
</tr>
<tr>
<td><code>services</code></td>
<td>no</td>
<td>Per-service local config overrides, keyed by service tag</td>
</tr>
</tbody>
</table>
<div class="admonition warning">
<p class="admonition-title">Port default</p>
<p>The <code>unshackle serve</code> default port is <strong><code>8786</code></strong> (not <code>8080</code>). Use the port
your server binds. Some older example snippets show <code>8080</code>, which is
not the default.</p>
</div>
<h3 id="selecting-a-server">Selecting a server<a class="headerlink" href="#selecting-a-server" title="Permanent link">&para;</a></h3>
<p>If you configure exactly one server, unshackle uses it implicitly:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a>unshackle<span class="w"> </span>dl<span class="w"> </span>--remote<span class="w"> </span>EXAMPLE<span class="w"> </span>0ABCDEF
</span></code></pre></div>
<p>If you configure <strong>more than one</strong> server, you must pick one with <code>--server</code>:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a>unshackle<span class="w"> </span>dl<span class="w"> </span>--remote<span class="w"> </span>--server<span class="w"> </span>my_server<span class="w"> </span>EXAMPLE<span class="w"> </span>0ABCDEF
</span></code></pre></div>
<p>With no <code>remote_services</code> configured at all, unshackle raises a clear error telling
you to add the block shown above.</p>
<h3 id="making-a-service-remote-by-default">Making a service remote by default<a class="headerlink" href="#making-a-service-remote-by-default" title="Permanent link">&para;</a></h3>
<p>To run a service on the server without <code>--remote</code>, set <code>remote</code> in the
<a href="../../../reference/configuration/services/#services"><code>services</code></a> block, not in the
<code>services</code> sub-dict of <code>remote_services</code>. Add <code>server</code> to select one of several servers:</p>
<div class="language-yaml highlight"><span class="filename">unshackle.yaml (client)</span><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="nt">services</span><span class="p">:</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="w"> </span><span class="nt">EXAMPLE</span><span class="p">:</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span><span class="nt">dl</span><span class="p">:</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span><span class="nt">remote</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">my_server</span>
</span></code></pre></div>
<p><code>unshackle dl EXAMPLE 0ABCDEF</code> and <code>unshackle search EXAMPLE "query"</code> then run on
<code>my_server</code>. A <code>--server</code> on the command line overrides the configured server, and <code>--local</code>
runs the local service code for that run. The global
<code>dl: remote: true</code> makes every <code>dl</code> run remote, but it has no effect on <code>search</code>.
The REST API ignores both settings: a job runs remote only when its request asks for it.</p>
<hr />
<h2 id="what-happens-during-a-download">What happens during a download<a class="headerlink" href="#what-happens-during-a-download" title="Permanent link">&para;</a></h2>
<p>From your point of view the command looks like an ordinary download. Under the
hood the client walks a remote session through its lifecycle.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:5"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">1. Create + authenticate</label><label for="__tabbed_1_2">2. Interactive prompts (if needed)</label><label for="__tabbed_1_3">3. Titles + tracks</label><label for="__tabbed_1_4">4. Licensing</label><label for="__tabbed_1_5">5. Download + close</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>The client calls <code>POST /api/session/create</code>, forwarding whatever it can so the
server does not have to prompt:</p>
<ul>
<li>Local <strong>credentials</strong> for the service/profile (<code>{username, password, extra?}</code>)</li>
<li>Local <strong>cookies</strong>, compressed and base64-encoded</li>
<li>A resolved <strong>proxy</strong>, or, if you did not set one, your detected
<strong>client region</strong>. A client that reports no region is not blocked. The
proxy must be a full proxy URI unless the operator gives your API key
<code>server_proxy</code>. Without <code>server_proxy</code> the server does not resolve country
codes with its own proxy providers, and it rejects the remote session when
it sits in a different region from the reported one, asking the client to
pass <code>--proxy</code> with its own proxy, or <code>--no-proxy</code> to accept the server's
own connection. With <code>server_proxy</code> the server picks a proxy that matches
the client region itself. For <code>--proxy controld:ca</code> the client points its
Control D profile at the region and sends only the resolver, as
<code>controld://&lt;resolver&gt;@dns.controld.com</code>. The server counts it as a full proxy
URI and runs its own forwarder for it. A bare region skips Control D, because its
proxy is a forwarder on the client. See
<a href="../../../guide/proxies-and-vpn/#control-d">Control D</a>.
The client also downloads playlists and segments through the same proxy,
as a local run does. <code>--no-proxy-download</code> and <code>--proxy-download</code> change
that for downloads only.</li>
<li>A <code>--local-proxy</code> goes as <code>tunnel:&lt;id&gt;</code>. The client opens
<code>GET /api/tunnel</code> first, and the server replaces the ID with a loopback SOCKS5
proxy that relays to the client's own connection. See
<a href="../../../guide/proxies-and-vpn/#send-the-servers-requests-through-your-machine">Send the server's requests through your machine</a>.</li>
<li>Track-selection hints (<code>range_</code>, <code>vcodec</code>, <code>quality</code>, <code>best_available</code>,
<code>generate_dv</code>) so the server fetches the right manifests</li>
<li>Your language and audio codec selection (<code>-l</code>, <code>-vl</code>, <code>-al</code>, <code>-a</code>, <code>-fs</code>)
in a <code>dl_params</code> object. Some services fetch one manifest
for each audio language, and pick the languages from these values</li>
<li>Your local per-service <strong>cache files</strong> (e.g. refreshed tokens), including
files in subdirectories of the service cache directory, but only the files
for the active profile. The client withholds a file whose path, in any
directory or file name, embeds a hash of another credential or the name of
another profile. One profile's tokens
therefore never reach the server while you use a different profile. At
worst, a withheld file makes the server authenticate again</li>
</ul>
<p>The server uses its own accounts only when the operator lists the service in
<code>serve.server_accounts</code> and gives your API key <code>server_accounts</code> in <code>serve.users</code>.
For any other service, a client that sends no credentials or cookies authenticates
with nothing. For a listed service the client sends no credentials, cookies, or
cache at all: the server picks one of its own accounts that works in your region and
rotates through them. <code>--proxy ca</code> picks a <code>ca</code> account.</p>
<p><code>GET /api/services</code> advertises the regions those accounts cover. When your own
region is not one of them and you set no <code>--proxy</code>, the client resolves a proxy for
the first advertised region itself, so both sides sit in a region the account
works in. This is a bare region query, so it skips Control D. A server-account
service does not accept a <code>controld://</code> resolver from the client.</p>
<p>The server responds immediately with a session ID and a status. Authentication
runs in the background on the server.</p>
</div>
<div class="tabbed-block">
<p>Some services need a one-time code, PIN, or device confirmation. When the
server's auth flow asks for input, the remote session enters <code>pending_input</code>. The
client polls, displays the prompt to you locally, collects your answer, and
sends it back. The server's auth thread resumes with your response.</p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a>Enter the 6-digit code sent to your email: 483920
</span></code></pre></div>
<p>A client older than the <code>service_params</code> change sends service options as
flat body keys only. A flat key that shares a name with a transport field
(such as <code>profile</code>, which at the top level always means the credentials
profile) no longer reaches the service; the server logs a warning naming
the dropped key. Update the client to send service options under
<code>service_params</code>.</p>
<p>The server also mirrors the service's own log output (<code>self.log</code>) into a
per-session buffer. The client drains it through
<code>GET /api/session/{id}/logs</code> after each step and prints the records
locally, so you see the server-side reason when a step fails.</p>
</div>
<div class="tabbed-block">
<p>Once authenticated, the client fetches the title list, then the tracks for the
chosen title. The tracks come back with playback URLs, and the server ships
DASH/ISM manifests as compressed XML so the client can re-parse them locally for
downloading. A service can build an AdaptationSet of its own that the served
manifest does not contain. Each such track gets a one-AdaptationSet MPD in
<code>track_manifests</code>, and the client re-parses that instead. Any
<code>session_headers</code> and cookies the server used are merged into the
client's local HTTP session. Each cookie in <code>session_cookie_jar</code> keeps its
domain, path and <code>secure</code> flag, so it goes only to its own hosts. <code>session_cookies</code> carries
the same cookies as names and values only, for older clients.</p>
<p>The tracks response repeats the title. A service can rewrite title fields inside
<code>get_tracks</code>, so the client takes this copy in place of the one from the title
list. Each track carries the full local field set. After the manifest re-parse the
client overlays the JSON-safe part of the service-set <code>track.data</code>, so a service
value wins over the parser's. An attachment that the service wrote to a file on the
server travels as base64, and the client writes it to its own temp directory.</p>
</div>
<div class="tabbed-block">
<p>For a DRM-protected title the client either <strong>proxies</strong> its CDM challenge through
the server, or, if <code>server_cdm: true</code>, asks the server to do the full CDM flow
and return the content keys directly. See
<a href="#server-side-vs-proxied-cdm">Server-side vs. proxied CDM</a> below.</p>
</div>
<div class="tabbed-block">
<p>The client downloads, decrypts, and muxes locally. While it works, it sends a
keep-alive request to the remote session at an interval of one third of
<code>session_ttl</code>, so a long download does not let the remote session expire. On
completion, and also when <code>dl</code> exits early (<code>--list</code>, Ctrl+C, an error), it
deletes the remote session. If the login belongs to the client, the <code>DELETE</code>
request returns the updated cache files (for example a refreshed token) and the
client saves them locally, so the <strong>next</strong> remote session can skip interactive
authentication. The login belongs to the client when it succeeds and the
remote session does not use a server account. This includes a pair code that
the user approves on another device. A server-account login never does,
and neither does a remote session on the server's device (see below).
The client saves only the returned files that pass the same profile check it
applies before it sends them, so a file for another profile cannot overwrite
that profile's local file.</p>
</div>
</div>
</div>
<div class="admonition tip">
<p class="admonition-title">Renaming remote titles locally</p>
<p>You can rename titles for a remote service you do not have installed locally by
adding a <code>title_map</code> under that service in your client <code>remote_services.&lt;name&gt;.services</code>
config. The server sends raw titles. Your local map wins.</p>
<p>This is deliberate: the server does <strong>no</strong> <code>title_map</code> remapping of its own and
sends titles exactly as the service returns them. All remapping happens on the
client, applied to the titles the server sends back. It keeps the final output
name fully under the client's control and lets you rename titles even for
services you have no local install of.</p>
</div>
<hr />
<h2 id="server-side-vs-proxied-cdm">Server-side vs. proxied CDM<a class="headerlink" href="#server-side-vs-proxied-cdm" title="Permanent link">&para;</a></h2>
<p>There are two ways DRM keys get resolved, chosen by the client's <code>server_cdm</code> flag.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:2"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Proxied CDM (default)</label><label for="__tabbed_2_2">Server-side CDM (server_cdm: true)</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>Your <strong>local CDM</strong> builds the license challenge. The client sends that challenge
to <code>POST /api/session/{id}/license</code>, the server forwards it to the service, and
the raw license bytes come back for your local CDM to parse.</p>
<ul>
<li>Keeps your CDM local. The server only relays the license request.</li>
<li>Used when <code>server_cdm</code> is <code>false</code> (the default).</li>
<li>Also used for a run with <code>dl --cdm &lt;name&gt;</code>, and for a service whose own entry in
your <code>cdm</code> config named the loaded device. In each case the client sends
<code>server_cdm: false</code> when it creates the remote session, and the server builds the
service on your device even when the API key has the server CDM.</li>
<li>A service option that the service lists in <code>SERVER_DEVICE_OPTIONS</code> keeps the server
CDM. The server reads the option from <code>service_params</code>, then from the top level of
the request. The create response gives the result in <code>server_cdm</code>, and the client
shows a warning when the server did not apply its choice.</li>
<li>The service on the server sees your device, not the server's: its DRM system,
security level, system ID and device type. A CDM call that the service makes
(for example a session-key exchange at login) goes to your client as a <code>cdm_call</code>,
and your local CDM answers it. See <a href="#your-own-device-identity">Your own device identity</a>.</li>
</ul>
</div>
<div class="tabbed-block">
<p>The <strong>server's CDM</strong> does everything. The client sends track IDs (or a PSSH),
the server checks its key vaults, loads the device configured for your API key,
runs the CDM flow, and returns <code>KID:KEY</code> pairs directly. The client needs no
CDM.</p>
<ul>
<li>Enable with <code>server_cdm: true</code> in the server entry.</li>
<li>The server tells the client which DRM type it used
(<code>widevine</code> or <code>playready</code>).</li>
<li>A content key the server took from its own vault is unproven. The response
lists its KID in <code>vault_keys</code>, the client decrypts and checks that the output
decodes, and a wrong pair goes back to <code>POST /api/session/{id}/keys/bad</code>. The
server flags the pair in every local vault it holds, reports it to the vault
that served it, and the next licence for that KID skips the pair and reaches
the CDM. When the CDM returns the same pair, the server removes the flag,
because then the client's check was wrong. A server with no local SQLite
vault cannot store the flag. The server accepts a report only for a pair it
served to that remote session, and does not flag a pair its CDM licensed.
The client never learns the server vault names.</li>
<li>A content key in the client's own vaults goes first, even when the server
already answered. The client proves it, and a server key waits as the next
candidate.</li>
</ul>
</div>
</div>
</div>
<div class="admonition note">
<p class="admonition-title">A height limit on the server CDM</p>
<p>An API key with <code>server_cdm_max_height</code> gets the server CDM only up to that height. The
session create response tells the client who licenses the session in <code>server_cdm</code> and gives
the limit in <code>server_cdm_max_height</code>. In a server CDM session, the batch licence response
lists each track the server refused to license live under <code>capped_tracks</code>. A single-track
server CDM licence answers <code>403 SERVER_CDM_CAPPED</code> for such a track. In both cases the client
licenses those tracks in proxy mode with its own local CDM. See
<a href="../../../reference/configuration/services/"><code>server_cdm_max_height</code></a>.</p>
</div>
<h3 id="your-own-device-identity">Your own device identity<a class="headerlink" href="#your-own-device-identity" title="Permanent link">&para;</a></h3>
<p>A service can need more than a licence challenge from your device: an ESN, model keys, or a
session-key exchange that runs on the CDM. The device never leaves your machine. Only these cross
the wire:</p>
<ul>
<li><strong>Device facts.</strong> When it creates the remote session, the client sends its DRM system, security level,
system ID and device type. The service sees a stand-in with these facts.</li>
<li><strong>CDM calls.</strong> When the service calls the CDM, the server sends a <code>cdm_call</code> to the client
and waits. The client makes the challenge, or parses the licence, on its own CDM. When
the service closes a CDM session, the server sends a <code>close</code> call, because a CDM holds
only a few sessions. The client polls for calls during login and while one of its own
requests runs service code, so an idle remote session sends nothing extra.</li>
<li><strong>Session keys only.</strong> A <code>keys</code> call returns Widevine <code>OPERATOR_SESSION</code> and <code>SIGNING</code>
keys. It never returns a content key, and the client refuses a PlayReady <code>keys</code> call.
So a server cannot use your device to license titles for itself.</li>
<li><strong>Identity config.</strong> A service names the config keys a client may send (for example
<code>esn</code>, <code>Kpe</code>, <code>Kph</code>, <code>esn_map</code>). The server lists them in the service listing as
<code>client_config</code>. The client sends only those keys, from its own <code>services: &lt;TAG&gt;:</code>
block in <code>unshackle.yaml</code>. When the client sends an identity, the server drops its own
value for each of those keys, so the two identities never mix. A mapping (such as
<code>esn_map</code>) merges the client's entries over the server's. A client entry replaces a
server entry with the same key, also when the server's key is a number. The server keeps
these values in the remote session's memory only.</li>
</ul>
<p>A service can also offer an option that builds the remote session on the server's own
device and identity, for the quality that device reaches. The session then makes no live
licence at all: each content key comes from the server vault or the client's own vaults,
and a content key that no vault holds stops the download with an error. The create
response sets <code>server_device</code> to <code>true</code> for such a remote session. The service then uses
the server's own config: the client's identity values do not apply. The client reads the
server vault only when the API key has the <code>server_vault</code> grant (or <code>server_cdm</code>); without
it, the client reads only its own vaults. The login tokens of such a remote session hold the
server's device identity, so the server keeps them: <code>DELETE</code> returns no cache files, and the
HTTP session headers and <code>track.data</code> lose their secret-looking keys, as for a server login.</p>
<div class="admonition note">
<p class="admonition-title">The server vault in a session your device licenses</p>
<p>With the <code>server_vault</code> grant (or <code>server_cdm</code>) on the API key, a remote session that your own
device licenses still takes content keys from the server vault. The server tries the
vault first. A track that no server vault holds goes back to your device. See
<a href="../../../reference/configuration/services/"><code>server_vault</code></a>.</p>
</div>
<hr />
<h2 id="remote-session-lifecycle-and-expiry">Remote session lifecycle and expiry<a class="headerlink" href="#remote-session-lifecycle-and-expiry" title="Permanent link">&para;</a></h2>
<p>Remote sessions live in an <strong>in-memory store on the server</strong> and expire on a timer. You
generally never touch this directly, but it explains behaviour you might observe.</p>
<table>
<thead>
<tr>
<th>Behavior</th>
<th>Value</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Idle session TTL</td>
<td><strong>300s</strong> (5 min) default</td>
<td>Refreshed on every request to the session and when a <code>titles</code> or <code>tracks</code> request ends. A remote session does not expire while a request works on it</td>
</tr>
<tr>
<td>Max concurrent sessions</td>
<td><strong>100</strong> default</td>
<td>Oldest (least recently used) is evicted when full. A remote session that a request works on goes last</td>
</tr>
<tr>
<td>Auth/input timeout</td>
<td><strong>600s</strong> (10 min)</td>
<td>Sessions still authenticating or awaiting a prompt use this longer window instead of the TTL</td>
</tr>
<tr>
<td>Cleanup sweep</td>
<td>every 60s</td>
<td>Expired sessions are removed and their input prompts cancelled</td>
</tr>
</tbody>
</table>
<div class="admonition note">
<p class="admonition-title">Auth is never rushed</p>
<p>A remote session that is still <code>authenticating</code> or waiting on <code>pending_input</code> is
<strong>not</strong> subject to the short 300s TTL. It gets the full 600s auth window. This gives
you time to enter an interactive code without the remote session vanishing
mid-prompt.</p>
</div>
<p>The TTL and max-session limits are read from server config:</p>
<div class="language-yaml highlight"><span class="filename">unshackle.yaml (server)</span><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="nt">serve</span><span class="p">:</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a><span class="w"> </span><span class="nt">session_ttl</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">300</span><span class="w"> </span><span class="c1"># seconds a session may sit idle</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a><span class="w"> </span><span class="nt">max_sessions</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">100</span><span class="w"> </span><span class="c1"># cap on concurrent sessions</span>
</span></code></pre></div>
<hr />
<h2 id="security-properties-worth-knowing">Security properties worth knowing<a class="headerlink" href="#security-properties-worth-knowing" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>API key required.</strong> Every remote session request carries <code>X-Secret-Key</code>. The
server rejects requests without a valid API key (health check aside). See
<a href="../authentication/">Authentication</a>.</li>
<li><strong>IP binding.</strong> A remote session records the creator's IP when the server makes it.
If a later request for the same remote session comes from a different IP, the server
returns <code>403 FORBIDDEN</code>. Remote sessions are not portable between hosts.</li>
<li><strong>Namespaced, isolated cache.</strong> Each remote session gets its own cache directory,
namespaced by a hash of the API key and the remote session ID, so remote sessions
cannot read each other's cached tokens. The server deletes the directory when the
remote session ends. A remote session that uses a server account (<code>serve.server_accounts</code>)
gets a persistent per-account cache instead, so the account's tokens outlive the
remote session. The server and the client both refuse a forwarded cache file that is not
JSON or that names a type other than a date or time, and the service then authenticates again.</li>
<li><strong>A tunnel belongs to its API key.</strong> The server accepts <code>tunnel:&lt;id&gt;</code> only from the API key
that opened the tunnel. Its SOCKS5 proxy listens on loopback only, and it needs a username and
password for that tunnel, so a raw <code>socks5h://127.0.0.1:&lt;port&gt;</code> proxy from a client fails.</li>
<li><strong>Only the client dials.</strong> The server ignores a request from the client to open a
connection, so a tunnel never gives a client access to the server's network.</li>
<li><strong>The server redacts secrets in logs.</strong> It redacts remote session IDs, service tags,
and other user values before it writes them to the log.</li>
</ul>
<hr />
<h2 id="developer-reference">Developer reference<a class="headerlink" href="#developer-reference" title="Permanent link">&para;</a></h2>
<div class="admonition info">
<p class="admonition-title">This section is for developers</p>
<p>The material below documents the server-side implementation and the HTTP
contract. End users configuring a client can stop at the sections above.</p>
</div>
<h3 id="remote-session-endpoints">Remote session endpoints<a class="headerlink" href="#remote-session-endpoints" title="Permanent link">&para;</a></h3>
<p>The server mounts all these routes, even in <code>--remote-only</code> mode. Paths use the
<code>session_id</code> that <code>create</code> returns.</p>
<div class="admonition note">
<p class="admonition-title">Why <code>--remote-only</code> exists</p>
<p><code>--remote-only</code> narrows the server to only the health, services, search, and
remote session subset (and emits CORS headers), precisely so it is safe to sit behind
Cloudflare or serve cross-origin browser clients. That trimmed surface, rather
than the full <code>--api-only</code> mode, is what makes a CORS/Cloudflare-fronted
deployment practical: reach for it when the server is public-facing or accessed
from a browser origin, not when you want only the local HTTP API.</p>
</div>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/create</code></td>
<td>Build + authenticate a service instance; returns a session ID immediately</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/tunnel</code></td>
<td>WebSocket that relays the connections of a <code>tunnel:&lt;id&gt;</code> proxy to the client; <code>?id=</code> reconnects</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/tunnel/check?id=</code></td>
<td>Exit IP address and lookup time through the caller's tunnel, for <code>unshackle tunnel-test</code></td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/session/{id}/titles</code></td>
<td>Fetch and cache the title list</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/tracks</code></td>
<td>Resolve tracks, manifests, chapters, headers/cookies for a title</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/segments</code></td>
<td>Resolve per-segment/track download descriptors</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/segment_filter</code></td>
<td>Unwanted HLS segment URIs for one track (ads, bumpers)</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/samples</code></td>
<td>Decrypt the cbcs samples of a track the service decrypts itself</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/license</code></td>
<td>DRM licensing (proxy or server CDM)</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/keys/bad</code></td>
<td>Report a server-vault content key that did not decrypt</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/session/{id}/logs</code></td>
<td>Drain the service's server-side log output</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/session/{id}/prompt</code></td>
<td>Poll interactive auth status / pending prompt</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/api/session/{id}/prompt</code></td>
<td>Submit an answer to a pending prompt</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/api/session/{id}</code></td>
<td>Session info (validity, TTL, counts)</td>
</tr>
<tr>
<td><code>DELETE</code></td>
<td><code>/api/session/{id}</code></td>
<td>Close the remote session, return updated cache, clean up</td>
</tr>
</tbody>
</table>
<h3 id="create-request-and-response">Create: request and response<a class="headerlink" href="#create-request-and-response" title="Permanent link">&para;</a></h3>
<p><code>POST /api/session/create</code> requires <code>service</code> and <code>title_id</code>. It also accepts
<code>credentials</code>, <code>cookies</code> (base64 of zlib-compressed Netscape cookie file), <code>proxy</code>,
<code>no_proxy</code>, <code>profile</code>, <code>cache</code> (a map of <code>filename → base64(zlib(bytes))</code>),
<code>client_region</code>, <code>server_cdm</code> (<code>false</code> when the user picked the local CDM), <code>cdm_type</code>, <code>cdm_security_level</code>, <code>cdm_relay</code> (<code>true</code> when the client
answers CDM calls), <code>cdm_system_id</code> and <code>cdm_device_type</code> (Widevine only), <code>service_config</code>
(the identity config keys the service lists in <code>client_config</code>), <code>client</code> (a freeform identity
object the dashboard shows as sent), and the track-selection hints <code>range_</code>, <code>vcodec</code>,
<code>quality</code>, <code>best_available</code>, <code>generate_dv</code>, plus arbitrary service CLI options
(<code>additionalProperties: true</code>).</p>
<p><code>dl_params</code> holds the <code>dl</code> track selection: <code>lang</code>, <code>v_lang</code> and <code>a_lang</code>
(arrays of language strings), <code>acodec</code> (an array of audio codec names, such as
<code>EC3</code>), and <code>forced_subs</code> and <code>exact_lang</code> (booleans). The server gives these values to the
service in <code>ctx.parent.params</code>, never as service options, so a service option
with the same name, such as an own <code>--lang</code>, does not get them. The server uses
the <code>dl</code> default for each absent or malformed value: <code>lang</code> is <code>["orig"]</code>,
<code>v_lang</code>, <code>a_lang</code> and <code>acodec</code> are <code>[]</code>, and <code>forced_subs</code> and <code>exact_lang</code> are <code>false</code>. The
server drops an unknown codec name. An empty array is a valid value and stays
empty.</p>
<p>The server does not forward the <code>dl</code> mode flags: <code>video_only</code>, <code>audio_only</code>,
<code>subs_only</code>, <code>chapters_only</code>, <code>list_</code>, <code>skip_dl</code>, <code>sub_format</code>,
<code>no_attachments</code>, <code>audio_description</code>, <code>audio_description_only</code> and
<code>forced_subs_only</code>. A service that reads one of them to skip work fetches more on
the server, but the result is the same.</p>
<p>In a client-login session (not a server account), the server gives the service
the client's <code>profile</code> in <code>ctx.parent.params</code>, the same as a local run with that
profile. The profile name keys the service's token cache, so the cache files
the client sends and gets back belong to that profile. A service can also use
the name to select per-profile entries in the server's service config, such as
a per-profile ESN.</p>
<p>The response returns <strong>before</strong> authentication finishes:</p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">"session_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"...uuid4..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"EXAMPLE"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"authenticating"</span><span class="p">,</span>
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="w"> </span><span class="nt">"server_cdm"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nt">"server_cdm_max_height"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="nt">"server_vault"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nt">"server_device"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div>
<p><code>server_vault</code> tells the client that the server vault serves a remote session its own device
licenses. <code>server_device</code> tells it that the service runs on the server's device, so every
content key comes from a vault.</p>
<p>Authentication runs on a background thread (<code>asyncio.to_thread(authenticate, ...)</code>).
The remote session starts in <code>AUTHENTICATING</code>. The client must poll the prompt
endpoint until it reaches <code>authenticated</code> (or <code>failed</code>).</p>
<h3 id="interactive-auth-the-inputbridge">Interactive auth: the <code>InputBridge</code><a class="headerlink" href="#interactive-auth-the-inputbridge" title="Permanent link">&para;</a></h3>
<p>When the service calls its input function during <code>authenticate()</code> on the server
thread, an <code>InputBridge</code> pauses that thread and exposes the prompt to the HTTP
layer.</p>
<ul>
<li><code>AuthStatus</code> values: <code>authenticating</code>, <code>pending_input</code>, <code>authenticated</code>, <code>failed</code>.</li>
<li><code>InputBridge.request_input(prompt, timeout=600)</code> blocks the sync auth thread on a
<code>threading.Event</code> until <code>submit_response()</code> or <code>cancel()</code> fires.</li>
<li>A timeout raises <code>TimeoutError</code> and marks the remote session <code>FAILED</code>.</li>
<li>A prompt after authentication (in <code>get_titles()</code>, <code>get_tracks()</code> or licensing) raises
<code>RuntimeError</code> at once. The client polls for prompts only during authentication, so
no answer can come.</li>
<li>A CDM call (<code>InputBridge.request_cdm</code>) works during and after authentication, because the
client also polls while its own requests run. A call that no client fetches within 10 seconds
fails then, because no client is listening. A fetched call waits 60 seconds for its answer.
The call leaves the auth status as it was. An answer never marks the remote session as client-authenticated.</li>
<li><code>AUTH_INPUT_TIMEOUT = 600.0</code> seconds. This is also the TTL granted to
<code>AUTHENTICATING</code> / <code>PENDING_INPUT</code> sessions in the store.</li>
</ul>
<p><code>GET /api/session/{id}/prompt</code> returns one of:</p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"authenticated"</span><span class="w"> </span><span class="p">}</span>
</span><span id="__span-8-2"><a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a><span class="p">{</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"authenticating"</span><span class="w"> </span><span class="p">}</span>
</span><span id="__span-8-3"><a id="__codelineno-8-3" name="__codelineno-8-3" href="#__codelineno-8-3"></a><span class="p">{</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending_input"</span><span class="p">,</span><span class="w"> </span><span class="nt">"prompt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Enter code: "</span><span class="w"> </span><span class="p">}</span>
</span><span id="__span-8-4"><a id="__codelineno-8-4" name="__codelineno-8-4" href="#__codelineno-8-4"></a><span class="p">{</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending_input"</span><span class="p">,</span><span class="w"> </span><span class="nt">"prompt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"cdm_call"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"op"</span><span class="p">:</span><span class="w"> </span><span class="s2">"challenge"</span><span class="p">,</span><span class="w"> </span><span class="nt">"drm"</span><span class="p">:</span><span class="w"> </span><span class="s2">"widevine"</span><span class="p">,</span><span class="w"> </span><span class="nt">"init_data"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"license_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OFFLINE"</span><span class="p">,</span><span class="w"> </span><span class="nt">"privacy_mode"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nt">"service_certificate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span>
</span><span id="__span-8-5"><a id="__codelineno-8-5" name="__codelineno-8-5" href="#__codelineno-8-5"></a><span class="p">{</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"failed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"error"</span><span class="p">:</span><span class="w"> </span><span class="s2">"...message..."</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div>
<p>A <code>cdm_call</code> comes first, in any auth state. The client answers it with a JSON object in
<code>response</code>: <code>{"session": "...", "challenge": "&lt;base64&gt;"}</code> for <code>challenge</code>,
<code>{"keys": [{"kid", "type", "key", "permissions"}]}</code> for <code>keys</code>, <code>{}</code> for <code>close</code>, or
<code>{"error": "..."}</code> when its CDM refuses. An older client refuses <code>close</code>, and the server
ignores that refusal. An answered call stops being pending at once, so a client that polls again
straight away does not see it twice.</p>
<p>A missing remote session returns <code>404 SESSION_NOT_FOUND</code>. An IP mismatch returns
<code>403 FORBIDDEN</code>. <code>POST /api/session/{id}/prompt</code> takes <code>{ "response": "..." }</code> and
returns <code>{ "status": "accepted" }</code>. A post with no pending prompt is an
<code>INVALID_INPUT</code> error.</p>
<h3 id="sessionstore-internals"><code>SessionStore</code> internals<a class="headerlink" href="#sessionstore-internals" title="Permanent link">&para;</a></h3>
<p>Source: <code>unshackle/core/api/session_store.py</code>. A singleton you get from
<code>get_session_store()</code>.</p>
<ul>
<li><strong>Config-driven limits.</strong> <code>serve.session_ttl</code> (default <code>300</code>) and
<code>serve.max_sessions</code> (default <code>100</code>) are read as properties, so config changes
take effect without recreating the store.</li>
<li><strong><code>create()</code></strong> evicts the least-recently-accessed remote session when at capacity,
then stores a new <code>SessionEntry</code>. It defaults <code>auth_status</code> to <code>AUTHENTICATED</code>, and
the <code>create</code> handler overrides it to <code>AUTHENTICATING</code>.</li>
<li><strong><code>get()</code></strong> refreshes <code>last_accessed</code> through <code>touch()</code>. It returns <code>None</code> (and
deletes the entry) if an authenticated remote session has been idle longer than the
TTL. Remote sessions in <code>AUTHENTICATING</code> / <code>PENDING_INPUT</code> are exempt from TTL expiry.</li>
<li><strong><code>cleanup_expired()</code></strong> runs every 60s: authenticated remote sessions expire at
<code>ttl</code>, in-flight-auth remote sessions expire at <code>AUTH_INPUT_TIMEOUT</code>. The removal of
a remote session cancels its <code>InputBridge</code> and deletes its cache directory (pruning
empty parent dirs up to, but not including, the cache root).</li>
<li>The server calls <strong><code>cancel_all_bridges()</code></strong> on shutdown, to unblock any waiting
auth threads.</li>
</ul>
<p><code>SessionEntry</code> fields:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>session_id</code></td>
<td>UUID4 string</td>
</tr>
<tr>
<td><code>service_tag</code></td>
<td>Normalized service tag</td>
</tr>
<tr>
<td><code>service_instance</code></td>
<td>The authenticated service object kept alive between calls</td>
</tr>
<tr>
<td><code>titles</code> / <code>title_map</code></td>
<td>Result of <code>get_titles()</code> and a <code>title_id → Title</code> map</td>
</tr>
<tr>
<td><code>tracks</code> / <code>tracks_by_title</code> / <code>chapters_by_title</code></td>
<td>Cached resolved tracks and chapters</td>
</tr>
<tr>
<td><code>creator_ip</code></td>
<td>IP recorded at create time for IP-binding checks</td>
</tr>
<tr>
<td><code>cache_tag</code></td>
<td>Per-session cache directory tag</td>
</tr>
<tr>
<td><code>input_bridge</code></td>
<td><code>InputBridge</code> for interactive auth, if any</td>
</tr>
<tr>
<td><code>auth_status</code> / <code>auth_error</code></td>
<td>Current <code>AuthStatus</code> and last error message</td>
</tr>
<tr>
<td><code>created_at</code> / <code>last_accessed</code></td>
<td>Timestamps; <code>last_accessed</code> drives TTL and LRU eviction</td>
</tr>
</tbody>
</table>
<h3 id="per-session-cache-namespacing">Per-session cache namespacing<a class="headerlink" href="#per-session-cache-namespacing" title="Permanent link">&para;</a></h3>
<p>The <code>create</code> handler builds a <code>Cacher</code> namespaced as:</p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a>_sessions/&lt;pbkdf2_hmac(sha256, X-Secret-Key, "unshackle-session-ns", 100000)[:12]&gt;/&lt;session_id&gt;/&lt;service&gt;
</span></code></pre></div>
<p>The handler writes forwarded <code>cache</code> files into that directory before authentication.
On <code>DELETE</code>, when the login belongs to the client (<code>client_auth</code>) and the remote
session does not run on the server's device, the handler harvests updated cache files from the whole directory tree (compressing each
with zlib and base64-encoding, <strong>excluding</strong> <code>titles_*</code> files) and returns them
under a <code>cache</code> field, so the client can keep refreshed tokens. Each cache key
is the file path relative to that directory, with <code>/</code> separators and no <code>.json</code>
suffix, which is the <code>Cacher</code> cache key the service reads it with:</p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-10-1"><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="p">{</span>
</span><span id="__span-10-2"><a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span>
</span><span id="__span-10-3"><a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a><span class="w"> </span><span class="nt">"cache"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-10-4"><a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="w"> </span><span class="nt">"tokens"</span><span class="p">:</span><span class="w"> </span><span class="s2">"...base64(zlib(bytes))..."</span><span class="p">,</span>
</span><span id="__span-10-5"><a id="__codelineno-10-5" name="__codelineno-10-5" href="#__codelineno-10-5"></a><span class="w"> </span><span class="nt">"session_web/&lt;sha1&gt;"</span><span class="p">:</span><span class="w"> </span><span class="s2">"...base64(zlib(bytes))..."</span>
</span><span id="__span-10-6"><a id="__codelineno-10-6" name="__codelineno-10-6" href="#__codelineno-10-6"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-10-7"><a id="__codelineno-10-7" name="__codelineno-10-7" href="#__codelineno-10-7"></a><span class="p">}</span>
</span></code></pre></div>
<p>Both sides reject a cache key with a drive letter, a root, a <code>..</code> segment, an empty
segment, or more than 8 segments before it becomes a path, so a peer cannot write
outside the cache directory. Both sides also refuse a <code>cache</code> map with more than
64 entries. The server skips a cache key it cannot write, such as one that names an
existing file as a directory, and logs a warning.</p>
<h3 id="remote-session-info-response">Remote session info response<a class="headerlink" href="#remote-session-info-response" title="Permanent link">&para;</a></h3>
<p><code>GET /api/session/{id}</code>:</p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-11-1"><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a><span class="p">{</span>
</span><span id="__span-11-2"><a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a><span class="w"> </span><span class="nt">"session_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span>
</span><span id="__span-11-3"><a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a><span class="w"> </span><span class="nt">"service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"EXAMPLE"</span><span class="p">,</span>
</span><span id="__span-11-4"><a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a><span class="w"> </span><span class="nt">"valid"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-11-5"><a id="__codelineno-11-5" name="__codelineno-11-5" href="#__codelineno-11-5"></a><span class="w"> </span><span class="nt">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span>
</span><span id="__span-11-6"><a id="__codelineno-11-6" name="__codelineno-11-6" href="#__codelineno-11-6"></a><span class="w"> </span><span class="nt">"track_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
</span><span id="__span-11-7"><a id="__codelineno-11-7" name="__codelineno-11-7" href="#__codelineno-11-7"></a><span class="w"> </span><span class="nt">"title_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span>
</span><span id="__span-11-8"><a id="__codelineno-11-8" name="__codelineno-11-8" href="#__codelineno-11-8"></a><span class="p">}</span>
</span></code></pre></div>
<div class="admonition warning">
<p class="admonition-title"><code>expires_in</code> is the configured TTL, not time remaining</p>
<p>The <code>expires_in</code> value reports the store's configured <code>session_ttl</code>, not the
seconds left before this specific remote session expires.</p>
</div>
<h3 id="reference-client-remoteservice">Reference client: <code>RemoteService</code><a class="headerlink" href="#reference-client-remoteservice" title="Permanent link">&para;</a></h3>
<p>Source: <code>unshackle/core/remote_service.py</code>. This is the canonical consumer of the
remote session API and a good template for any client.</p>
<ul>
<li><strong><code>RemoteClient.request</code></strong> sets <code>X-Secret-Key</code> and <code>User-Agent: unshackle/&lt;version&gt;</code>,
uses the server's <code>timeout</code> from <code>remote_services</code> (default 120s) as the read timeout,
and treats any <code>status_code &gt;= 400</code> as fatal: it logs <code>Server error [&lt;error_code&gt;]: &lt;message&gt;</code>
and raises <code>SystemExit(1)</code>. A <code>200</code> body with <code>"status": "error"</code> is also fatal (see
the heartbeat below).</li>
<li><strong>Heartbeat.</strong> The <code>titles</code>, <code>tracks</code> and <code>license</code> routes send a newline every 30s while
the service works. The read timeout starts again at each byte, so a slow request stays
open for as long as the server works on it, and a reverse proxy does not close it as
idle. A route that answers within 30s sends the usual response. After 30s the status
is already <code>200</code>, so a failure after that time arrives as the usual error body with
status <code>200</code>. Your client must check <code>status</code> in the body. JSON parsers ignore the
leading newlines. The server does not gzip-compress a heartbeat response, because gzip
output cannot start before the body is ready.</li>
<li><strong>Retries.</strong> The download-side HTTP session mounts an adapter with
<code>Retry(total=5, backoff_factor=0.2, status_forcelist=[429, 500, 502, 503, 504])</code>.</li>
<li><strong>Flow.</strong> <code>authenticate()</code> → <code>create</code> (+ poll <code>prompt</code> every 2s up to a 600s
deadline, answering <code>pending_input</code> prompts. With stdin closed the client logs an
error and raises <code>SystemExit(1)</code> instead of posting an empty answer) → <code>get_titles()</code> → <code>get_tracks()</code>
(merging returned <code>session_headers</code>/<code>session_cookie_jar</code>, re-parsing <code>manifests</code>) →
get the licence in proxy mode or <code>server_cdm</code> mode → <code>close()</code> (<code>DELETE</code>, saving any returned
<code>cache</code>).</li>
<li><strong>Server resolution.</strong> <code>resolve_server()</code> reads <code>config.remote_services.&lt;name&gt;</code>
into <code>{url, api_key, services, server_cdm}</code>, injecting the <code>server_cdm</code> flag into
the services map as <code>_server_cdm</code>.</li>
</ul>
<hr />
<h2 id="troubleshooting">Troubleshooting<a class="headerlink" href="#troubleshooting" title="Permanent link">&para;</a></h2>
<div class="admonition example">
<p class="admonition-title">\"Could not connect to remote server at ...\"</p>
<p>The client could not reach the URL. Make sure that <code>unshackle serve</code> operates on
the server, the <code>url</code>/port in <code>remote_services</code> are correct (default port <code>8786</code>),
and any firewall or reverse proxy allows the connection.</p>
</div>
<div class="admonition example">
<p class="admonition-title"><code>403 FORBIDDEN</code> mid-download</p>
<p>The request came from a different IP than the one that made the remote session.
Remote sessions are IP-bound. Do not move between networks (or NAT egress IPs) during a
remote download.</p>
</div>
<div class="admonition example">
<p class="admonition-title">Auth times out or the prompt never resolves</p>
<p>Interactive auth allows up to 600s. If you miss that window, the server marks the
remote session <code>failed</code> and cleans it up. Operate the download again. If the server
never prompts you, make sure that the credentials and cookies the client forwarded
are valid for the service.</p>
</div>
<div class="admonition example">
<p class="admonition-title">\"Multiple remote services configured. Use --server ...\"</p>
<p>You have more than one entry under <code>remote_services</code>. Pass <code>--server &lt;name&gt;</code>
to pick one.</p>
</div>
</article>
</div>
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../dashboard/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Dashboard">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Dashboard
</div>
</div>
</a>
<a href="../errors/" class="md-footer__link md-footer__link--next" aria-label="Next: Error Responses">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Error Responses
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../..", "features": ["content.code.annotate", "content.code.copy", "content.tabs.link", "content.tooltips", "navigation.footer", "navigation.indexes", "navigation.sections", "navigation.tabs", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../assets/javascripts/bundle.d7400e89.min.js"></script>
</body>
</html>