├── .gitignore ├── README.md ├── cache └── .empty ├── classes ├── AbstractKabelDeutschland.php ├── KabelDeutschland.php └── Log.php ├── config └── config.php ├── kd.php └── logs └── .empty /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 4 | 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .idea/ 9 | # if you remove the above rule, at least ignore the following: 10 | 11 | # User-specific stuff: 12 | # .idea/workspace.xml 13 | # .idea/tasks.xml 14 | # .idea/dictionaries 15 | 16 | # Sensitive or high-churn files: 17 | # .idea/dataSources.ids 18 | # .idea/dataSources.xml 19 | # .idea/sqlDataSources.xml 20 | # .idea/dynamic.xml 21 | # .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | # .idea/gradle.xml 25 | # .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | # .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.ipr 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KabelDeutschland simple streaming proxy 2 | 3 | As all of you KabelDeutschland customers know, there is an existing iOS App that provides full TV-channel streaming if your are a paying customer of both cable internet and tv. 4 | Unfortunatly there is no released Android nor Desktop version of this streaming possibility. KabelDeutschland kept us in the dark, if and when there will be such an option. 5 | 6 | That's why i built this script to be able to watch all of the channels on my desktop and also with XBMC/KODI. 7 | 8 | Beside this script, there is now an even simpler version. You can download the binary from my page [freshest.me/kd](http://freshest.me/simplified-kabeldeutschland-streaming-proxy/). 9 | This version was build with golang and handles all functions in a simple executable. Mainly the new version was designed for every-day user who do not want to setup their own web-server but simply start the proxy. 10 | 11 | The version of the proxy, written in GO, is now also available as open-source on [github/edi-design/kd-go](https://github.com/edi-design/kd-go). 12 | 13 | 14 | ## Requirements 15 | 16 | ### Contract 17 | 18 | * KabelDeutschland Cable TV Package (the smallest one for 2,95€ is enough) 19 | * KabelDeutschland Internet via cable 20 | 21 | The first package is needed because the KD backend ensure with your credentials that you are a paying customer. The cable internet contract is needed cause the final stream link is available only in their network. So you can't connect and watch from any other ip-address outside of the KD network. 22 | 23 | ### Software 24 | 25 | * webserver with php installed 26 | * php5_curl (`apt-get install php5-curl) 27 | 28 | 29 | ## Installation 30 | 31 | Just copy the content of this repository to your webserver and configure it with your credentials. 32 | 33 | ## Configuration 34 | 35 | * add your credentials instead of the placeholders. These credentials are the same you use to login into the kd customer-service-center. 36 | 37 | ## Run 38 | 39 | Point your browser to your webserver for example http://localhost:8000/kd.php. 40 | That call provides you with the download of a playlist file. This m3u-file can opened with vlc and contains the redirect links. 41 | 42 | You can also add the url http://localhost:8000/kd.php directly to VLC. This will resolve the playlist automatically. 43 | 44 | Once imported into your VLC player, the scripts handle the generation of a licensed stream link and forwards the player automatically to this link. 45 | 46 | There is a param named `quality` which can be `low` `medium` and `high`. This determines the stream bandwidth. 47 | Try `http://localhost:8000/kd.php?quality=high` for the highest quality. Medium is the default if no param was given. 48 | 49 | This new version now works with Kodi / XBMC (PVR IPTV Plugin) and also the tv-stream-recorder for Synology based NAS-systems. 50 | 51 | The first call of the script will take about a minute to generate the playlist. So please be patient. The next time it will be cached for 24 hours. 52 | To use the cache, you web-server needs to have write acces to the folder `cache`. 53 | 54 | If you use the script on a Synology NAS the write access will be controlled with the Control-Panel -> Groups. The http group needs read/write access to the web folder. 55 | 56 | ## Debug 57 | 58 | There are three steps of debugging included. 59 | The simplest one is to call the script with format=txt to get a the textual output of the playlist. 60 | http://localhost:8000/kd.php?format=txt 61 | 62 | The more detailed version works with log_level=debug and writes al lot of data to logs/*.log. 63 | http://localhost:8000/kd.php?format=txt&log_level=debug 64 | 65 | # TODOs 66 | 67 | * UDID generation 68 | 69 | # Explanation 70 | 71 | To learn about how this script works, see [freshest.me](https://freshest.me). 72 | -------------------------------------------------------------------------------- /cache/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-streaming-proxy/720dfef9f868f968c8f8db086d73786adaf65034/cache/.empty -------------------------------------------------------------------------------- /classes/AbstractKabelDeutschland.php: -------------------------------------------------------------------------------- 1 | null, 'password' => null); 21 | 22 | /** 23 | * api config 24 | * @var array 25 | */ 26 | protected $api = array('version' => null, 'device' => null, 'ios_version' => null, 'udid' => null); 27 | 28 | /** 29 | * @var array 30 | */ 31 | protected $methods = array('sign_in' => null, 'channel_list' => null); 32 | 33 | protected $base_url = ''; 34 | 35 | protected $init_object = array('initObj' => null); 36 | 37 | /** 38 | * Logger 39 | * @var Log 40 | */ 41 | protected $obj_log = null; 42 | 43 | /** 44 | * initialized cache file with directory 45 | * @var null 46 | */ 47 | private $cache_file = null; 48 | 49 | private $cache_file_format = 'playlist_%s.m3u'; 50 | 51 | /** 52 | * constructor 53 | * 54 | * @param $arr_config 55 | * @param $arr_api_config 56 | */ 57 | public function __construct($arr_config, $arr_api_config) 58 | { 59 | $this->base_config = $arr_api_config['base_config']; 60 | $this->credentials = $arr_config['credentials']; 61 | $this->api = $arr_config['api']; 62 | $this->methods = $arr_api_config['methods']; 63 | 64 | // init cache 65 | $cache_folder = dirname(__FILE__) . '/../cache/'; 66 | $this->cache_file = $cache_folder . $this->cache_file_format; 67 | } 68 | 69 | /** 70 | * main entry method 71 | */ 72 | public function run() 73 | { 74 | // get configuration from server 75 | $this->initConfigFromKabelDeutschland(); 76 | } 77 | 78 | /** 79 | * @param null $obj_log 80 | */ 81 | public function setObjLog($obj_log) 82 | { 83 | $this->obj_log = $obj_log; 84 | } 85 | 86 | /** 87 | * handles all curl request 88 | * 89 | * @param $url 90 | * @param $arr_params 91 | * @param bool $bool_init 92 | * @return mixed 93 | */ 94 | protected function get($url, $arr_params, $bool_init = false) 95 | { 96 | if (!$bool_init) 97 | { 98 | // merge init params from config call 99 | $arr_params = array_merge($arr_params, $this->init_object); 100 | } 101 | 102 | $params = json_encode($arr_params, JSON_UNESCAPED_SLASHES); 103 | 104 | $ch = curl_init($url); 105 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); 106 | curl_setopt($ch, CURLOPT_POSTFIELDS, $params); 107 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 108 | curl_setopt($ch, CURLOPT_HTTPHEADER, array( 109 | 'Content-Type: application/json', 110 | 'Content-Length: ' . strlen($params)) 111 | ); 112 | 113 | $result = curl_exec($ch); 114 | $this->obj_log->logResult($url, $result); 115 | 116 | return json_decode($result, true); 117 | } 118 | 119 | /** 120 | * returns a full url according to given method 121 | * 122 | * @param $method 123 | * @return string 124 | */ 125 | protected function generateUrl($method) 126 | { 127 | return $this->base_url . 128 | '?m=' . $this->methods[$method]. 129 | '&iOSv=' . $this->api['ios_version']. 130 | '&Appv=' . $this->api['version']; 131 | } 132 | 133 | /** 134 | * calls main kd-config backend to retrieve user-specific vars 135 | */ 136 | protected function initConfigFromKabelDeutschland() 137 | { 138 | $url = $this->base_config['config_url'] . '?'; 139 | foreach($this->base_config['params'] as $name => $value) 140 | { 141 | $url .= urlencode($name).'='.urlencode($value).'&'; 142 | } 143 | $url = substr($url, 0, strlen($url)-1); 144 | 145 | $arr_config = $this->get($url, array(), true); 146 | 147 | $this->base_url = $arr_config['params']['Gateways'][0]['JsonGW']; 148 | 149 | // init obj has wrong format 150 | $arr_init = $arr_config['params']['InitObj']; 151 | foreach ($arr_init as $element) 152 | { 153 | foreach ($element as $key => $value) 154 | { 155 | if (!is_array($value)) 156 | { 157 | $this->init_object['initObj'][$key] = $value; 158 | } else { 159 | foreach ($value as $sub_element) 160 | { 161 | foreach ($sub_element as $sub_key => $sub_value) 162 | { 163 | $this->init_object['initObj'][$key][$sub_key] = $sub_value; 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | // set hardcoded values 171 | $this->init_object['initObj']['UDID'] = $this->api['udid']; 172 | 173 | $this->obj_log->log('initConfig', json_encode($this->init_object)); 174 | } 175 | 176 | /** 177 | * @return null 178 | */ 179 | public function getCacheFile($quality) 180 | { 181 | return sprintf($this->cache_file, $quality); 182 | } 183 | } -------------------------------------------------------------------------------- /classes/KabelDeutschland.php: -------------------------------------------------------------------------------- 1 | 'CCURstream564000.m3u8', 33 | 'medium' => 'CCURstream1064000.m3u8', 34 | 'high' => 'CCURstream1664000.m3u8' 35 | ); 36 | 37 | /** 38 | * lifetime 1 day 39 | * @var int 40 | */ 41 | protected $cache_lifetime = 86400; // 60s * 60m * 24h = 1day in seconds 42 | 43 | /** 44 | * main entry point 45 | */ 46 | public function run() 47 | { 48 | /* 49 | * initial set-up calls 50 | */ 51 | parent::run(); 52 | $this->signIn(); 53 | 54 | $arr_params = $_GET; 55 | 56 | $arr_channels = $this->getChannelList(); 57 | $this->showChannelList($arr_params, $arr_channels); 58 | } 59 | 60 | /** 61 | * handles the login procedure and sets some environment vars 62 | * needs to be called after domain init 63 | */ 64 | private function signIn() 65 | { 66 | $url = $this->generateUrl('sign_in'); 67 | 68 | $arr_params = array( 69 | 'userName' => $this->credentials['username'], 70 | 'password' => $this->credentials['password'], 71 | "providerID" => 0, 72 | ); 73 | 74 | $arr_session = $this->get($url, $arr_params); 75 | 76 | $this->init_object['initObj']['SiteGuid'] = $arr_session['SiteGuid']; 77 | $this->init_object['initObj']['DomainID'] = $arr_session['DomainID']; 78 | 79 | $this->obj_log->log('signIn', json_encode($this->init_object)); 80 | } 81 | 82 | /** 83 | * get list of channels from kd backend 84 | * @return array 85 | */ 86 | private function getChannelList() 87 | { 88 | $url = $this->generateUrl('channellist'); 89 | 90 | $arr_params = array( 91 | "orderBy" => $this->api['default_channelorder'], 92 | "pageSize" => $this->api['default_pagesize'], 93 | "picSize" => $this->api['default_picsize'], 94 | "ChannelID" => $this->api['default_channelid'] 95 | ); 96 | 97 | $arr_channel = $this->get($url, $arr_params); 98 | 99 | $arr_return = array(); 100 | foreach ($arr_channel as $channel) 101 | { 102 | if (!empty($channel['Files'][0]['URL'])) 103 | { 104 | $arr_return[] = array( 105 | 'link' => trim($channel['Files'][0]['URL']), 106 | 'id' => $channel['Files'][0]['FileID'], 107 | 'title' => $channel['MediaName'] 108 | ); 109 | } 110 | } 111 | 112 | $this->obj_log->log('channelList', 'Count: '. count($arr_return)); 113 | 114 | return $arr_return; 115 | } 116 | 117 | /** 118 | * handles head and output for channellist overview 119 | * 120 | * @param $arr_params 121 | * @param $arr_channels 122 | */ 123 | private function showChannelList($arr_params, $arr_channels) 124 | { 125 | // quality selection 126 | $quality = isset($arr_params['quality']) ? $arr_params['quality'] : 'medium'; 127 | $playlist = isset($this->arr_quality[$quality]) ? $this->arr_quality[$quality] : $this->arr_quality['medium']; 128 | 129 | // Manage the output format 130 | $format = isset($arr_params['format']) ? $arr_params['format'] : 'm3u'; 131 | switch($format) 132 | { 133 | case 'txt': 134 | $mime_type = 'text/plain'; 135 | break; 136 | case 'm3u': 137 | default: 138 | $mime_type = 'application/vnd.apple.mpegurl'; 139 | break; 140 | } 141 | 142 | // Output headers and the playlist contents 143 | header('Status: 200 OK'); 144 | header("Content-Type: $mime_type"); 145 | header('Content-Disposition: inline; filename="playlist.m3u"'); 146 | header('Cache-Control: no-cache, must-revalidate'); 147 | header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); 148 | 149 | // check cache 150 | $cached = false; 151 | if (file_exists($this->getCacheFile($quality))) 152 | { 153 | $file_mtime = filemtime($this->getCacheFile($quality)); 154 | if ((time() - $file_mtime) <= $this->cache_lifetime) 155 | { 156 | $cached = true; 157 | } 158 | } 159 | 160 | if ($cached === false) 161 | { 162 | $bool_save_to_cache = true; 163 | 164 | // re-call licensed links 165 | $output = $this->m3u_head; 166 | foreach ($arr_channels as $channel) { 167 | $link = $this->getLicensedLink($channel['id'], $channel['link']); 168 | if (empty($link)) 169 | { 170 | // can't get licensed link, abort channel-list request 171 | $output = "Can't get licensed link, try the golang version from http://freshest.me"; 172 | $this->obj_log->log('licensedLink', $output); 173 | $bool_save_to_cache = false; 174 | break; 175 | } 176 | $link = $this->getRedirectTarget($link); 177 | if (empty($link)) 178 | { 179 | // if redirect does not work, skip channel in list 180 | $this->obj_log->log('licensedLink', 'Can not get Channel link from CDN for '. $channel['title']); 181 | continue; 182 | } 183 | 184 | // remove given playlist and replace it with direct ts-stream-playlist 185 | $link = substr($link, 0, strrpos($link, '/')) . '/' . $playlist; 186 | 187 | $output .= sprintf($this->m3u_line, $channel['title'], $link); 188 | } 189 | 190 | // save playlist as cached file 191 | if ($bool_save_to_cache) 192 | { 193 | file_put_contents($this->getCacheFile($quality), $output); 194 | } 195 | } else { 196 | // read from cache 197 | $output = file_get_contents($this->getCacheFile($quality)); 198 | } 199 | 200 | // return the playlist 201 | echo $output; 202 | } 203 | 204 | /** 205 | * returns the final destination of a redirected link 206 | * 207 | * @param $destination 208 | * @return mixed 209 | */ 210 | private function getRedirectTarget($destination) 211 | { 212 | $ch = curl_init($destination); 213 | curl_setopt($ch, CURLOPT_HEADER, 1); 214 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 215 | $headers = curl_exec($ch); 216 | curl_close($ch); 217 | 218 | // Check if there's a Location: header (redirect) 219 | if (preg_match('/^Location: (.+)$/im', $headers, $matches)) 220 | { 221 | return trim($matches[1]); 222 | } 223 | 224 | // no redirect means problem with KabelDeutschland CDN 225 | return null; 226 | } 227 | 228 | /** 229 | * calls the kd backend to generate a valid stream link for the given channel 230 | * 231 | * @param $media_file_id 232 | * @param $base_link 233 | * @return mixed 234 | */ 235 | private function getLicensedLink($media_file_id, $base_link) 236 | { 237 | $url = $this->generateUrl('licensedlink'); 238 | 239 | $arr_params = array( 240 | "mediaFileID" => (int)$media_file_id, 241 | "baseLink" => $base_link 242 | ); 243 | 244 | $arr_link = $this->get($url, $arr_params); 245 | 246 | $this->obj_log->log('licensedLink', json_encode(array('params' => $arr_params, 'return' => $arr_link))); 247 | 248 | return $arr_link['mainUrl']; 249 | } 250 | 251 | } -------------------------------------------------------------------------------- /classes/Log.php: -------------------------------------------------------------------------------- 1 | generic_log = $log_folder . $arr_config['generic_log']; 49 | $this->result_log = $log_folder . $arr_config['result_log']; 50 | } 51 | 52 | /** 53 | * @param null $log_level 54 | */ 55 | public function setLogLevel($log_level) 56 | { 57 | $this->log_level = $log_level; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getLogLevel() 64 | { 65 | return $this->log_level; 66 | } 67 | 68 | /** 69 | * generic log-writer 70 | * 71 | * @param $namespace 72 | * @param $message 73 | */ 74 | public function log($namespace, $message) 75 | { 76 | switch ($this->log_level) 77 | { 78 | case self::LOG_LEVEL_DEBUG: 79 | case self::LOG_LEVEL_ERROR: 80 | $message = '['.date('Y-m-d H:m:s').'] - ['.$namespace.']' . "\n" . $message . "\n"; 81 | file_put_contents($this->generic_log, $message, FILE_APPEND); 82 | break; 83 | } 84 | } 85 | 86 | /** 87 | * result log-writer, only if log-level is debug 88 | * 89 | * @param $namespace 90 | * @param $message 91 | */ 92 | public function logResult($namespace, $message) 93 | { 94 | switch ($this->log_level) 95 | { 96 | case self::LOG_LEVEL_DEBUG: 97 | $message = 98 | '['.date('Y-m-d H:m:s').'] - ['.$namespace.']' . "\n" . 99 | preg_replace("/\r|\n/",'',$message) . "\n"; 100 | 101 | file_put_contents($this->result_log, $message, FILE_APPEND); 102 | break; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | array( 17 | 'username' => '##CSC_USERNAME###', 18 | 'password' => '##CSC_PASSWORD###' 19 | ), 20 | 'api' => array( 21 | 'version' => '1.1.5', 22 | 'device' => 'iPad', 23 | 'ios_version' => '8.1.2', 24 | 'udid' => 'D2AC633AFB644CF0A67505370578CDE5', 25 | 'default_channelorder' => 'None', 26 | 'default_channelid' => 340758, 27 | 'default_picsize' => '100X100', 28 | 'default_pagesize' => 1000, 29 | ), 30 | 'log' => array( 31 | 'generic_log' => 'generic.log', 32 | 'result_log' => 'result.log' 33 | ) 34 | ); 35 | 36 | /** 37 | * KabelDeutschland configuration 38 | * - no need to change anything 39 | */ 40 | $arr_api_config = array( 41 | 'base_config' => array( 42 | 'config_url' => 'https://dms-live.iptv.kabel-deutschland.de/api.svc/getconfig', 43 | 'params' => array( 44 | 'username' => 'kdg', 45 | 'password' => 'DKNaAHvuuaTkhPN8rtTD', 46 | 'appname' => 'com.kabeldeutschland.tvapp', 47 | 'cver' => '1.1.5', 48 | 'platform' => 'iOS', 49 | 'udid' => $arr_config['api']['udid'] 50 | ) 51 | ), 52 | 'methods' => array( 53 | 'sign_in' => 'SSOSignIn', 54 | 'channellist' => 'GetChannelMediaList', 55 | 'licensedlink' => 'GetLicensedLinks', 56 | 'devicedomains' => 'GetDeviceDomains' 57 | ) 58 | ); -------------------------------------------------------------------------------- /kd.php: -------------------------------------------------------------------------------- 1 | setLogLevel($log_level); 38 | 39 | /* 40 | * init main class 41 | */ 42 | $obj_kd = new KabelDeutschland($arr_config, $arr_api_config); 43 | $obj_kd->setObjLog($obj_logger); 44 | 45 | // run 46 | $obj_kd->run(); -------------------------------------------------------------------------------- /logs/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edi-design/kd-streaming-proxy/720dfef9f868f968c8f8db086d73786adaf65034/logs/.empty --------------------------------------------------------------------------------