Files
unshackle/dev/creating-a-service/index.html
T
2026-08-12 01:18:53 +00:00

3140 lines
140 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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/creating-a-service/">
<link rel="prev" href="../../reference/configuration/example/">
<link rel="next" href="../service-architecture/">
<link rel="icon" href="../../assets/favicon.svg">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
<title>Creating a Service - 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">
<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="#creating-a-service" 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">
Creating a Service
</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="./" 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/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 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">
Creating a Service
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Creating a Service
</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="#how-a-service-is-discovered" class="md-nav__link">
<span class="md-ellipsis">
How a service is discovered
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-service-base-class" class="md-nav__link">
<span class="md-ellipsis">
The Service base class
</span>
</a>
<nav class="md-nav" aria-label="The Service base class">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#class-variables" class="md-nav__link">
<span class="md-ellipsis">
Class variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#what-service__init__-wires-up" class="md-nav__link">
<span class="md-ellipsis">
What Service.__init__ wires up
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#the-methods-you-implement" class="md-nav__link">
<span class="md-ellipsis">
The methods you implement
</span>
</a>
<nav class="md-nav" aria-label="The methods you implement">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#cli-the-click-entry-point" class="md-nav__link">
<span class="md-ellipsis">
cli: the Click entry point
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#authenticate-optional-login" class="md-nav__link">
<span class="md-ellipsis">
authenticate: optional login
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get_titles-required" class="md-nav__link">
<span class="md-ellipsis">
get_titles: required
</span>
</a>
<nav class="md-nav" aria-label="get_titles: required">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#split-episodes" class="md-nav__link">
<span class="md-ellipsis">
Split episodes
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#get_tracks-required" class="md-nav__link">
<span class="md-ellipsis">
get_tracks: required
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get_chapters-required" class="md-nav__link">
<span class="md-ellipsis">
get_chapters: required
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#handling-drm" class="md-nav__link">
<span class="md-ellipsis">
Handling DRM
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#optional-event-hooks" class="md-nav__link">
<span class="md-ellipsis">
Optional event hooks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#minimal-service-skeleton" class="md-nav__link">
<span class="md-ellipsis">
Minimal service skeleton
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#checklist" class="md-nav__link">
<span class="md-ellipsis">
Checklist
</span>
</a>
</li>
</ul>
</nav>
</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--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5_3" >
<div class="md-nav__link md-nav__container">
<a href="../rest-api/" 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="false">
<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="../rest-api/quickstart/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../rest-api/authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../rest-api/endpoints/" class="md-nav__link">
<span class="md-ellipsis">
Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../rest-api/job-lifecycle/" class="md-nav__link">
<span class="md-ellipsis">
Job Lifecycle
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../rest-api/remote-sessions/" class="md-nav__link">
<span class="md-ellipsis">
Remote Sessions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../rest-api/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="#how-a-service-is-discovered" class="md-nav__link">
<span class="md-ellipsis">
How a service is discovered
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#the-service-base-class" class="md-nav__link">
<span class="md-ellipsis">
The Service base class
</span>
</a>
<nav class="md-nav" aria-label="The Service base class">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#class-variables" class="md-nav__link">
<span class="md-ellipsis">
Class variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#what-service__init__-wires-up" class="md-nav__link">
<span class="md-ellipsis">
What Service.__init__ wires up
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#the-methods-you-implement" class="md-nav__link">
<span class="md-ellipsis">
The methods you implement
</span>
</a>
<nav class="md-nav" aria-label="The methods you implement">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#cli-the-click-entry-point" class="md-nav__link">
<span class="md-ellipsis">
cli: the Click entry point
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#authenticate-optional-login" class="md-nav__link">
<span class="md-ellipsis">
authenticate: optional login
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get_titles-required" class="md-nav__link">
<span class="md-ellipsis">
get_titles: required
</span>
</a>
<nav class="md-nav" aria-label="get_titles: required">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#split-episodes" class="md-nav__link">
<span class="md-ellipsis">
Split episodes
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#get_tracks-required" class="md-nav__link">
<span class="md-ellipsis">
get_tracks: required
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get_chapters-required" class="md-nav__link">
<span class="md-ellipsis">
get_chapters: required
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#handling-drm" class="md-nav__link">
<span class="md-ellipsis">
Handling DRM
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#optional-event-hooks" class="md-nav__link">
<span class="md-ellipsis">
Optional event hooks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#minimal-service-skeleton" class="md-nav__link">
<span class="md-ellipsis">
Minimal service skeleton
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#checklist" class="md-nav__link">
<span class="md-ellipsis">
Checklist
</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="creating-a-service">Creating a Service<a class="headerlink" href="#creating-a-service" title="Permanent link">&para;</a></h1>
<p>A <strong>service</strong> is a plugin that teaches unshackle how to talk to one streaming
provider: how to log in, how to look up a title, what video/audio/subtitle
tracks exist, and how to license any DRM. Everything else (track selection,
downloading, decryption, muxing, naming) is handled by the unshackle core.</p>
<div class="admonition info">
<p class="admonition-title">Who this page is for</p>
<p>This is a <strong>developer guide</strong>. If you only want to <em>use</em> services, see
<a href="../../guide/downloading/">downloading</a> and
<a href="../../reference/configuration/">configuration</a>. Nothing here is required to run
the CLI. Service code is deliberately kept separate from the core so it can
live in its own private repository.</p>
</div>
<p>Unshackle ships one reference service, <strong>EXAMPLE</strong>, at
<code>unshackle/services/EXAMPLE/</code>. It is a deliberately exhaustive, non-runnable
showcase of every framework feature in a single file. Read it alongside this
page; most snippets below are drawn from it.</p>
<hr />
<h2 id="how-a-service-is-discovered">How a service is discovered<a class="headerlink" href="#how-a-service-is-discovered" title="Permanent link">&para;</a></h2>
<p>At startup unshackle scans every path in the <code>directories.services</code> config key
(a <strong>list</strong>). Each entry is either a local directory of services or a remote
repo spec (a git URL or <code>owner/repo</code> shorthand, which is cloned automatically).
The default is the bundled <code>unshackle/services</code> directory.</p>
<p>Within a services directory, <strong>every subfolder that contains an <code>__init__.py</code>
is a service</strong>, and the folder name is the service <strong>tag</strong>:</p>
<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>services/
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>└── EXAMPLE/
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> ├── __init__.py # required - defines the service class
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a> └── config.yaml # optional - per-service configuration
</span></code></pre></div>
<p>Two rules the loader enforces:</p>
<ol>
<li><strong>The class name must exactly match the folder/tag name.</strong> Folder <code>EXAMPLE</code> must
define <code>class EXAMPLE</code>. A mismatch raises a <code>RuntimeError</code>.</li>
<li><strong>List order is priority.</strong> The first services path to define a given tag
wins; later duplicates are shadowed. Put your own local overrides <em>last</em> to
use them as fallbacks, or <em>first</em> to override a repo.</li>
</ol>
<p>Once installed, a service is invoked as a subcommand of <code>dl</code>:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a>unshackle<span class="w"> </span>dl<span class="w"> </span>EXAMPLE<span class="w"> </span><span class="m">20914</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a>unshackle<span class="w"> </span>dl<span class="w"> </span>EX<span class="w"> </span><span class="m">20914</span><span class="w"> </span><span class="c1"># via an ALIAS</span>
</span></code></pre></div>
<div class="admonition tip">
<p class="admonition-title">Config lives next to the code</p>
<p>A service's own settings go in <code>config.yaml</code> <strong>inside the service folder</strong>
(<code>directories.services/&lt;TAG&gt;/config.yaml</code>). This is separate from the global
<code>unshackle.yaml</code>. Access it in code as <code>self.config[...]</code>. Never hardcode
URLs, user agents, or certificates; put them here.</p>
</div>
<div class="admonition warning">
<p class="admonition-title"><code>config.yaml</code> is shared: keep secrets out of it</p>
<p>Treat <code>config.yaml</code> as a checked-in file of <strong>defaults</strong>: it belongs in the
service repo and is shared by everyone who runs the service. Per-user secrets
(API keys, device IDs, account tokens) must <strong>not</strong> live here. Those go in the
user's own <code>unshackle.yaml</code> under <code>services.&lt;TAG&gt;</code>, which is <strong>merged into
<code>self.config</code> at runtime</strong>. That checked-in-defaults / per-user-overrides
split is exactly why every URL lives in <code>config.yaml</code> with <code>{}</code>/<code>{name}</code>
placeholders while the secrets that fill them stay out of it. Both halves
read back through the same <code>self.config[...]</code>.</p>
</div>
<div class="admonition warning">
<p class="admonition-title">Split across files? Use relative imports</p>
<p>If your service grows beyond a single <code>__init__.py</code>, import your own
modules <strong>relatively</strong>:</p>
<div class="language-python highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">.helpers</span><span class="w"> </span><span class="kn">import</span> <span class="n">parse_manifest</span> <span class="c1"># correct</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">unshackle.services.TAG.helpers</span><span class="w"> </span><span class="kn">import</span> <span class="n">parse_manifest</span> <span class="c1"># breaks from a repo</span>
</span></code></pre></div>
<p>A service loaded from a remote repo lives outside the <code>unshackle</code> package,
so the absolute <code>unshackle.services.&lt;TAG&gt;.*</code> path does not resolve and the
import fails with <code>no known parent package</code>. Relative imports work both
locally and from a cloned repo.</p>
</div>
<hr />
<h2 id="the-service-base-class">The Service base class<a class="headerlink" href="#the-service-base-class" title="Permanent link">&para;</a></h2>
<p>Every service subclasses <code>unshackle.core.service.Service</code>. The minimum is a
Click <code>cli</code> entry point plus three abstract methods.</p>
<h3 id="class-variables">Class variables<a class="headerlink" href="#class-variables" title="Permanent link">&para;</a></h3>
<p>Declared at class level to configure framework behaviour:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ALIASES</code></td>
<td><code>tuple[str, ...]</code></td>
<td>Extra tags that resolve to this service (e.g. <code>("EX", "DOMAIN")</code>). Default <code>()</code>.</td>
</tr>
<tr>
<td><code>GEOFENCE</code></td>
<td><code>tuple[str, ...]</code></td>
<td>IP region codes the service requires. Empty = no geofence. The <strong>first</strong> entry is treated as the main region for auto-proxy.</td>
</tr>
<tr>
<td><code>VAULT_TAG</code></td>
<td><code>Optional[str]</code></td>
<td>Overrides the key-vault namespace so sibling services can share one vault. Default <code>None</code> (use the service's own tag).</td>
</tr>
<tr>
<td><code>AUTH_METHODS</code></td>
<td><code>Optional[tuple[str, ...]]</code></td>
<td>Auth methods accepted (<code>"cookies"</code> / <code>"credentials"</code>). When <code>None</code>, the REST <code>/services</code> endpoint infers them from <code>authenticate()</code>.</td>
</tr>
<tr>
<td><code>NO_SUBTITLES</code></td>
<td><code>bool</code></td>
<td>Set <code>True</code> on a service with no subtitle tracks to skip subtitle handling entirely.</td>
</tr>
<tr>
<td><code>ANIME</code></td>
<td><code>bool</code></td>
<td>Set <code>True</code> when the catalog is anime, so metadata lookups prefer AniList. A title's own <code>anime</code> flag overrides it.</td>
</tr>
<tr>
<td><code>DAILY</code></td>
<td><code>bool</code></td>
<td>Set <code>True</code> when the catalog is daily/date-based (talk shows, news, sports), so episodes are named by air date. A title's own <code>daily</code> flag overrides it.</td>
</tr>
</tbody>
</table>
<div class="admonition note">
<p class="admonition-title"><code>NO_SUBTITLES</code> is a convention, not a base-class attribute</p>
<p><code>NO_SUBTITLES</code> is checked by <code>dl.py</code> via <code>hasattr</code> and is deliberately <strong>not</strong>
declared on the <code>Service</code> base class, so it only exists if your service
defines it. Setting <code>NO_SUBTITLES = True</code> is what makes the pipeline skip
subtitle work for services that never carry subtitles; leave it off otherwise.</p>
</div>
<p><code>GEOFENCE</code> drives an automatic proxy: on startup, if you did not pass an
explicit <code>--proxy</code>, the base class does a live IP check and, if your region is
blocked, fetches a proxy to <code>GEOFENCE[0]</code> from your configured proxy providers.</p>
<div class="language-python highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="k">class</span><span class="w"> </span><span class="nc">EXAMPLE</span><span class="p">(</span><span class="n">Service</span><span class="p">):</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a> <span class="n">ALIASES</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&quot;EX&quot;</span><span class="p">,</span> <span class="s2">&quot;DOMAIN&quot;</span><span class="p">)</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a> <span class="n">GEOFENCE</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&quot;US&quot;</span><span class="p">,</span> <span class="s2">&quot;UK&quot;</span><span class="p">)</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a> <span class="n">VAULT_TAG</span> <span class="o">=</span> <span class="s2">&quot;DIFFERENT_NAME&quot;</span>
</span></code></pre></div>
<h3 id="what-service__init__-wires-up">What <code>Service.__init__</code> wires up<a class="headerlink" href="#what-service__init__-wires-up" title="Permanent link">&para;</a></h3>
<p>When you call <code>super().__init__(ctx)</code>, the base class sets up the attributes you
will use throughout the service:</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>What it is</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>self.config</code></td>
<td>This service's <code>config.yaml</code> contents (a dict), or <code>None</code> if absent.</td>
</tr>
<tr>
<td><code>self.log</code></td>
<td>A <code>logging.Logger</code> named after your class.</td>
</tr>
<tr>
<td><code>self.session</code></td>
<td>A prepared <code>requests.Session</code> with config headers and a retry adapter (5 retries, backoff 0.2, retries on 429/500/502/503/504).</td>
</tr>
<tr>
<td><code>self.cache</code></td>
<td>A <code>Cacher</code> for arbitrary key/value data (tokens, etc.).</td>
</tr>
<tr>
<td><code>self.title_cache</code></td>
<td>A region/account-aware <code>TitleCacher</code> used by <code>get_titles_cached</code>.</td>
</tr>
<tr>
<td><code>self.cache_dir</code></td>
<td><code>config.directories.cache / &lt;ClassName&gt;</code>.</td>
</tr>
<tr>
<td><code>self.track_request</code></td>
<td>A <code>TrackRequest</code> built from the CLI <code>--vcodec</code> / <code>--range</code> / <code>--best-available</code> flags.</td>
</tr>
<tr>
<td><code>self.credential</code></td>
<td><code>None</code> until <code>authenticate()</code> runs.</td>
</tr>
<tr>
<td><code>self.current_region</code></td>
<td>The two-letter region resolved from proxy/IP.</td>
</tr>
</tbody>
</table>
<p><code>self.track_request</code> is a small dataclass you can read <em>and rewrite</em> before
fetching tracks:</p>
<div class="language-python highlight"><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="nd">@dataclass</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="k">class</span><span class="w"> </span><span class="nc">TrackRequest</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="n">codecs</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Video</span><span class="o">.</span><span class="n">Codec</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span> <span class="c1"># empty == accept any codec</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a> <span class="n">ranges</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Video</span><span class="o">.</span><span class="n">Range</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">Video</span><span class="o">.</span><span class="n">Range</span><span class="o">.</span><span class="n">SDR</span><span class="p">]</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a> <span class="n">best_available</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span>
</span></code></pre></div>
<div class="admonition note">
<p class="admonition-title">Read the request, don't second-guess the user</p>
<p>Services may narrow <code>track_request</code> for hard technical constraints (e.g.
<em>"HDR on this service is only delivered as HEVC"</em>), but must <strong>not</strong> filter
the tracks they return by resolution, bitrate, or language. All selection is
done by the core after <code>get_tracks</code> returns.</p>
</div>
<div class="admonition tip">
<p class="admonition-title">Override <code>get_session()</code> to defeat TLS-fingerprint bot detection</p>
<p>The base <code>get_session()</code> returns a plain <code>requests.Session</code> (config headers +
retry adapter) and <strong>deliberately does not</strong> do TLS impersonation. When a
provider blocks you on TLS fingerprint alone, override it to return
<code>session("Chrome131")</code> (any <code>rnet.Impersonate</code> preset). <code>RnetSession</code> is a
drop-in <code>requests.Session</code> replacement (same <code>.get</code>/<code>.post</code>/cookie-jar
interface), so nothing else in the service changes. If instead you need
specific SSL <em>cipher</em> behaviour, mount
<code>unshackle.core.utils.sslciphers.SSLCiphers</code> onto a plain <code>requests.Session</code>.</p>
</div>
<hr />
<h2 id="the-methods-you-implement">The methods you implement<a class="headerlink" href="#the-methods-you-implement" title="Permanent link">&para;</a></h2>
<p>Methods run in the order shown. Only the three abstract ones are mandatory.</p>
<pre class="mermaid"><code>graph LR
A[cli] --&gt; B[__init__] --&gt; C[authenticate] --&gt; D[get_titles] --&gt; E[get_tracks] --&gt; F[get_chapters]</code></pre>
<h3 id="cli-the-click-entry-point"><code>cli</code>: the Click entry point<a class="headerlink" href="#cli-the-click-entry-point" title="Permanent link">&para;</a></h3>
<p>A static method decorated as a Click command. It defines the CLI arguments and
returns an <strong>instance</strong> of your service. Keep the command <code>name=</code> matching the
tag.</p>
<div class="admonition note">
<p class="admonition-title"><code>name=</code> is cosmetic: the directory name is what resolves the command</p>
<p><code>unshackle dl EXAMPLE</code> is dispatched by the <strong>folder/class name</strong>, not by the
<code>name=</code> in <code>@click.command</code>. A mismatched <code>name=</code> still loads and runs; it
just produces confusing <code>--help</code>/usage output that names the wrong command.
That confusing output is the reason to keep it in sync, not any dispatch
requirement.</p>
</div>
<div class="language-python highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="nd">@staticmethod</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a><span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&quot;EXAMPLE&quot;</span><span class="p">,</span> <span class="n">short_help</span><span class="o">=</span><span class="s2">&quot;https://domain.com&quot;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="vm">__doc__</span><span class="p">)</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a><span class="nd">@click</span><span class="o">.</span><span class="n">argument</span><span class="p">(</span><span class="s2">&quot;title&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">)</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a><span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span><span class="s2">&quot;-m&quot;</span><span class="p">,</span> <span class="s2">&quot;--movie&quot;</span><span class="p">,</span> <span class="n">is_flag</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">&quot;Treat the title as a movie.&quot;</span><span class="p">)</span>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a><span class="nd">@click</span><span class="o">.</span><span class="n">option</span><span class="p">(</span><span class="s2">&quot;-d&quot;</span><span class="p">,</span> <span class="s2">&quot;--device&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">click</span><span class="o">.</span><span class="n">Choice</span><span class="p">([</span><span class="s2">&quot;android_tv&quot;</span><span class="p">,</span> <span class="s2">&quot;web&quot;</span><span class="p">,</span> <span class="s2">&quot;ios&quot;</span><span class="p">]),</span> <span class="n">default</span><span class="o">=</span><span class="s2">&quot;android_tv&quot;</span><span class="p">)</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="nd">@click</span><span class="o">.</span><span class="n">pass_context</span>
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a><span class="k">def</span><span class="w"> </span><span class="nf">cli</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="n">click</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">Any</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">EXAMPLE</span><span class="p">:</span>
</span><span id="__span-5-8"><a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a> <span class="k">return</span> <span class="n">EXAMPLE</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span><span id="__span-5-9"><a id="__codelineno-5-9" name="__codelineno-5-9" href="#__codelineno-5-9"></a>
</span><span id="__span-5-10"><a id="__codelineno-5-10" name="__codelineno-5-10" href="#__codelineno-5-10"></a><span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">click</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">movie</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span> <span class="n">device</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span><span id="__span-5-11"><a id="__codelineno-5-11" name="__codelineno-5-11" href="#__codelineno-5-11"></a> <span class="bp">self</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="n">title</span>
</span><span id="__span-5-12"><a id="__codelineno-5-12" name="__codelineno-5-12" href="#__codelineno-5-12"></a> <span class="bp">self</span><span class="o">.</span><span class="n">movie</span> <span class="o">=</span> <span class="n">movie</span>
</span><span id="__span-5-13"><a id="__codelineno-5-13" name="__codelineno-5-13" href="#__codelineno-5-13"></a> <span class="bp">self</span><span class="o">.</span><span class="n">device</span> <span class="o">=</span> <span class="n">device</span>
</span><span id="__span-5-14"><a id="__codelineno-5-14" name="__codelineno-5-14" href="#__codelineno-5-14"></a> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span> <span class="c1"># wires up config, log, session, caches, track_request...</span>
</span><span id="__span-5-15"><a id="__codelineno-5-15" name="__codelineno-5-15" href="#__codelineno-5-15"></a> <span class="bp">self</span><span class="o">.</span><span class="n">cdm</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="n">obj</span><span class="o">.</span><span class="n">cdm</span>
</span></code></pre></div>
<p>Your docstring becomes the CLI <code>--help</code> text (<code>help=__doc__</code>), so document the
accepted URL/ID formats and options there.</p>
<div class="admonition tip">
<p class="admonition-title">CDM-aware behaviour</p>
<p>The resolved CDM is on <code>ctx.obj.cdm</code> (may be <code>None</code> for DRM-free runs). Use
<code>is_widevine_cdm()</code> / <code>is_playready_cdm()</code> from <code>unshackle.core.cdm.detect</code>
to classify it. These correctly handle local <em>and</em> remote CDMs, so never
hand-roll an <code>isinstance</code> check. Services often pick a device profile or
manifest endpoint based on which DRM the CDM speaks.</p>
</div>
<h3 id="authenticate-optional-login"><code>authenticate</code>: optional login<a class="headerlink" href="#authenticate-optional-login" title="Permanent link">&para;</a></h3>
<p>Override to log in with cookies and/or credentials. <strong>Call <code>super().authenticate()</code>
first</strong>: the base implementation loads the cookie jar into <code>self.session.cookies</code>
and stores <code>self.credential</code>. Do all token fetching here; it runs before
<code>get_titles</code>.</p>
<div class="language-python highlight"><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="k">def</span><span class="w"> </span><span class="nf">authenticate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">cookies</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">credential</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</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="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">authenticate</span><span class="p">(</span><span class="n">cookies</span><span class="p">,</span> <span class="n">credential</span><span class="p">)</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="s2">&quot;user-agent&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;client&quot;</span><span class="p">][</span><span class="bp">self</span><span class="o">.</span><span class="n">device</span><span class="p">][</span><span class="s2">&quot;user_agent&quot;</span><span class="p">]})</span>
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a>
</span><span id="__span-6-5"><a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a> <span class="n">cache</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;tokens_</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">device</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">profile</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
</span><span id="__span-6-6"><a id="__codelineno-6-6" name="__codelineno-6-6" href="#__codelineno-6-6"></a> <span class="k">if</span> <span class="n">cache</span> <span class="ow">and</span> <span class="n">cache</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;expires_in&quot;</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&gt;</span> <span class="nb">int</span><span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">timestamp</span><span class="p">()):</span>
</span><span id="__span-6-7"><a id="__codelineno-6-7" name="__codelineno-6-7" href="#__codelineno-6-7"></a> <span class="bp">self</span><span class="o">.</span><span class="n">log</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot; + Using cached tokens&quot;</span><span class="p">)</span>
</span><span id="__span-6-8"><a id="__codelineno-6-8" name="__codelineno-6-8" href="#__codelineno-6-8"></a> <span class="k">else</span><span class="p">:</span>
</span><span id="__span-6-9"><a id="__codelineno-6-9" name="__codelineno-6-9" href="#__codelineno-6-9"></a> <span class="n">token</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;endpoints&quot;</span><span class="p">][</span><span class="s2">&quot;login&quot;</span><span class="p">],</span> <span class="n">data</span><span class="o">=</span><span class="n">body</span><span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span><span id="__span-6-10"><a id="__codelineno-6-10" name="__codelineno-6-10" href="#__codelineno-6-10"></a> <span class="n">cache</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="n">token</span><span class="p">,</span> <span class="n">expiration</span><span class="o">=</span><span class="n">token</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;expires_in&quot;</span><span class="p">))</span>
</span><span id="__span-6-11"><a id="__codelineno-6-11" name="__codelineno-6-11" href="#__codelineno-6-11"></a> <span class="bp">self</span><span class="o">.</span><span class="n">token</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">&quot;token&quot;</span><span class="p">]</span>
</span></code></pre></div>
<p>Cookies come from files under <code>directories.cookies</code>; credentials from the
<code>credentials</code> map in <code>unshackle.yaml</code>. For interactive prompts (OTP, captcha)
use <code>self.request_input(prompt)</code>, never a bare <code>input()</code>. Under <code>serve</code> mode
there is no local terminal, so a bare <code>input()</code> would hang the server waiting on
stdin that never arrives; <code>request_input</code> instead relays the prompt to the
remote client through the attached <code>InputBridge</code>. Locally it routes through the
shared Rich console (<code>console.input</code>) so the prompt renders correctly alongside
progress and log output.</p>
<div class="admonition warning">
<p class="admonition-title">Key token caches by whatever varies per session</p>
<p>The cache key above is <code>tokens_{device}_{profile}</code> on purpose. A token cache
must include every dimension that changes the token (device, profile, or
<code>credential.sha1</code>), or two profiles/devices will share one cache entry and
stomp each other's tokens. This isn't obvious from the caching API, which
happily lets you use a single flat key.</p>
</div>
<div class="admonition warning">
<p class="admonition-title">Handle the credential carefully</p>
<p>Use the <code>Credential</code> only inside <code>authenticate()</code> to obtain tokens; don't
stash it or the raw cookie jar elsewhere. The base class already caches
identity for you.</p>
</div>
<h3 id="get_titles-required"><code>get_titles</code>: required<a class="headerlink" href="#get_titles-required" title="Permanent link">&para;</a></h3>
<p>Return a titles collection for the given ID. The return type depends on content:</p>
<table>
<thead>
<tr>
<th>Return</th>
<th>Contains</th>
<th>Use for</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Movies</code></td>
<td><code>Movie</code> objects</td>
<td>Films</td>
</tr>
<tr>
<td><code>Series</code></td>
<td><code>Episode</code> objects</td>
<td>Shows (flatten seasons into episodes)</td>
</tr>
<tr>
<td><code>Album</code></td>
<td><code>Song</code> objects</td>
<td>Music</td>
</tr>
</tbody>
</table>
<p>Every title carries a <code>language</code> (its <strong>original recorded language</strong>) and an
arbitrary <code>data</code> dict you can use to stash metadata for later methods. At least
one title must be returned, or the ID is treated as invalid.</p>
<div class="language-python 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="k">def</span><span class="w"> </span><span class="nf">get_titles</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Titles_T</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="n">match</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">TITLE_RE</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">match</span><span class="p">:</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&quot;Could not parse a title ID - is the URL/ID correct?&quot;</span><span class="p">)</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a> <span class="n">metadata</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
</span><span id="__span-7-6"><a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;endpoints&quot;</span><span class="p">][</span><span class="s2">&quot;metadata&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">title_id</span><span class="o">=</span><span class="n">match</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="s2">&quot;title_id&quot;</span><span class="p">)),</span>
</span><span id="__span-7-7"><a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a> <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;token&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">token</span><span class="p">},</span>
</span><span id="__span-7-8"><a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a> <span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span><span id="__span-7-9"><a id="__codelineno-7-9" name="__codelineno-7-9" href="#__codelineno-7-9"></a> <span class="n">original_lang</span> <span class="o">=</span> <span class="n">Language</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">metadata</span><span class="p">[</span><span class="s2">&quot;languages&quot;</span><span class="p">][</span><span class="mi">0</span><span class="p">])</span>
</span><span id="__span-7-10"><a id="__codelineno-7-10" name="__codelineno-7-10" href="#__codelineno-7-10"></a>
</span><span id="__span-7-11"><a id="__codelineno-7-11" name="__codelineno-7-11" href="#__codelineno-7-11"></a> <span class="k">return</span> <span class="n">Movies</span><span class="p">([</span>
</span><span id="__span-7-12"><a id="__codelineno-7-12" name="__codelineno-7-12" href="#__codelineno-7-12"></a> <span class="n">Movie</span><span class="p">(</span>
</span><span id="__span-7-13"><a id="__codelineno-7-13" name="__codelineno-7-13" href="#__codelineno-7-13"></a> <span class="n">id_</span><span class="o">=</span><span class="n">metadata</span><span class="p">[</span><span class="s2">&quot;id&quot;</span><span class="p">],</span>
</span><span id="__span-7-14"><a id="__codelineno-7-14" name="__codelineno-7-14" href="#__codelineno-7-14"></a> <span class="n">service</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="p">,</span>
</span><span id="__span-7-15"><a id="__codelineno-7-15" name="__codelineno-7-15" href="#__codelineno-7-15"></a> <span class="n">name</span><span class="o">=</span><span class="n">metadata</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">],</span>
</span><span id="__span-7-16"><a id="__codelineno-7-16" name="__codelineno-7-16" href="#__codelineno-7-16"></a> <span class="n">year</span><span class="o">=</span><span class="n">metadata</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;releaseYear&quot;</span><span class="p">)</span> <span class="ow">or</span> <span class="kc">None</span><span class="p">,</span>
</span><span id="__span-7-17"><a id="__codelineno-7-17" name="__codelineno-7-17" href="#__codelineno-7-17"></a> <span class="n">language</span><span class="o">=</span><span class="n">original_lang</span><span class="p">,</span> <span class="c1"># ORIGINAL audio language, not the -l preference</span>
</span><span id="__span-7-18"><a id="__codelineno-7-18" name="__codelineno-7-18" href="#__codelineno-7-18"></a> <span class="n">data</span><span class="o">=</span><span class="n">metadata</span><span class="p">,</span> <span class="c1"># read later as title.data</span>
</span><span id="__span-7-19"><a id="__codelineno-7-19" name="__codelineno-7-19" href="#__codelineno-7-19"></a> <span class="p">)</span>
</span><span id="__span-7-20"><a id="__codelineno-7-20" name="__codelineno-7-20" href="#__codelineno-7-20"></a> <span class="p">])</span>
</span></code></pre></div>
<p>Common constructor fields:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:3"><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" /><div class="tabbed-labels"><label for="__tabbed_1_1">Movie</label><label for="__tabbed_1_2">Episode</label><label for="__tabbed_1_3">Song</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<div class="language-python 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="n">Movie</span><span class="p">(</span><span class="n">id_</span><span class="p">,</span> <span class="n">service</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">year</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">language</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
</span></code></pre></div>
</div>
<div class="tabbed-block">
<div class="language-python highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="n">Episode</span><span class="p">(</span><span class="n">id_</span><span class="p">,</span> <span class="n">service</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">season</span><span class="p">,</span> <span class="n">number</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">year</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
</span><span id="__span-9-2"><a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a> <span class="n">language</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">air_date</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
</span><span id="__span-9-3"><a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a> <span class="n">part</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
</span></code></pre></div>
<p>Pass <code>air_date</code> for daily/sports content to name by date instead of
<code>SxxExx</code>. Return a <code>Series([...])</code> of episodes. Pass <code>part</code> only when the
service splits one episode into several videos, see
<a href="#split-episodes">Split episodes</a>.</p>
</div>
<div class="tabbed-block">
<div class="language-python 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="n">Song</span><span class="p">(</span><span class="n">id_</span><span class="p">,</span> <span class="n">service</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">artist</span><span class="p">,</span> <span class="n">album</span><span class="p">,</span> <span class="n">track</span><span class="p">,</span> <span class="n">disc</span><span class="p">,</span> <span class="n">year</span><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="n">language</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
</span></code></pre></div>
<p>Return an <code>Album([...])</code> of songs.</p>
</div>
</div>
</div>
<div class="admonition note">
<p class="admonition-title">The ID must be unique and stable</p>
<p><code>id_</code> must be truthy and, if it has a length, at least 4 characters, to
avoid clashes. Use the service's own stable IDs.</p>
</div>
<div class="admonition tip">
<p class="admonition-title">Title caching</p>
<p>You can call <code>self.get_titles_cached()</code> instead of <code>get_titles()</code> from the
framework path to reuse cached results and gracefully fall back to stale data
when the API errors. It also applies the per-service <code>title_map</code> config.</p>
</div>
<h4 id="split-episodes">Split episodes<a class="headerlink" href="#split-episodes" title="Permanent link">&para;</a></h4>
<p>Some services split one logical episode into several separately playable videos, for
example a variety show that ships episode 1 as <em>Episode 1 (Part 1)</em>, <em>(Part 2)</em> and
<em>(Part 3)</em>. <code>Episode.number</code> stays an <code>int</code>. The optional <code>part</code> carries the index.</p>
<div class="language-python 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="n">Episode</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">season</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">number</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">part</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;The Reckoning&quot;</span><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="n">Episode</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">season</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">number</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">part</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;The Reckoning&quot;</span><span class="p">)</span>
</span></code></pre></div>
<ol>
<li><code>part</code> counts from 1, contiguous. <code>part=0</code> is rejected, and <code>-w</code> can only address
parts up to 99.</li>
<li>Every part of one episode shares the same <code>season</code> and <code>number</code>.</li>
<li>Each part is a separate <code>Episode</code> with its own unique <code>id_</code>.</li>
<li>Unsplit episode: leave <code>part</code> unset. Never pass <code>part=1</code> for a whole episode.</li>
<li>Do not put the part in <code>name</code> or <code>number</code>. Pass a clean <code>name</code> (or <code>None</code>) and set
<code>part</code>.</li>
<li><code>part</code> is a split <em>episode</em>, not a split <em>season</em>. A service listing <em>Season 1 Part 2</em>
describes a group of episodes. Keep numbering those in sequence, the way the shipped
services do.</li>
</ol>
<p><code>part</code> accepts an <code>int</code> or a digit string. A value that is not an integer raises
<code>TypeError</code>. A value of <code>0</code> or less raises <code>ValueError</code>.</p>
<div class="admonition warning">
<p class="admonition-title">The framework never infers a part from the episode name</p>
<p>Rule 6 is the reason. Shipped services already use the word "Part" to mean a <em>season</em>
part, so name-sniffing would misread every one of them. The part has to arrive
structurally, as the <code>part</code> argument.</p>
</div>
<div class="admonition tip">
<p class="admonition-title">Clean the part out of the name</p>
<p><code>Episode</code> discards any <code>name</code> that starts with <code>Episode &lt;number&gt;</code>, so a raw
<code>"Episode 1 (Part 2): The Reckoning"</code> leaves you with no episode name at all. Strip
the prefix before you pass it, or pass <code>None</code>.</p>
</div>
<p>Once <code>part</code> is set, the filename gains a part token (<code>S01E01.Part.2</code>, see
<a href="../../guide/output-and-naming/#split-episodes">Output &amp; naming</a>), <code>-w S01E01.2</code> selects a
single part while a bare <code>-w S01E01</code> selects them all, and the <code>--list-titles</code> tree labels
the episode <code>01.2</code>. The REST API reports it as an extra <code>part</code> key on the serialized
title, described in <a href="../rest-api/endpoints/#post-apilist-titles">Endpoints</a>.</p>
<div class="admonition note">
<p class="admonition-title">Opt-in and additive</p>
<p>Leaving <code>part</code> unset costs you nothing: filenames, sort order, <code>-w</code> matching and the
API JSON are identical to a plain episode, so a service that does not split episodes
has nothing to adopt.</p>
</div>
<h3 id="get_tracks-required"><code>get_tracks</code>: required<a class="headerlink" href="#get_tracks-required" title="Permanent link">&para;</a></h3>
<p>Given one title, return a <code>Tracks</code> object holding <code>Video</code>, <code>Audio</code>, and
<code>Subtitle</code> tracks. In almost all cases you build these by parsing a manifest,
not by hand.</p>
<p>The manifest parsers live in <code>unshackle.core.manifests</code>:</p>
<div class="language-python highlight"><pre><span></span><code><span id="__span-12-1"><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">unshackle.core.manifests</span><span class="w"> </span><span class="kn">import</span> <span class="n">DASH</span><span class="p">,</span> <span class="n">HLS</span><span class="p">,</span> <span class="n">ISM</span>
</span></code></pre></div>
<p>Each exposes <code>from_url(url, session=...)</code> (or <code>from_text(text, url)</code>) and then
<code>.to_tracks(language=...)</code>:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:3"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">DASH (.mpd)</label><label for="__tabbed_2_2">HLS (.m3u8)</label><label for="__tabbed_2_3">ISM (Smooth Streaming)</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<div class="language-python highlight"><pre><span></span><code><span id="__span-13-1"><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_tracks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tracks</span><span class="p">:</span>
</span><span id="__span-13-2"><a id="__codelineno-13-2" name="__codelineno-13-2" href="#__codelineno-13-2"></a> <span class="k">return</span> <span class="n">DASH</span><span class="o">.</span><span class="n">from_url</span><span class="p">(</span><span class="n">manifest_url</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="p">)</span> \
</span><span id="__span-13-3"><a id="__codelineno-13-3" name="__codelineno-13-3" href="#__codelineno-13-3"></a> <span class="o">.</span><span class="n">to_tracks</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="n">title</span><span class="o">.</span><span class="n">language</span><span class="p">)</span>
</span></code></pre></div>
</div>
<div class="tabbed-block">
<div class="language-python highlight"><pre><span></span><code><span id="__span-14-1"><a id="__codelineno-14-1" name="__codelineno-14-1" href="#__codelineno-14-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_tracks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tracks</span><span class="p">:</span>
</span><span id="__span-14-2"><a id="__codelineno-14-2" name="__codelineno-14-2" href="#__codelineno-14-2"></a> <span class="k">return</span> <span class="n">HLS</span><span class="o">.</span><span class="n">from_url</span><span class="p">(</span><span class="n">manifest_url</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="p">)</span> \
</span><span id="__span-14-3"><a id="__codelineno-14-3" name="__codelineno-14-3" href="#__codelineno-14-3"></a> <span class="o">.</span><span class="n">to_tracks</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="n">title</span><span class="o">.</span><span class="n">language</span><span class="p">)</span>
</span></code></pre></div>
<p>The URL must be a <strong>variant (master) playlist</strong>, not a media playlist.</p>
</div>
<div class="tabbed-block">
<div class="language-python highlight"><pre><span></span><code><span id="__span-15-1"><a id="__codelineno-15-1" name="__codelineno-15-1" href="#__codelineno-15-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_tracks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tracks</span><span class="p">:</span>
</span><span id="__span-15-2"><a id="__codelineno-15-2" name="__codelineno-15-2" href="#__codelineno-15-2"></a> <span class="k">return</span> <span class="n">ISM</span><span class="o">.</span><span class="n">from_url</span><span class="p">(</span><span class="n">manifest_url</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="p">)</span> \
</span><span id="__span-15-3"><a id="__codelineno-15-3" name="__codelineno-15-3" href="#__codelineno-15-3"></a> <span class="o">.</span><span class="n">to_tracks</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="n">title</span><span class="o">.</span><span class="n">language</span><span class="p">)</span>
</span></code></pre></div>
</div>
</div>
</div>
<div class="admonition warning">
<p class="admonition-title">Always pass <code>language=</code></p>
<p>Pass the title's original language to <code>to_tracks(language=...)</code>. For DASH and
HLS this is <strong>required as a fallback</strong>. If a track's language can't be
derived and no valid fallback was given, <code>to_tracks</code> raises <code>ValueError</code>.
It's also what lets the parser flag <code>is_original_lang</code> on each track (via
<code>is_close_match</code>), driving <code>-l best/all</code> selection and the filename language
token.</p>
</div>
<p>Some services deliver a <strong>separate manifest per codec/range</strong>. The base class
provides <code>_get_tracks_for_variants(title, fetch_fn)</code> to fan out over every
codec×range in the <code>TrackRequest</code>, including <code>HYBRID</code> (fetch HDR10 + DV and
merge) and <code>--best-available</code> skip-on-error handling:</p>
<div class="language-python highlight"><pre><span></span><code><span id="__span-16-1"><a id="__codelineno-16-1" name="__codelineno-16-1" href="#__codelineno-16-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_tracks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tracks</span><span class="p">:</span>
</span><span id="__span-16-2"><a id="__codelineno-16-2" name="__codelineno-16-2" href="#__codelineno-16-2"></a> <span class="k">def</span><span class="w"> </span><span class="nf">_fetch_variant</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">codec</span><span class="p">,</span> <span class="n">range_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tracks</span><span class="p">:</span>
</span><span id="__span-16-3"><a id="__codelineno-16-3" name="__codelineno-16-3" href="#__codelineno-16-3"></a> <span class="n">vcodec</span> <span class="o">=</span> <span class="s2">&quot;H265&quot;</span> <span class="k">if</span> <span class="n">codec</span> <span class="o">==</span> <span class="n">Video</span><span class="o">.</span><span class="n">Codec</span><span class="o">.</span><span class="n">HEVC</span> <span class="k">else</span> <span class="s2">&quot;H264&quot;</span>
</span><span id="__span-16-4"><a id="__codelineno-16-4" name="__codelineno-16-4" href="#__codelineno-16-4"></a> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_fetch_dash_manifest</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">vcodec</span><span class="o">=</span><span class="n">vcodec</span><span class="p">,</span> <span class="n">range_</span><span class="o">=</span><span class="n">range_</span><span class="p">)</span>
</span><span id="__span-16-5"><a id="__codelineno-16-5" name="__codelineno-16-5" href="#__codelineno-16-5"></a> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_tracks_for_variants</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">_fetch_variant</span><span class="p">)</span>
</span></code></pre></div>
<p>After parsing, you may <strong>correct</strong> track metadata the manifest gets wrong:
stamp the real <code>video.range</code>, fix odd audio channel counts, mark descriptive
audio, add hand-built subtitles or an <code>Attachment</code> (e.g. cover art). Do <em>not</em>
filter for resolution/bitrate. See <code>_fetch_dash_manifest</code> in the EXAMPLE service
for a thorough demonstration.</p>
<div class="admonition note">
<p class="admonition-title">Flip HDR10 → HDR10+ by hand when you know the platform embeds it</p>
<p>HDR10+ is a <strong>bitstream (SEI)</strong> feature. The HLS parser intentionally does
not sniff it from the stream, so a manifest that embeds HDR10+ SEI but labels
the variant plain HDR10 will parse as HDR10. The label is the only signal
the parser has, and it's wrong. Only the service author knows the platform
embeds HDR10+, so it's on you to correct <code>video.range</code> to HDR10+ here for
those services.</p>
</div>
<div class="admonition warning">
<p class="admonition-title"><code>get_tracks</code> is your only chance to capture license inputs</p>
<p>The license callbacks (<code>get_widevine_license</code>, etc.) fire <strong>much later</strong>, in
the download/decrypt step, long after <code>get_tracks</code> has returned, and they
receive <strong>only</strong> <code>challenge</code>, <code>title</code>, and <code>track</code>. Nothing else from the
manifest response reaches them. So anything a license call needs (the license
URL, <code>dt-custom-data</code>, a session token) has to be stashed <strong>now</strong>, during
<code>get_tracks</code>, onto <code>self.license_data</code> or <code>title.data</code>. This is a
lifecycle-timing consequence you can't infer from the callback signatures.</p>
</div>
<div class="admonition tip">
<p class="admonition-title">Provide the KID if you cheaply can</p>
<p>If you can obtain a track's Key ID (32-char hex) without downloading stream
data, set <code>track.kid</code>; it speeds up the decryption/key-lookup path. And be
sure encrypted tracks carry the correct <code>drm</code> so the core licenses them.</p>
</div>
<h3 id="get_chapters-required"><code>get_chapters</code>: required<a class="headerlink" href="#get_chapters-required" title="Permanent link">&para;</a></h3>
<p>Return a <code>Chapters</code> object (0 or more <code>Chapter</code>s). You don't need to number or
sort them; <code>Chapters</code> does that automatically and inserts a <code>00:00:00.000</code>
marker if missing. A <code>Chapter</code> timestamp accepts <code>"HH:MM:SS[.mmm]"</code>, an int in
milliseconds, or a float in seconds.</p>
<div class="language-python highlight"><pre><span></span><code><span id="__span-17-1"><a id="__codelineno-17-1" name="__codelineno-17-1" href="#__codelineno-17-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_chapters</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Chapters</span><span class="p">:</span>
</span><span id="__span-17-2"><a id="__codelineno-17-2" name="__codelineno-17-2" href="#__codelineno-17-2"></a> <span class="n">chapters</span> <span class="o">=</span> <span class="n">Chapters</span><span class="p">()</span>
</span><span id="__span-17-3"><a id="__codelineno-17-3" name="__codelineno-17-3" href="#__codelineno-17-3"></a> <span class="n">seen</span><span class="p">:</span> <span class="nb">set</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span><span id="__span-17-4"><a id="__codelineno-17-4" name="__codelineno-17-4" href="#__codelineno-17-4"></a> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">title</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;chapters&quot;</span><span class="p">,</span> <span class="p">[]):</span>
</span><span id="__span-17-5"><a id="__codelineno-17-5" name="__codelineno-17-5" href="#__codelineno-17-5"></a> <span class="n">chapter</span> <span class="o">=</span> <span class="n">Chapter</span><span class="p">(</span><span class="n">timestamp</span><span class="o">=</span><span class="n">ch</span><span class="p">[</span><span class="s2">&quot;start&quot;</span><span class="p">],</span> <span class="n">name</span><span class="o">=</span><span class="n">ch</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;name&quot;</span><span class="p">))</span>
</span><span id="__span-17-6"><a id="__codelineno-17-6" name="__codelineno-17-6" href="#__codelineno-17-6"></a> <span class="k">if</span> <span class="n">chapter</span><span class="o">.</span><span class="n">timestamp</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">or</span> <span class="n">chapter</span><span class="o">.</span><span class="n">timestamp</span> <span class="ow">in</span> <span class="n">seen</span><span class="p">:</span>
</span><span id="__span-17-7"><a id="__codelineno-17-7" name="__codelineno-17-7" href="#__codelineno-17-7"></a> <span class="k">continue</span> <span class="c1"># see the warning below</span>
</span><span id="__span-17-8"><a id="__codelineno-17-8" name="__codelineno-17-8" href="#__codelineno-17-8"></a> <span class="n">seen</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">chapter</span><span class="o">.</span><span class="n">timestamp</span><span class="p">)</span>
</span><span id="__span-17-9"><a id="__codelineno-17-9" name="__codelineno-17-9" href="#__codelineno-17-9"></a> <span class="n">chapters</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">chapter</span><span class="p">)</span>
</span><span id="__span-17-10"><a id="__codelineno-17-10" name="__codelineno-17-10" href="#__codelineno-17-10"></a> <span class="k">return</span> <span class="n">chapters</span>
</span></code></pre></div>
<div class="admonition warning">
<p class="admonition-title">Skip markers at 0 and de-duplicate timestamps before <code>add()</code></p>
<p><code>Chapters.add()</code> auto-injects a <code>Chapter(0)</code> at <code>00:00:00.000</code> the first time
you add <em>any</em> chapter, <strong>and</strong> it raises <code>ValueError</code> if a chapter already
exists at an exact timestamp. So an API marker reported at <code>0</code> (an intro or
recap that starts at the very beginning) collides with that auto-inserted
opening chapter, and two markers sharing a timestamp collide with each other.
Guard both cases: drop any marker at timestamp <code>0</code> and de-duplicate timestamps
before adding, as above.</p>
</div>
<div class="admonition note">
<p class="admonition-title">An unnamed chapter is the idiom for closing a named range</p>
<p>A <code>Chapter</code> with no name is perfectly valid. Inserting an unnamed marker is
the standard way to <em>close out</em> a named range: e.g. add <code>Chapter(name="Intro")</code>
at the start and an unnamed <code>Chapter</code> where the intro ends, so the "Intro"
label applies only to that span.</p>
</div>
<div class="admonition warning">
<p class="admonition-title">Don't invent chapter names</p>
<p>Never name chapters <code>"Chapter 1"</code> yourself. Leave unnamed markers unnamed;
users who want generic names set <code>chapter_fallback_name</code> (e.g.
<code>"Chapter {i:02}"</code>) in their config.</p>
</div>
<hr />
<h2 id="handling-drm">Handling DRM<a class="headerlink" href="#handling-drm" title="Permanent link">&para;</a></h2>
<p>The core drives licensing. Your job is to answer the license challenges the CDM
produces. Mark encrypted tracks with the right DRM in <code>get_tracks</code> (the manifest
parsers do this automatically for PSSH/<code>EXT-X-KEY</code> found in the manifest), then
implement the license callbacks you need. Each receives the challenge plus the
current <code>title</code> and <code>track</code>:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>For</th>
<th>Returns</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>get_widevine_service_certificate</code></td>
<td>Widevine privacy mode</td>
<td>The service certificate (bytes or base64 str), or <code>None</code>.</td>
</tr>
<tr>
<td><code>get_widevine_license</code></td>
<td>Widevine</td>
<td>The raw license response (bytes or base64 str), <strong>unmodified</strong>.</td>
</tr>
<tr>
<td><code>get_playready_license</code></td>
<td>PlayReady</td>
<td>The license response. Defaults to delegating to <code>get_widevine_license</code>.</td>
</tr>
<tr>
<td><code>get_clearkey_license</code></td>
<td>DASH <code>org.w3.clearkey</code></td>
<td>The JWK Set (dict/JSON/bytes), or <code>None</code> to let the framework POST the challenge to the manifest's Laurl.</td>
</tr>
</tbody>
</table>
<div class="language-python highlight"><pre><span></span><code><span id="__span-18-1"><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_widevine_license</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">challenge</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">track</span><span class="p">):</span>
</span><span id="__span-18-2"><a id="__codelineno-18-2" name="__codelineno-18-2" href="#__codelineno-18-2"></a> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">post</span><span class="p">(</span>
</span><span id="__span-18-3"><a id="__codelineno-18-3" name="__codelineno-18-3" href="#__codelineno-18-3"></a> <span class="bp">self</span><span class="o">.</span><span class="n">license_data</span><span class="p">[</span><span class="s2">&quot;url&quot;</span><span class="p">],</span>
</span><span id="__span-18-4"><a id="__codelineno-18-4" name="__codelineno-18-4" href="#__codelineno-18-4"></a> <span class="n">data</span><span class="o">=</span><span class="n">challenge</span><span class="p">,</span> <span class="c1"># POST the challenge as-is</span>
</span><span id="__span-18-5"><a id="__codelineno-18-5" name="__codelineno-18-5" href="#__codelineno-18-5"></a> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;dt-custom-data&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">license_data</span><span class="p">[</span><span class="s2">&quot;data&quot;</span><span class="p">]},</span>
</span><span id="__span-18-6"><a id="__codelineno-18-6" name="__codelineno-18-6" href="#__codelineno-18-6"></a> <span class="p">)</span>
</span><span id="__span-18-7"><a id="__codelineno-18-7" name="__codelineno-18-7" href="#__codelineno-18-7"></a> <span class="n">response</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span><span id="__span-18-8"><a id="__codelineno-18-8" name="__codelineno-18-8" href="#__codelineno-18-8"></a> <span class="k">try</span><span class="p">:</span>
</span><span id="__span-18-9"><a id="__codelineno-18-9" name="__codelineno-18-9" href="#__codelineno-18-9"></a> <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()[</span><span class="s2">&quot;license&quot;</span><span class="p">]</span> <span class="c1"># some services wrap it in JSON</span>
</span><span id="__span-18-10"><a id="__codelineno-18-10" name="__codelineno-18-10" href="#__codelineno-18-10"></a> <span class="k">except</span> <span class="p">(</span><span class="ne">ValueError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">):</span>
</span><span id="__span-18-11"><a id="__codelineno-18-11" name="__codelineno-18-11" href="#__codelineno-18-11"></a> <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">content</span> <span class="c1"># others return raw bytes</span>
</span></code></pre></div>
<div class="admonition warning">
<p class="admonition-title">Return the license untouched</p>
<p>Do <strong>not</strong> base64-encode or decode the challenge or response. Pass them
through verbatim. A malformed license request can get your CDM/device
flagged, banned, or downgraded.</p>
</div>
<div class="admonition tip">
<p class="admonition-title"><code>ClearKeyCENC</code> (<code>org.w3.clearkey</code>) has three escalating integration levels</p>
<p>Pick the lowest level that works for the platform:</p>
<ol>
<li><strong>The manifest carries a <code>&lt;Laurl&gt;</code></strong>: implement nothing. The framework
POSTs the challenge to that URL itself.</li>
<li><strong>A custom endpoint or extra headers are needed</strong>: override
<code>get_clearkey_license</code> to do the POST yourself.</li>
<li><strong>Keys arrive obfuscated or through a bespoke channel</strong>: fetch and unwrap
the key in <code>get_tracks</code> and pre-populate
<code>drm.content_keys[kid] = key_hex</code> on the track's <code>ClearKeyCENC</code>. The
non-obvious payoff: when every KID is already keyed, the framework skips
the license round-trip entirely.</li>
</ol>
</div>
<p>Things you don't handle:</p>
<ul>
<li><strong>HLS AES-128 (<code>ClearKey</code>)</strong> and <strong>DRM-free</strong> content have no license
callback. The key comes from the manifest and is applied directly.</li>
<li><strong>Key vaults</strong> cache <code>KID:KEY</code> pairs across runs, so repeat downloads skip the
license round-trip entirely.</li>
<li><strong>Decryption tooling</strong> (shaka-packager or mp4decrypt) and the choice of local
vs remote CDM are user configuration, not service concerns.</li>
</ul>
<p>See the <a href="../../guide/drm-and-cdm/">DRM &amp; CDM reference</a> for the full picture.</p>
<hr />
<h2 id="optional-event-hooks">Optional event hooks<a class="headerlink" href="#optional-event-hooks" title="Permanent link">&para;</a></h2>
<p>Override any of these to react to pipeline stages (all no-ops by default):
<code>on_segment_downloaded</code>, <code>on_track_downloaded</code>, <code>on_track_decrypted</code>,
<code>on_track_repacked</code>, <code>on_track_multiplex</code>. Also override <code>search()</code> to yield
<code>SearchResult</code> objects for <code>unshackle search</code>.</p>
<div class="language-python highlight"><pre><span></span><code><span id="__span-19-1"><a id="__codelineno-19-1" name="__codelineno-19-1" href="#__codelineno-19-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">search</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Generator</span><span class="p">[</span><span class="n">SearchResult</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">]:</span>
</span><span id="__span-19-2"><a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a> <span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;endpoints&quot;</span><span class="p">][</span><span class="s2">&quot;search&quot;</span><span class="p">],</span>
</span><span id="__span-19-3"><a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a> <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;q&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">})</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span><span id="__span-19-4"><a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span><span class="p">[</span><span class="s2">&quot;entries&quot;</span><span class="p">]:</span>
</span><span id="__span-19-5"><a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a> <span class="k">yield</span> <span class="n">SearchResult</span><span class="p">(</span><span class="n">id_</span><span class="o">=</span><span class="n">r</span><span class="p">[</span><span class="s2">&quot;id&quot;</span><span class="p">],</span> <span class="n">title</span><span class="o">=</span><span class="n">r</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">],</span>
</span><span id="__span-19-6"><a id="__codelineno-19-6" name="__codelineno-19-6" href="#__codelineno-19-6"></a> <span class="n">description</span><span class="o">=</span><span class="n">r</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;description&quot;</span><span class="p">),</span> <span class="n">url</span><span class="o">=</span><span class="n">r</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;url&quot;</span><span class="p">))</span>
</span></code></pre></div>
<hr />
<h2 id="minimal-service-skeleton">Minimal service skeleton<a class="headerlink" href="#minimal-service-skeleton" title="Permanent link">&para;</a></h2>
<p>A complete, minimal single-manifest service. Save as
<code>services/MYSVC/__init__.py</code> (class name <code>MYSVC</code>), with an optional
<code>services/MYSVC/config.yaml</code> beside it.</p>
<div class="language-python highlight"><span class="filename">services/MYSVC/__init__.py</span><pre><span></span><code><span id="__span-20-1"><a id="__codelineno-20-1" name="__codelineno-20-1" href="#__codelineno-20-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">__future__</span><span class="w"> </span><span class="kn">import</span> <span class="n">annotations</span>
</span><span id="__span-20-2"><a id="__codelineno-20-2" name="__codelineno-20-2" href="#__codelineno-20-2"></a>
</span><span id="__span-20-3"><a id="__codelineno-20-3" name="__codelineno-20-3" href="#__codelineno-20-3"></a><span class="kn">import</span><span class="w"> </span><span class="nn">re</span>
</span><span id="__span-20-4"><a id="__codelineno-20-4" name="__codelineno-20-4" href="#__codelineno-20-4"></a><span class="kn">from</span><span class="w"> </span><span class="nn">typing</span><span class="w"> </span><span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Optional</span>
</span><span id="__span-20-5"><a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a>
</span><span id="__span-20-6"><a id="__codelineno-20-6" name="__codelineno-20-6" href="#__codelineno-20-6"></a><span class="kn">import</span><span class="w"> </span><span class="nn">click</span>
</span><span id="__span-20-7"><a id="__codelineno-20-7" name="__codelineno-20-7" href="#__codelineno-20-7"></a>
</span><span id="__span-20-8"><a id="__codelineno-20-8" name="__codelineno-20-8" href="#__codelineno-20-8"></a><span class="kn">from</span><span class="w"> </span><span class="nn">unshackle.core.manifests</span><span class="w"> </span><span class="kn">import</span> <span class="n">DASH</span>
</span><span id="__span-20-9"><a id="__codelineno-20-9" name="__codelineno-20-9" href="#__codelineno-20-9"></a><span class="kn">from</span><span class="w"> </span><span class="nn">unshackle.core.service</span><span class="w"> </span><span class="kn">import</span> <span class="n">Service</span>
</span><span id="__span-20-10"><a id="__codelineno-20-10" name="__codelineno-20-10" href="#__codelineno-20-10"></a><span class="kn">from</span><span class="w"> </span><span class="nn">unshackle.core.titles</span><span class="w"> </span><span class="kn">import</span> <span class="n">Movie</span><span class="p">,</span> <span class="n">Movies</span><span class="p">,</span> <span class="n">Title_T</span><span class="p">,</span> <span class="n">Titles_T</span>
</span><span id="__span-20-11"><a id="__codelineno-20-11" name="__codelineno-20-11" href="#__codelineno-20-11"></a><span class="kn">from</span><span class="w"> </span><span class="nn">unshackle.core.tracks</span><span class="w"> </span><span class="kn">import</span> <span class="n">Chapters</span><span class="p">,</span> <span class="n">Tracks</span>
</span><span id="__span-20-12"><a id="__codelineno-20-12" name="__codelineno-20-12" href="#__codelineno-20-12"></a>
</span><span id="__span-20-13"><a id="__codelineno-20-13" name="__codelineno-20-13" href="#__codelineno-20-13"></a>
</span><span id="__span-20-14"><a id="__codelineno-20-14" name="__codelineno-20-14" href="#__codelineno-20-14"></a><span class="k">class</span><span class="w"> </span><span class="nc">MYSVC</span><span class="p">(</span><span class="n">Service</span><span class="p">):</span>
</span><span id="__span-20-15"><a id="__codelineno-20-15" name="__codelineno-20-15" href="#__codelineno-20-15"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;</span>
</span><span id="__span-20-16"><a id="__codelineno-20-16" name="__codelineno-20-16" href="#__codelineno-20-16"></a><span class="sd"> MyService - https://myservice.com</span>
</span><span id="__span-20-17"><a id="__codelineno-20-17" name="__codelineno-20-17" href="#__codelineno-20-17"></a>
</span><span id="__span-20-18"><a id="__codelineno-20-18" name="__codelineno-20-18" href="#__codelineno-20-18"></a><span class="sd"> \b</span>
</span><span id="__span-20-19"><a id="__codelineno-20-19" name="__codelineno-20-19" href="#__codelineno-20-19"></a><span class="sd"> Usage: unshackle dl MYSVC &lt;title-id-or-url&gt;</span>
</span><span id="__span-20-20"><a id="__codelineno-20-20" name="__codelineno-20-20" href="#__codelineno-20-20"></a><span class="sd"> &quot;&quot;&quot;</span>
</span><span id="__span-20-21"><a id="__codelineno-20-21" name="__codelineno-20-21" href="#__codelineno-20-21"></a>
</span><span id="__span-20-22"><a id="__codelineno-20-22" name="__codelineno-20-22" href="#__codelineno-20-22"></a> <span class="n">ALIASES</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&quot;MY&quot;</span><span class="p">,)</span>
</span><span id="__span-20-23"><a id="__codelineno-20-23" name="__codelineno-20-23" href="#__codelineno-20-23"></a> <span class="n">GEOFENCE</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&quot;US&quot;</span><span class="p">,)</span>
</span><span id="__span-20-24"><a id="__codelineno-20-24" name="__codelineno-20-24" href="#__codelineno-20-24"></a> <span class="n">TITLE_RE</span> <span class="o">=</span> <span class="sa">r</span><span class="s2">&quot;^(?:https?://myservice\.com/watch/)?(?P&lt;id&gt;[\w-]+)&quot;</span>
</span><span id="__span-20-25"><a id="__codelineno-20-25" name="__codelineno-20-25" href="#__codelineno-20-25"></a>
</span><span id="__span-20-26"><a id="__codelineno-20-26" name="__codelineno-20-26" href="#__codelineno-20-26"></a> <span class="nd">@staticmethod</span>
</span><span id="__span-20-27"><a id="__codelineno-20-27" name="__codelineno-20-27" href="#__codelineno-20-27"></a> <span class="nd">@click</span><span class="o">.</span><span class="n">command</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&quot;MYSVC&quot;</span><span class="p">,</span> <span class="n">short_help</span><span class="o">=</span><span class="s2">&quot;https://myservice.com&quot;</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="vm">__doc__</span><span class="p">)</span>
</span><span id="__span-20-28"><a id="__codelineno-20-28" name="__codelineno-20-28" href="#__codelineno-20-28"></a> <span class="nd">@click</span><span class="o">.</span><span class="n">argument</span><span class="p">(</span><span class="s2">&quot;title&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">)</span>
</span><span id="__span-20-29"><a id="__codelineno-20-29" name="__codelineno-20-29" href="#__codelineno-20-29"></a> <span class="nd">@click</span><span class="o">.</span><span class="n">pass_context</span>
</span><span id="__span-20-30"><a id="__codelineno-20-30" name="__codelineno-20-30" href="#__codelineno-20-30"></a> <span class="k">def</span><span class="w"> </span><span class="nf">cli</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="n">click</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">Any</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="s2">&quot;MYSVC&quot;</span><span class="p">:</span>
</span><span id="__span-20-31"><a id="__codelineno-20-31" name="__codelineno-20-31" href="#__codelineno-20-31"></a> <span class="k">return</span> <span class="n">MYSVC</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span><span id="__span-20-32"><a id="__codelineno-20-32" name="__codelineno-20-32" href="#__codelineno-20-32"></a>
</span><span id="__span-20-33"><a id="__codelineno-20-33" name="__codelineno-20-33" href="#__codelineno-20-33"></a> <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">click</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span><span id="__span-20-34"><a id="__codelineno-20-34" name="__codelineno-20-34" href="#__codelineno-20-34"></a> <span class="bp">self</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="n">title</span>
</span><span id="__span-20-35"><a id="__codelineno-20-35" name="__codelineno-20-35" href="#__codelineno-20-35"></a> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>
</span><span id="__span-20-36"><a id="__codelineno-20-36" name="__codelineno-20-36" href="#__codelineno-20-36"></a>
</span><span id="__span-20-37"><a id="__codelineno-20-37" name="__codelineno-20-37" href="#__codelineno-20-37"></a> <span class="k">def</span><span class="w"> </span><span class="nf">authenticate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">cookies</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">credential</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span><span id="__span-20-38"><a id="__codelineno-20-38" name="__codelineno-20-38" href="#__codelineno-20-38"></a> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">authenticate</span><span class="p">(</span><span class="n">cookies</span><span class="p">,</span> <span class="n">credential</span><span class="p">)</span>
</span><span id="__span-20-39"><a id="__codelineno-20-39" name="__codelineno-20-39" href="#__codelineno-20-39"></a> <span class="c1"># obtain any tokens here, e.g. self.token = ...</span>
</span><span id="__span-20-40"><a id="__codelineno-20-40" name="__codelineno-20-40" href="#__codelineno-20-40"></a>
</span><span id="__span-20-41"><a id="__codelineno-20-41" name="__codelineno-20-41" href="#__codelineno-20-41"></a> <span class="k">def</span><span class="w"> </span><span class="nf">get_titles</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Titles_T</span><span class="p">:</span>
</span><span id="__span-20-42"><a id="__codelineno-20-42" name="__codelineno-20-42" href="#__codelineno-20-42"></a> <span class="n">match</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">TITLE_RE</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">)</span>
</span><span id="__span-20-43"><a id="__codelineno-20-43" name="__codelineno-20-43" href="#__codelineno-20-43"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">match</span><span class="p">:</span>
</span><span id="__span-20-44"><a id="__codelineno-20-44" name="__codelineno-20-44" href="#__codelineno-20-44"></a> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&quot;Could not parse a title ID.&quot;</span><span class="p">)</span>
</span><span id="__span-20-45"><a id="__codelineno-20-45" name="__codelineno-20-45" href="#__codelineno-20-45"></a> <span class="n">meta</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
</span><span id="__span-20-46"><a id="__codelineno-20-46" name="__codelineno-20-46" href="#__codelineno-20-46"></a> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;endpoints&quot;</span><span class="p">][</span><span class="s2">&quot;metadata&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="n">match</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="s2">&quot;id&quot;</span><span class="p">))</span>
</span><span id="__span-20-47"><a id="__codelineno-20-47" name="__codelineno-20-47" href="#__codelineno-20-47"></a> <span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span><span id="__span-20-48"><a id="__codelineno-20-48" name="__codelineno-20-48" href="#__codelineno-20-48"></a> <span class="k">return</span> <span class="n">Movies</span><span class="p">([</span>
</span><span id="__span-20-49"><a id="__codelineno-20-49" name="__codelineno-20-49" href="#__codelineno-20-49"></a> <span class="n">Movie</span><span class="p">(</span>
</span><span id="__span-20-50"><a id="__codelineno-20-50" name="__codelineno-20-50" href="#__codelineno-20-50"></a> <span class="n">id_</span><span class="o">=</span><span class="n">meta</span><span class="p">[</span><span class="s2">&quot;id&quot;</span><span class="p">],</span>
</span><span id="__span-20-51"><a id="__codelineno-20-51" name="__codelineno-20-51" href="#__codelineno-20-51"></a> <span class="n">service</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="p">,</span>
</span><span id="__span-20-52"><a id="__codelineno-20-52" name="__codelineno-20-52" href="#__codelineno-20-52"></a> <span class="n">name</span><span class="o">=</span><span class="n">meta</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">],</span>
</span><span id="__span-20-53"><a id="__codelineno-20-53" name="__codelineno-20-53" href="#__codelineno-20-53"></a> <span class="n">year</span><span class="o">=</span><span class="n">meta</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;year&quot;</span><span class="p">)</span> <span class="ow">or</span> <span class="kc">None</span><span class="p">,</span>
</span><span id="__span-20-54"><a id="__codelineno-20-54" name="__codelineno-20-54" href="#__codelineno-20-54"></a> <span class="n">language</span><span class="o">=</span><span class="n">meta</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;originalLanguage&quot;</span><span class="p">),</span>
</span><span id="__span-20-55"><a id="__codelineno-20-55" name="__codelineno-20-55" href="#__codelineno-20-55"></a> <span class="n">data</span><span class="o">=</span><span class="n">meta</span><span class="p">,</span>
</span><span id="__span-20-56"><a id="__codelineno-20-56" name="__codelineno-20-56" href="#__codelineno-20-56"></a> <span class="p">)</span>
</span><span id="__span-20-57"><a id="__codelineno-20-57" name="__codelineno-20-57" href="#__codelineno-20-57"></a> <span class="p">])</span>
</span><span id="__span-20-58"><a id="__codelineno-20-58" name="__codelineno-20-58" href="#__codelineno-20-58"></a>
</span><span id="__span-20-59"><a id="__codelineno-20-59" name="__codelineno-20-59" href="#__codelineno-20-59"></a> <span class="k">def</span><span class="w"> </span><span class="nf">get_tracks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tracks</span><span class="p">:</span>
</span><span id="__span-20-60"><a id="__codelineno-20-60" name="__codelineno-20-60" href="#__codelineno-20-60"></a> <span class="n">playback</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
</span><span id="__span-20-61"><a id="__codelineno-20-61" name="__codelineno-20-61" href="#__codelineno-20-61"></a> <span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;endpoints&quot;</span><span class="p">][</span><span class="s2">&quot;playback&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="n">title</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span><span id="__span-20-62"><a id="__codelineno-20-62" name="__codelineno-20-62" href="#__codelineno-20-62"></a> <span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span><span id="__span-20-63"><a id="__codelineno-20-63" name="__codelineno-20-63" href="#__codelineno-20-63"></a> <span class="k">return</span> <span class="n">DASH</span><span class="o">.</span><span class="n">from_url</span><span class="p">(</span>
</span><span id="__span-20-64"><a id="__codelineno-20-64" name="__codelineno-20-64" href="#__codelineno-20-64"></a> <span class="n">url</span><span class="o">=</span><span class="n">playback</span><span class="p">[</span><span class="s2">&quot;manifest_url&quot;</span><span class="p">],</span> <span class="n">session</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">session</span>
</span><span id="__span-20-65"><a id="__codelineno-20-65" name="__codelineno-20-65" href="#__codelineno-20-65"></a> <span class="p">)</span><span class="o">.</span><span class="n">to_tracks</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="n">title</span><span class="o">.</span><span class="n">language</span><span class="p">)</span>
</span><span id="__span-20-66"><a id="__codelineno-20-66" name="__codelineno-20-66" href="#__codelineno-20-66"></a>
</span><span id="__span-20-67"><a id="__codelineno-20-67" name="__codelineno-20-67" href="#__codelineno-20-67"></a> <span class="k">def</span><span class="w"> </span><span class="nf">get_chapters</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="n">Title_T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Chapters</span><span class="p">:</span>
</span><span id="__span-20-68"><a id="__codelineno-20-68" name="__codelineno-20-68" href="#__codelineno-20-68"></a> <span class="k">return</span> <span class="n">Chapters</span><span class="p">()</span>
</span><span id="__span-20-69"><a id="__codelineno-20-69" name="__codelineno-20-69" href="#__codelineno-20-69"></a>
</span><span id="__span-20-70"><a id="__codelineno-20-70" name="__codelineno-20-70" href="#__codelineno-20-70"></a> <span class="k">def</span><span class="w"> </span><span class="nf">get_widevine_license</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">challenge</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">track</span><span class="p">):</span>
</span><span id="__span-20-71"><a id="__codelineno-20-71" name="__codelineno-20-71" href="#__codelineno-20-71"></a> <span class="n">res</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s2">&quot;endpoints&quot;</span><span class="p">][</span><span class="s2">&quot;license&quot;</span><span class="p">],</span> <span class="n">data</span><span class="o">=</span><span class="n">challenge</span><span class="p">)</span>
</span><span id="__span-20-72"><a id="__codelineno-20-72" name="__codelineno-20-72" href="#__codelineno-20-72"></a> <span class="n">res</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span><span id="__span-20-73"><a id="__codelineno-20-73" name="__codelineno-20-73" href="#__codelineno-20-73"></a> <span class="k">return</span> <span class="n">res</span><span class="o">.</span><span class="n">content</span>
</span></code></pre></div>
<div class="language-yaml highlight"><span class="filename">services/MYSVC/config.yaml</span><pre><span></span><code><span id="__span-21-1"><a id="__codelineno-21-1" name="__codelineno-21-1" href="#__codelineno-21-1"></a><span class="nt">endpoints</span><span class="p">:</span>
</span><span id="__span-21-2"><a id="__codelineno-21-2" name="__codelineno-21-2" href="#__codelineno-21-2"></a><span class="w"> </span><span class="nt">metadata</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">https://api.myservice.com/v1/metadata/{id}</span>
</span><span id="__span-21-3"><a id="__codelineno-21-3" name="__codelineno-21-3" href="#__codelineno-21-3"></a><span class="w"> </span><span class="nt">playback</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">https://api.myservice.com/v1/playback/{id}</span>
</span><span id="__span-21-4"><a id="__codelineno-21-4" name="__codelineno-21-4" href="#__codelineno-21-4"></a><span class="w"> </span><span class="nt">license</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">https://api.myservice.com/v1/license/widevine</span>
</span></code></pre></div>
<p>Test it with:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-22-1"><a id="__codelineno-22-1" name="__codelineno-22-1" href="#__codelineno-22-1"></a>unshackle<span class="w"> </span>dl<span class="w"> </span>MYSVC<span class="w"> </span>https://myservice.com/watch/abc123
</span></code></pre></div>
<hr />
<h2 id="checklist">Checklist<a class="headerlink" href="#checklist" title="Permanent link">&para;</a></h2>
<ul class="task-list">
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Folder name, class name, and Click <code>name=</code> all match the tag exactly.</li>
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> <code>cli</code> is a <code>@staticmethod</code> returning an instance; <code>__init__</code> calls <code>super().__init__(ctx)</code>.</li>
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> <code>get_titles</code>, <code>get_tracks</code>, <code>get_chapters</code> implemented; titles carry the original <code>language</code>.</li>
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Tracks come from a manifest parser with <code>to_tracks(language=title.language)</code>; no filtering by quality/language.</li>
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Encrypted tracks are marked with DRM; the license callbacks you need return responses <strong>unmodified</strong>.</li>
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> URLs, user agents, and certificates live in <code>config.yaml</code>, read via <code>self.config[...]</code>.</li>
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Multi-file services import their own modules relatively (<code>from .helpers import x</code>), so they work when loaded from a repo.</li>
</ul>
<p>Read <code>unshackle/services/EXAMPLE/</code> end to end; it annotates every feature
touched above in one place.</p>
</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="../../reference/configuration/example/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Annotated Example">
<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">
Annotated Example
</div>
</div>
</a>
<a href="../service-architecture/" class="md-footer__link md-footer__link--next" aria-label="Next: Service Architecture">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Service Architecture
</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>