# -*- coding: utf-8 -*-
import json
import requests
import hashlib
import re
import random
import string
import copy

import libmediathek4utils as lm4utils

#profiles: 
#default
#player
#player-3
#video-app
#video-app-teaser
#current-video-type
#mit-xperts-2 - referenced in the hbbtv mediathek

log = lm4utils.log


class parser:
	def __init__(self):
		self.result = {'items':[],'pagination':{'currentPage':0}}
		self.template = {'params':{}, 'metadata':{'art':{}}, 'type':'video'}
		self.playerId = 'ngplayer_2_4'
		#self.playerId = 'ngplayer_2_4'
		#self.playerId = 'ngplayer_2_5'
		#self.playerId = 'ngplayer_2_2_modul'
		#self.playerId = 'chromecast_1'
		#self.playerId = 'android_native_1'
		#self.playerId = 'android_native_2'
		#self.playerId = 'android_native_3'
		#self.playerId = 'smarttv_1'
		#self.playerId = 'smarttv_2'
		#self.playerId = 'smarttv_3'
		#self.playerId = 'smarttv_4'
		#self.playerId = 'smarttv_5'
		#self.playerId = 'ios_native_1'
		#self.playerId = 'ios_native_2'
		#self.playerId = 'voice_1'
		#self.playerId = 'portal'

	def _getTokenFromUrl(self):
		# Bastelpingu Hotfix 5.0.3 (2026-08-16):
		# zdf-cdn.live.cellular.de is no longer resolvable.
		tokenUrl = self.tokenUrl
		if tokenUrl and 'zdf-cdn.live.cellular.de' in tokenUrl:
			tokenUrl = tokenUrl.replace(
				'https://zdf-cdn.live.cellular.de',
				'https://zdf-prod-futura.zdf.de'
			)
		r = requests.get(tokenUrl, timeout=15)
		r.raise_for_status()
		data = r.json()
		token = data.get('token')
		if not token:
			raise RuntimeError('ZDF token response did not contain a token')
		lm4utils.f_mkdir(lm4utils.pathUserdata(''))
		lm4utils.f_write(lm4utils.pathUserdata('token'), token)
		return token

	def _getTokenFromAPI(self):
		headers = {'user-agent': self.userAgent,'Host':self.baseApi.split('/')[2]}
		r = requests.get(f'{self.baseApi}/oauth/getApiToken',headers=headers)
		wwwAuth = r.headers['WWW-Authenticate']

		realm      = re.compile('Digest realm="(.+?)"', re.DOTALL).findall(wwwAuth)[0]
		qop        = re.compile('qop="(.+?)"', re.DOTALL).findall(wwwAuth)[0]
		nonce      = re.compile('nonce="(.+?)"', re.DOTALL).findall(wwwAuth)[0]
		uri        = f'{self.baseApi}/oauth/getApiToken'
		nonceCount = '00000002'
		cnonce     = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))

		HA1        = hashlib.md5(f'{self.API_CLIENT_ID}:{realm}:{self.API_CLIENT_KEY}'.encode('utf-8')).hexdigest()
		HA2        = hashlib.md5(f'GET:{uri}'.encode('utf-8')).hexdigest()
		response   = hashlib.md5(f'{HA1}:{nonce}:{nonceCount}:{cnonce}:{qop}:{HA2}'.encode('utf-8')).hexdigest()

		headers['authorization'] = f'Digest username="{self.API_CLIENT_ID}", realm="{realm}", nonce="{nonce}", uri="{uri}", response="{response}", opaque="null", qop={qop}, nc={nonceCount}, cnonce="{cnonce}"'

		j = requests.get(f'{self.baseApi}/oauth/getApiToken',headers=headers).json()

		token = j['apiToken']
		lm4utils.f_mkdir(lm4utils.pathUserdata(''))
		lm4utils.f_write(lm4utils.pathUserdata('token'), token)
		return token

	def _getU(self,url,Menu=False):
		token = lm4utils.f_open(lm4utils.pathUserdata('token'))
		if token == '':
			token = self._getToken()
		header = {'Api-Auth': f'Bearer {token}', 'Accept-Encoding': 'gzip, deflate'}
		response = requests.get(url,headers=header)

		# Bastelpingu Hotfix 5.0.3:
		# Bei abgelaufenem/ungültigem Cache-Token einmal neu holen.
		if response.status_code in (401, 403) or response.text == '':
			token = self._getToken()
			header = {'Api-Auth': f'Bearer {token}', 'Accept-Encoding': 'gzip, deflate'}
			response = requests.get(url,headers=header)
		return response.text

	def _getToken(self):
		if self.tokenUrl:
			return self._getTokenFromUrl()
		else:
			return self._getTokenFromAPI()

	def parsePage(self,url):
		response = self._getU(url,True)
		j = json.loads(response)
		if   j['profile'] == 'http://zdf.de/rels/search/result':
			return self._parseSearch(j)
		elif j['profile'] == 'http://zdf.de/rels/search/result-page':
			return self._parseSearchPage(j)
		elif j['profile'] in ['http://zdf.de/rels/content/page-index','http://zdf.de/rels/content/page-index-video-app']:
			#return self._parsePageIndex(j)
			return self._parsePageIndex2(j)
		elif j['profile'] in ['http://zdf.de/rels/content/content-filter','http://zdf.de/rels/content/content-filter-page-video_episode_vod']:
			return self._parseContentFilter(j)
		#elif j['profile'] in ['http://zdf.de/rels/content/page-index-teaser','http://zdf.de/rels/content/page-index-video-app-teaser']:
		#	return self._parseTeaser(j)#not implemented atm
		elif j['profile'] == 'http://zdf.de/rels/content/special-page-live-tv-video-app':
			return self._parseTV(j)
		elif j['profile'] == 'http://zdf.de/rels/cmdm/resultpage-broadcasts':
			return self._parseBroadcast(j)
		else:
			profile = j.get('profile', '<missing>')
			log('Unknown profile: ' + str(profile))
			raise RuntimeError(
				'Unknown ZDF profile: ' + str(profile)
			)

	def getAZ(self, uri=None):
	    url = (
	        self.baseApi
	        + '/search/documents'
	        + '?q=%2A'
	        + '&contentTypes=brand'
	        + '&limit=100'
	    )
	    return self.parsePage(url)

	def _firstImageUrl(self, obj):
	    if isinstance(obj, str):
	        if obj.startswith('http://') or obj.startswith('https://'):
	            return obj
	        return None

	    if isinstance(obj, dict):
	        preferred = [
	            '1920x1080',
	            '1280x720',
	            '768x432',
	            '384x216',
	            'url',
	            'src',
	        ]

	        for key in preferred:
	            if key in obj:
	                result = self._firstImageUrl(obj[key])
	                if result:
	                    return result

	        for value in obj.values():
	            result = self._firstImageUrl(value)
	            if result:
	                return result

	    if isinstance(obj, list):
	        for value in obj:
	            result = self._firstImageUrl(value)
	            if result:
	                return result

	    return None


	def _setFuturaMetadata(self, d, target):
	    d['metadata']['name'] = (
	        target.get('titel')
	        or target.get('title')
	        or target.get('teaserHeadline')
	        or target.get('headline')
	        or 'ZDF'
	    )

	    d['metadata']['plot'] = (
	        target.get('beschreibung')
	        or target.get('teasertext')
	        or target.get('shortText')
	        or ''
	    )

	    image = None

	    for key in (
	        'teaserBild',
	        'teaser',
	        'image',
	        'logo',
	    ):
	        if key in target:
	            image = self._firstImageUrl(
	                target[key]
	            )
	            if image:
	                break

	    if image:
	        d['metadata']['art']['thumb'] = image

	    if target.get('seasonNumber') is not None:
	        d['metadata']['season'] = target.get(
	            'seasonNumber'
	        )

	    if target.get('episodeNumber') is not None:
	        d['metadata']['episode'] = target.get(
	            'episodeNumber'
	        )

	    length = (
	        target.get('length')
	        or target.get('duration')
	    )

	    if length:
	        d['metadata']['duration'] = length


	def _futuraItem(
	    self,
	    target,
	    playId=None,
	    directoryId=None
	):
	    d = copy.deepcopy(self.template)

	    self._setFuturaMetadata(
	        d,
	        target
	    )

	    contentType = target.get(
	        'contentType'
	    )

	    isVideo = bool(playId)

	    if not isVideo:
	        isVideo = (
	            target.get('type') == 'video'
	            or contentType in (
	                'episode',
	                'clip',
	                'video',
	            )
	        )

	    if isVideo:
	        ident = (
	            playId
	            or target.get('id')
	            or target.get('canonical')
	        )

	        if not ident:
	            return None

	        d['params']['mode'] = (
	            'libZdfPlayById'
	        )

	        d['params']['id'] = ident

	        if contentType == 'episode':
	            d['type'] = 'episode'
	        else:
	            d['type'] = 'video'

	        return d

	    ident = (
	        directoryId
	        or target.get('canonical')
	        or target.get('id')
	    )

	    if not ident:
	        return None

	    d['params']['mode'] = (
	        'libZdfListFuturaDocument'
	    )

	    d['params']['id'] = ident
	    d['type'] = 'dir'

	    return d


	def getTrending(self):
	    operation = 'GetClusterList'

	    sha256 = (
	        'ad69192ddc2fdd9a33048137e944fb47'
	        '1011cab80eabd4e9f9a64f255246be97'
	    )

	    variables = {
	        'collectionId':
	            'b82972dd-2688-49d0-84a3-70d7083611b7',
	        'configuration': 'dkdi',
	        'input': {
	            'appId': 'zdf-web-5be2cdce',
	            'filters': {},
	            'pagination': {
	                'first': 24
	            },
	            'user': {
	                'abGroup': 'gruppe-a',
	                'userSegment': 'segment_0'
	            }
	        }
	    }

	    extensions = {
	        'clientLibrary': {
	            'name': '@apollo/client',
	            'version': '4.1.6'
	        },
	        'persistedQuery': {
	            'version': 1,
	            'sha256Hash': sha256
	        }
	    }

	    token = lm4utils.f_open(
	        lm4utils.pathUserdata('token')
	    )

	    if not token:
	        token = self._getToken()

	    headers = {
	        'Api-Auth': 'Bearer ' + token,
	        'Accept': 'application/json',
	        'x-apollo-operation-name':
	            operation,
	        'User-Agent':
	            'ffw-mt-web-2c770629',
	        'Referer':
	            'https://www.zdf.de/',
	    }

	    params = {
	        'operationName': operation,
	        'variables': json.dumps(
	            variables,
	            separators=(',', ':')
	        ),
	        'extensions': json.dumps(
	            extensions,
	            separators=(',', ':')
	        ),
	    }

	    response = requests.get(
	        'https://api.zdf.de/graphql',
	        headers=headers,
	        params=params,
	        timeout=15
	    )

	    if response.status_code in (
	        401,
	        403
	    ):
	        token = self._getToken()

	        headers['Api-Auth'] = (
	            'Bearer ' + token
	        )

	        response = requests.get(
	            'https://api.zdf.de/graphql',
	            headers=headers,
	            params=params,
	            timeout=15
	        )

	    response.raise_for_status()

	    data = response.json()

	    clusters = (
	        data
	        .get('data', {})
	        .get('clusterlist', {})
	        .get('clusters', [])
	    )

	    result = {
	        'items': [],
	        'pagination': {
	            'currentPage': 0
	        }
	    }

	    seen = set()

	    for cluster in clusters:
	        for target in cluster.get(
	            'items',
	            []
	        ):
	            canonical = target.get(
	                'canonical'
	            )

	            video = target.get(
	                'video'
	            ) or {}

	            playId = None

	            if isinstance(video, dict):
	                playId = (
	                    video.get('canonical')
	                    or video.get('id')
	                )

	            key = (
	                playId
	                or canonical
	                or target.get('id')
	            )

	            if not key or key in seen:
	                continue

	            seen.add(key)

	            item = self._futuraItem(
	                target,
	                playId=playId,
	                directoryId=canonical
	            )

	            if item:
	                result['items'].append(
	                    item
	                )

	    return result


	def getFuturaDocument(self, ident):
	    url = (
	        'https://zdf-prod-futura.zdf.de'
	        '/mediathekV2/document/'
	        + ident
	    )

	    response = self._getU(
	        url,
	        True
	    )

	    data = json.loads(response)

	    document = data.get(
	        'document',
	        {}
	    )

	    result = {
	        'items': [],
	        'pagination': {
	            'currentPage': 0
	        }
	    }

	    result['name'] = (
	        document.get('titel')
	        or document.get('title')
	        or 'ZDF'
	    )

	    # Falls direkt ein Video-Dokument geöffnet wurde.
	    if (
	        document.get('type') == 'video'
	        or document.get('contentType') in (
	            'episode',
	            'clip',
	            'video',
	        )
	    ):
	        item = self._futuraItem(
	            document,
	            playId=(
	                document.get('id')
	                or ident
	            )
	        )

	        if item:
	            result['items'].append(
	                item
	            )

	        return result

	    seen = set()

	    for cluster in data.get(
	        'cluster',
	        []
	    ):
	        for target in cluster.get(
	            'teaser',
	            []
	        ):
	            targetId = (
	                target.get('id')
	                or target.get('canonical')
	            )

	            if (
	                not targetId
	                or targetId in seen
	            ):
	                continue

	            seen.add(targetId)

	            contentType = target.get(
	                'contentType'
	            )

	            isVideo = (
	                target.get('type') == 'video'
	                or contentType in (
	                    'episode',
	                    'clip',
	                    'video',
	                )
	            )

	            if isVideo:
	                item = self._futuraItem(
	                    target,
	                    playId=targetId
	                )
	            else:
	                item = self._futuraItem(
	                    target,
	                    directoryId=targetId
	                )

	            if item:
	                result['items'].append(
	                    item
	                )

	    return result










	def _parseSearch(self,j):
		for module in j['module']:
			for result in module['filterRef']['resultsWithVideo']['http://zdf.de/rels/search/results']:
				target = result['http://zdf.de/rels/target']
				self.template['metadata']['views'] = result['viewCount']
				self._grepItem(target)
		return self.result
				
	def _parseSearchPage(self,j):
		for result in j['http://zdf.de/rels/search/results']:
			target = result['http://zdf.de/rels/target']
			if not target['profile'] == 'http://zdf.de/rels/cmdm/broadcast-teaser':#filters out future broadcasts
				self._grepItem(target)
		return self.result
		
	def _parsePageIndex(self,j):
		for module in j['module']:
			if 'teaser' in module:
				for teaser in module['teaser']:
					self._grepItem(teaser['http://zdf.de/rels/target'])
		return self.result

	def _parsePageIndex2(self,j):
		for result in j['module'][0]['filterRef']['resultsWithVideo']['http://zdf.de/rels/search/results']:
			target = result['http://zdf.de/rels/target']
			self.template['metadata']['views'] = result['viewCount']
			self._grepItem(target)
		return self.result
		
	def _parseContentFilter(self,j):
		for result in j['resultsWithVideo']['http://zdf.de/rels/search/results']:
			target = result['http://zdf.de/rels/target']
			self.template['metadata']['views'] = result['viewCount']
			self._grepItem(target)
		return self.result
		
	def _parseBroadcast(self,j):
		for broadcast in j['http://zdf.de/rels/cmdm/broadcasts']:
			if 'http://zdf.de/rels/content/video-page-teaser' in broadcast:
				target = broadcast['http://zdf.de/rels/content/video-page-teaser']
				if broadcast['effectiveAirtimeBegin'] is not None:#TODO: find alternative for videos without this field
					self.template['metadata']['aired'] = {'ISO8601':broadcast['effectiveAirtimeBegin']}
					self._grepItem(target,'date')
		return self.result

	def _parseTV(self,j):
		for broadcast in j['tvService'][0]['http://zdf.de/rels/broadcasts-page']['http://zdf.de/rels/cmdm/broadcasts']:
			if 'http://zdf.de/rels/content/video-page-teaser' in broadcast:
				target = broadcast['http://zdf.de/rels/content/video-page-teaser']
				if broadcast['effectiveAirtimeBegin'] is not None:#TODO: find alternative for videos without this field
					self.template['metadata']['aired'] = {'ISO8601':broadcast['effectiveAirtimeBegin']}
					self._grepItem(target,'date')
		return self.result

	def _grepItem(self,target,forcedType=False):
		if target['profile'] in ['http://zdf.de/rels/not-found','http://zdf.de/rels/gone']:
			return False
		else:
			self._grepItemDefault(target,forcedType)

	def _grepItemDefault(self,target,forcedType=False):

		self.d = copy.deepcopy(self.template)
		self.d['metadata']['name'] = target['teaserHeadline']
		self.d['metadata']['plot'] = target['teasertext']
		self._grepArt(target)
		self._grepActors(target)

		if target['contentType'] == 'topic':
			if target.get('hasVideo') == False: return False

			self.d['params']['url'] = self.baseApi + target['self']+'&limit=100'
			self.d['params']['mode'] = 'libZdfListPage'
			self.d['type'] = 'dir'

		elif target['contentType'] in ['brand','category','topic']:
			if target.get('hasVideo') == False: return False

			self.d['params']['url'] = self.baseApi + target['http://zdf.de/rels/search/page-video-counter-with-video']['self'].replace('&limit=0','&limit=100')
			self.d['params']['mode'] = 'libZdfListPage'
			self.d['type'] = 'dir'

		elif target['contentType'] == 'clip':
			try:
				self.d['params']['url'] = self.baseApi + target['mainVideoContent']['http://zdf.de/rels/target']['http://zdf.de/rels/streams/ptmd-template'].replace('{playerId}',self.playerId)
				if 'duration' in target['mainVideoContent']['http://zdf.de/rels/target']:
					self.d['metadata']['duration'] = target['mainVideoContent']['http://zdf.de/rels/target']['duration']
				self.d['params']['mode'] = 'libZdfPlay'
				#d['type'] = 'clip'
				self.d['type'] = 'video'
			except: self.d = False

		elif target['contentType'] == 'episode':# or target['contentType'] == 'clip':
			if not target.get('hasVideo'):
				pass
				#return False
			#if target['mainVideoContent']['http://zdf.de/rels/target']['showCaption']:
			#	d['suburl'] = self.baseApi + target['mainVideoContent']['http://zdf.de/rels/target']['captionUrl']
			if 'mainVideoContent' in target:
				content = target['mainVideoContent']['http://zdf.de/rels/target']
			elif 'mainContent' in target:
				content = target['mainContent'][0]['videoContent'][0]['http://zdf.de/rels/target']
			else: return False
				
			if 'duration' in content:
				self.d['metadata']['duration'] = content['duration']
			#if 'programmeItem' in target and len(target['programmeItem']) >= 1 and 'http://zdf.de/rels/target' in target['programmeItem'][0] and target['programmeItem'][0]['http://zdf.de/rels/target']['http://zdf.de/rels/cmdm/season'] is not None:
			#	self.d['metadata']['season'] = target['programmeItem'][0]['http://zdf.de/rels/target']['http://zdf.de/rels/cmdm/season']['seasonNumber']
			#	self.d['metadata']['episode'] = target['programmeItem'][0]['http://zdf.de/rels/target']['episodeNumber']
			#self.d['metadata']['tvshowtitle'] = 'ListItem.TVShowTitle'#target['teaserHeadline']

			if not 'http://zdf.de/rels/streams/ptmd-template' in content: return False
			self.d['params']['url'] = self.baseApi + content['http://zdf.de/rels/streams/ptmd-template'].replace('{playerId}',self.playerId)
			self.d['params']['mode'] = 'libZdfPlay'

			self.d['type'] = 'episode'

		else:
			log('Unknown target type: ' + target['contentType'])
			self.d = False


		if self.d:
			if forcedType:
				self.d['type'] = forcedType
			self.result['items'].append(self.d)
			return True
		else:
			return False
		
	def _grepArt(self,target,isVideo=False):
		art = {}
		if not isVideo:
			if 'layouts' in target['teaserImageRef']:
				if '384xauto' in target['teaserImageRef']['layouts']:
					self.d['metadata']['art']['thumb'] = target['teaserImageRef']['layouts']['384xauto']
				elif '1920x1080' in target['teaserImageRef']['layouts']:
					self.d['metadata']['art']['thumb'] = target['teaserImageRef']['layouts']['1920x1080']

	def _grepActors(self,target):
		if 'programmeItem' in target:
			for item in target['programmeItem']:
				if item['profile'] == 'http://zdf.de/rels/content-reference/programme-item':
					if 'actorDetails' in item['http://zdf.de/rels/target'] and item['http://zdf.de/rels/target']['actorDetails']:
						actors = []
						for actor in item['http://zdf.de/rels/target']['actorDetails']['actorDetail']:
							if actor['name'] != '':
								actors.append(actor)
						self.d['metadata']['actors'] = actors
					if 'crewDetails' in item['http://zdf.de/rels/target'] and item['http://zdf.de/rels/target']['crewDetails'] != None:
						writers = []
						directors = []
						artists = []
						for crew in item['http://zdf.de/rels/target']['crewDetails']['crewDetail']:
							if crew['function'] == 'autor':
								writers.append(crew['name'])
							elif crew['function'] == 'kamera':
								directors.append(crew['name'])
							elif crew['function'] == 'musik':
								artists.append(crew['name'])
							elif crew['function'] == 'regie':
								directors.append(crew['name'])
						if len(writers) != 0: self.d['metadata']['writers'] = writers
						if len(directors) != 0: self.d['metadata']['directors'] = directors
						if len(artists) != 0: self.d['metadata']['artists'] = artists
					if 'text' in item['http://zdf.de/rels/target'] and item['http://zdf.de/rels/target']['text'] != None:
						self.d['metadata']['plot'] = item['http://zdf.de/rels/target']['text'].replace('<br/>','\n').replace('<b>','[B]').replace('</b>','[/B]')
					if 'originalTitle' in item['http://zdf.de/rels/target'] and item['http://zdf.de/rels/target']['originalTitle'] != None:
						self.d['metadata']['tvshowtitle'] = item['http://zdf.de/rels/target']['originalTitle']

	def getVideoUrlById(self, id):
	    url = (
	        'https://zdf-prod-futura.zdf.de'
	        '/mediathekV2/document/'
	        + id
	    )

	    response = self._getU(
	        url,
	        True
	    )

	    data = json.loads(response)

	    document = data.get(
	        'document',
	        data
	    )

	    streamUrl = (
	        document.get(
	            'streamApiUrlIOS'
	        )
	        or document.get(
	            'streamApiUrlAndroid'
	        )
	        or document.get(
	            'streamApiUrlVoice'
	        )
	    )

	    if not streamUrl:
	        for stream in document.get(
	            'streams',
	            []
	        ):
	            streamUrl = (
	                stream.get(
	                    'streamApiUrlIOS'
	                )
	                or stream.get(
	                    'streamApiUrlAndroid'
	                )
	            )

	            if streamUrl:
	                break

	    if not streamUrl:
	        raise RuntimeError(
	            'ZDF Futura: '
	            'keine Stream-API-URL gefunden '
	            'für ' + id
	        )

	    d = self.getVideoUrl(
	        streamUrl
	    )

	    d['metadata'] = {}

	    d['metadata']['name'] = (
	        document.get('titel')
	        or document.get('title')
	        or id
	    )

	    d['metadata']['plot'] = (
	        document.get('beschreibung')
	        or ''
	    )

	    if document.get('length'):
	        d['metadata']['duration'] = (
	            document.get('length')
	        )

	    image = self._firstImageUrl(
	        document.get(
	            'teaserBild',
	            document.get('image')
	        )
	    )

	    if image:
	        d['metadata']['art'] = {
	            'thumb': image
	        }

	    return d

	def getVideoUrl(self, url):
	    d = {
	        'media': [],
	        'subtitle': []
	    }

	    response = self._getU(
	        url,
	        False
	    )

	    j = json.loads(response)

	    for caption in j.get(
	        'captions',
	        []
	    ):
	        if caption.get('format') == (
	            'ebu-tt-d-basic-de'
	        ):
	            uri = caption.get('uri')

	            if uri:
	                d['subtitle'].append({
	                    'url': uri,
	                    'type': 'ttml',
	                    'lang': 'de',
	                    'colour': True
	                })

	    seen = set()

	    def addMedia(uri):
	        if (
	            not uri
	            or uri in seen
	        ):
	            return

	        seen.add(uri)

	        d['media'].append({
	            'url': uri,
	            'type': 'video',
	            'stream': 'HLS'
	        })

	    for priority in j.get(
	        'priorityList',
	        []
	    ):
	        for formatItem in priority.get(
	            'formitaeten',
	            []
	        ):
	            for quality in formatItem.get(
	                'qualities',
	                []
	            ):
	                audio = quality.get(
	                    'audio',
	                    {}
	                )

	                for track in audio.get(
	                    'tracks',
	                    []
	                ):
	                    uri = track.get('uri')

	                    if (
	                        uri
	                        and '.m3u8' in uri
	                    ):
	                        addMedia(uri)

	    # Fallback für geänderte PTMD-Strukturen:
	    # jede HLS-URL im JSON suchen.
	    if not d['media']:
	        def walk(obj):
	            if isinstance(obj, dict):
	                for value in obj.values():
	                    walk(value)

	            elif isinstance(obj, list):
	                for value in obj:
	                    walk(value)

	            elif isinstance(obj, str):
	                if '.m3u8' in obj:
	                    addMedia(obj)

	        walk(j)

	    if not d['media']:
	        raise RuntimeError(
	            'ZDF PTMD: '
	            'keinen HLS-Stream gefunden'
	        )

	    return d

