logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

drtv.py (15887B)


  1. import json
  2. import uuid
  3. from .common import InfoExtractor
  4. from ..utils import (
  5. ExtractorError,
  6. int_or_none,
  7. mimetype2ext,
  8. parse_iso8601,
  9. try_call,
  10. update_url_query,
  11. url_or_none,
  12. )
  13. from ..utils.traversal import traverse_obj
  14. SERIES_API = 'https://production-cdn.dr-massive.com/api/page?device=web_browser&item_detail_expand=all&lang=da&max_list_prefetch=3&path=%s'
  15. class DRTVIE(InfoExtractor):
  16. _VALID_URL = r'''(?x)
  17. https?://
  18. (?:
  19. (?:www\.)?dr\.dk/tv/se(?:/ondemand)?/(?:[^/?#]+/)*|
  20. (?:www\.)?(?:dr\.dk|dr-massive\.com)/drtv/(?:se|episode|program)/
  21. )
  22. (?P<id>[\da-z_-]+)
  23. '''
  24. _GEO_BYPASS = False
  25. _GEO_COUNTRIES = ['DK']
  26. IE_NAME = 'drtv'
  27. _TESTS = [{
  28. 'url': 'https://www.dr.dk/tv/se/boern/ultra/klassen-ultra/klassen-darlig-taber-10',
  29. 'md5': '25e659cccc9a2ed956110a299fdf5983',
  30. 'info_dict': {
  31. 'id': 'klassen-darlig-taber-10',
  32. 'ext': 'mp4',
  33. 'title': 'Klassen - Dårlig taber (10)',
  34. 'description': 'md5:815fe1b7fa656ed80580f31e8b3c79aa',
  35. 'timestamp': 1539085800,
  36. 'upload_date': '20181009',
  37. 'duration': 606.84,
  38. 'series': 'Klassen',
  39. 'season': 'Klassen I',
  40. 'season_number': 1,
  41. 'season_id': 'urn:dr:mu:bundle:57d7e8216187a4031cfd6f6b',
  42. 'episode': 'Episode 10',
  43. 'episode_number': 10,
  44. 'release_year': 2016,
  45. },
  46. 'expected_warnings': ['Unable to download f4m manifest'],
  47. 'skip': 'this video has been removed',
  48. }, {
  49. # with SignLanguage formats
  50. 'url': 'https://www.dr.dk/tv/se/historien-om-danmark/-/historien-om-danmark-stenalder',
  51. 'info_dict': {
  52. 'id': '00831690010',
  53. 'ext': 'mp4',
  54. 'title': 'Historien om Danmark: Stenalder',
  55. 'description': 'md5:8c66dcbc1669bbc6f873879880f37f2a',
  56. 'timestamp': 1546628400,
  57. 'upload_date': '20190104',
  58. 'duration': 3504.619,
  59. 'formats': 'mincount:20',
  60. 'release_year': 2017,
  61. 'season_id': 'urn:dr:mu:bundle:5afc03ad6187a4065ca5fd35',
  62. 'season_number': 1,
  63. 'season': 'Historien om Danmark',
  64. 'series': 'Historien om Danmark',
  65. },
  66. 'skip': 'this video has been removed',
  67. }, {
  68. 'url': 'https://www.dr.dk/drtv/se/frank-and-kastaniegaarden_71769',
  69. 'info_dict': {
  70. 'id': '00951930010',
  71. 'ext': 'mp4',
  72. 'title': 'Frank & Kastaniegaarden',
  73. 'description': 'md5:974e1780934cf3275ef10280204bccb0',
  74. 'release_timestamp': 1546545600,
  75. 'release_date': '20190103',
  76. 'duration': 2576,
  77. 'season': 'Frank & Kastaniegaarden',
  78. 'season_id': '67125',
  79. 'release_year': 2019,
  80. 'season_number': 2019,
  81. 'series': 'Frank & Kastaniegaarden',
  82. 'episode_number': 1,
  83. 'episode': 'Frank & Kastaniegaarden',
  84. 'thumbnail': r're:https?://.+',
  85. },
  86. 'params': {
  87. 'skip_download': True,
  88. },
  89. }, {
  90. # Foreign and Regular subtitle track
  91. 'url': 'https://www.dr.dk/drtv/se/spise-med-price_-pasta-selv_397445',
  92. 'info_dict': {
  93. 'id': '00212301010',
  94. 'ext': 'mp4',
  95. 'episode_number': 1,
  96. 'title': 'Spise med Price: Pasta Selv',
  97. 'alt_title': '1. Pasta Selv',
  98. 'release_date': '20230807',
  99. 'description': 'md5:2da9060524fed707810d71080b3d0cd8',
  100. 'duration': 1750,
  101. 'season': 'Spise med Price',
  102. 'release_timestamp': 1691438400,
  103. 'season_id': '397440',
  104. 'episode': 'Spise med Price: Pasta Selv',
  105. 'thumbnail': r're:https?://.+',
  106. 'season_number': 15,
  107. 'series': 'Spise med Price',
  108. 'release_year': 2022,
  109. 'subtitles': 'mincount:2',
  110. },
  111. 'params': {
  112. 'skip_download': 'm3u8',
  113. },
  114. }, {
  115. 'url': 'https://www.dr.dk/drtv/episode/bonderoeven_71769',
  116. 'only_matching': True,
  117. }, {
  118. 'url': 'https://dr-massive.com/drtv/se/bonderoeven_71769',
  119. 'only_matching': True,
  120. }, {
  121. 'url': 'https://www.dr.dk/drtv/program/jagten_220924',
  122. 'only_matching': True,
  123. }]
  124. SUBTITLE_LANGS = {
  125. 'DanishLanguageSubtitles': 'da',
  126. 'ForeignLanguageSubtitles': 'da_foreign',
  127. 'CombinedLanguageSubtitles': 'da_combined',
  128. }
  129. _TOKEN = None
  130. def _real_initialize(self):
  131. if self._TOKEN:
  132. return
  133. token_response = self._download_json(
  134. 'https://isl.dr-massive.com/api/authorization/anonymous-sso', None,
  135. note='Downloading anonymous token', headers={
  136. 'content-type': 'application/json',
  137. }, query={
  138. 'device': 'phone_android',
  139. 'lang': 'da',
  140. 'supportFallbackToken': 'true',
  141. }, data=json.dumps({
  142. 'deviceId': str(uuid.uuid4()),
  143. 'scopes': ['Catalog'],
  144. 'optout': True,
  145. }).encode())
  146. self._TOKEN = traverse_obj(
  147. token_response, (lambda _, x: x['type'] == 'UserAccount', 'value', {str}), get_all=False)
  148. if not self._TOKEN:
  149. raise ExtractorError('Unable to get anonymous token')
  150. def _real_extract(self, url):
  151. url_slug = self._match_id(url)
  152. webpage = self._download_webpage(url, url_slug)
  153. json_data = self._search_json(
  154. r'window\.__data\s*=', webpage, 'data', url_slug, fatal=False) or {}
  155. item = traverse_obj(
  156. json_data, ('cache', 'page', ..., (None, ('entries', 0)), 'item', {dict}), get_all=False)
  157. if item:
  158. item_id = item.get('id')
  159. else:
  160. item_id = url_slug.rsplit('_', 1)[-1]
  161. item = self._download_json(
  162. f'https://production-cdn.dr-massive.com/api/items/{item_id}', item_id,
  163. note='Attempting to download backup item data', query={
  164. 'device': 'web_browser',
  165. 'expand': 'all',
  166. 'ff': 'idp,ldp,rpt',
  167. 'geoLocation': 'dk',
  168. 'isDeviceAbroad': 'false',
  169. 'lang': 'da',
  170. 'segments': 'drtv,optedout',
  171. 'sub': 'Anonymous',
  172. })
  173. video_id = try_call(lambda: item['customId'].rsplit(':', 1)[-1]) or item_id
  174. stream_data = self._download_json(
  175. f'https://production.dr-massive.com/api/account/items/{item_id}/videos', video_id,
  176. note='Downloading stream data', query={
  177. 'delivery': 'stream',
  178. 'device': 'web_browser',
  179. 'ff': 'idp,ldp,rpt',
  180. 'lang': 'da',
  181. 'resolution': 'HD-1080',
  182. 'sub': 'Anonymous',
  183. }, headers={'authorization': f'Bearer {self._TOKEN}'})
  184. formats = []
  185. subtitles = {}
  186. for stream in traverse_obj(stream_data, (lambda _, x: x['url'])):
  187. format_id = stream.get('format', 'na')
  188. access_service = stream.get('accessService')
  189. preference = None
  190. subtitle_suffix = ''
  191. if access_service in ('SpokenSubtitles', 'SignLanguage', 'VisuallyInterpreted'):
  192. preference = -1
  193. format_id += f'-{access_service}'
  194. subtitle_suffix = f'-{access_service}'
  195. elif access_service == 'StandardVideo':
  196. preference = 1
  197. fmts, subs = self._extract_m3u8_formats_and_subtitles(
  198. stream.get('url'), video_id, ext='mp4', preference=preference, m3u8_id=format_id, fatal=False)
  199. formats.extend(fmts)
  200. api_subtitles = traverse_obj(stream, ('subtitles', lambda _, v: url_or_none(v['link']), {dict}))
  201. if not api_subtitles:
  202. self._merge_subtitles(subs, target=subtitles)
  203. for sub_track in api_subtitles:
  204. lang = sub_track.get('language') or 'da'
  205. subtitles.setdefault(self.SUBTITLE_LANGS.get(lang, lang) + subtitle_suffix, []).append({
  206. 'url': sub_track['link'],
  207. 'ext': mimetype2ext(sub_track.get('format')) or 'vtt',
  208. })
  209. if not formats and traverse_obj(item, ('season', 'customFields', 'IsGeoRestricted')):
  210. self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
  211. return {
  212. 'id': video_id,
  213. 'formats': formats,
  214. 'subtitles': subtitles,
  215. **traverse_obj(item, {
  216. 'title': 'title',
  217. 'alt_title': 'contextualTitle',
  218. 'description': 'description',
  219. 'thumbnail': ('images', 'wallpaper'),
  220. 'release_timestamp': ('customFields', 'BroadcastTimeDK', {parse_iso8601}),
  221. 'duration': ('duration', {int_or_none}),
  222. 'series': ('season', 'show', 'title'),
  223. 'season': ('season', 'title'),
  224. 'season_number': ('season', 'seasonNumber', {int_or_none}),
  225. 'season_id': 'seasonId',
  226. 'episode': 'episodeName',
  227. 'episode_number': ('episodeNumber', {int_or_none}),
  228. 'release_year': ('releaseYear', {int_or_none}),
  229. }),
  230. }
  231. class DRTVLiveIE(InfoExtractor):
  232. IE_NAME = 'drtv:live'
  233. _VALID_URL = r'https?://(?:www\.)?dr\.dk/(?:tv|TV)/live/(?P<id>[\da-z-]+)'
  234. _GEO_COUNTRIES = ['DK']
  235. _TEST = {
  236. 'url': 'https://www.dr.dk/tv/live/dr1',
  237. 'info_dict': {
  238. 'id': 'dr1',
  239. 'ext': 'mp4',
  240. 'title': 're:^DR1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
  241. },
  242. 'params': {
  243. # m3u8 download
  244. 'skip_download': True,
  245. },
  246. }
  247. def _real_extract(self, url):
  248. channel_id = self._match_id(url)
  249. channel_data = self._download_json(
  250. 'https://www.dr.dk/mu-online/api/1.0/channel/' + channel_id,
  251. channel_id)
  252. title = channel_data['Title']
  253. formats = []
  254. for streaming_server in channel_data.get('StreamingServers', []):
  255. server = streaming_server.get('Server')
  256. if not server:
  257. continue
  258. link_type = streaming_server.get('LinkType')
  259. for quality in streaming_server.get('Qualities', []):
  260. for stream in quality.get('Streams', []):
  261. stream_path = stream.get('Stream')
  262. if not stream_path:
  263. continue
  264. stream_url = update_url_query(
  265. f'{server}/{stream_path}', {'b': ''})
  266. if link_type == 'HLS':
  267. formats.extend(self._extract_m3u8_formats(
  268. stream_url, channel_id, 'mp4',
  269. m3u8_id=link_type, fatal=False, live=True))
  270. elif link_type == 'HDS':
  271. formats.extend(self._extract_f4m_formats(update_url_query(
  272. f'{server}/{stream_path}', {'hdcore': '3.7.0'}),
  273. channel_id, f4m_id=link_type, fatal=False))
  274. return {
  275. 'id': channel_id,
  276. 'title': title,
  277. 'thumbnail': channel_data.get('PrimaryImageUri'),
  278. 'formats': formats,
  279. 'is_live': True,
  280. }
  281. class DRTVSeasonIE(InfoExtractor):
  282. IE_NAME = 'drtv:season'
  283. _VALID_URL = r'https?://(?:www\.)?(?:dr\.dk|dr-massive\.com)/drtv/saeson/(?P<display_id>[\w-]+)_(?P<id>\d+)'
  284. _GEO_COUNTRIES = ['DK']
  285. _TESTS = [{
  286. 'url': 'https://www.dr.dk/drtv/saeson/frank-and-kastaniegaarden_9008',
  287. 'info_dict': {
  288. 'id': '9008',
  289. 'display_id': 'frank-and-kastaniegaarden',
  290. 'title': 'Frank & Kastaniegaarden',
  291. 'series': 'Frank & Kastaniegaarden',
  292. 'season_number': 2008,
  293. 'alt_title': 'Season 2008',
  294. },
  295. 'playlist_mincount': 8,
  296. }, {
  297. 'url': 'https://www.dr.dk/drtv/saeson/frank-and-kastaniegaarden_8761',
  298. 'info_dict': {
  299. 'id': '8761',
  300. 'display_id': 'frank-and-kastaniegaarden',
  301. 'title': 'Frank & Kastaniegaarden',
  302. 'series': 'Frank & Kastaniegaarden',
  303. 'season_number': 2009,
  304. 'alt_title': 'Season 2009',
  305. },
  306. 'playlist_mincount': 19,
  307. }]
  308. def _real_extract(self, url):
  309. display_id, season_id = self._match_valid_url(url).group('display_id', 'id')
  310. data = self._download_json(SERIES_API % f'/saeson/{display_id}_{season_id}', display_id)
  311. entries = [{
  312. '_type': 'url',
  313. 'url': f'https://www.dr.dk/drtv{episode["path"]}',
  314. 'ie_key': DRTVIE.ie_key(),
  315. 'title': episode.get('title'),
  316. 'alt_title': episode.get('contextualTitle'),
  317. 'episode': episode.get('episodeName'),
  318. 'description': episode.get('shortDescription'),
  319. 'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
  320. 'season_number': traverse_obj(data, ('entries', 0, 'item', 'seasonNumber')),
  321. 'episode_number': episode.get('episodeNumber'),
  322. } for episode in traverse_obj(data, ('entries', 0, 'item', 'episodes', 'items'))]
  323. return {
  324. '_type': 'playlist',
  325. 'id': season_id,
  326. 'display_id': display_id,
  327. 'title': traverse_obj(data, ('entries', 0, 'item', 'title')),
  328. 'alt_title': traverse_obj(data, ('entries', 0, 'item', 'contextualTitle')),
  329. 'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
  330. 'entries': entries,
  331. 'season_number': traverse_obj(data, ('entries', 0, 'item', 'seasonNumber')),
  332. }
  333. class DRTVSeriesIE(InfoExtractor):
  334. IE_NAME = 'drtv:series'
  335. _VALID_URL = r'https?://(?:www\.)?(?:dr\.dk|dr-massive\.com)/drtv/serie/(?P<display_id>[\w-]+)_(?P<id>\d+)'
  336. _GEO_COUNTRIES = ['DK']
  337. _TESTS = [{
  338. 'url': 'https://www.dr.dk/drtv/serie/frank-and-kastaniegaarden_6954',
  339. 'info_dict': {
  340. 'id': '6954',
  341. 'display_id': 'frank-and-kastaniegaarden',
  342. 'title': 'Frank & Kastaniegaarden',
  343. 'series': 'Frank & Kastaniegaarden',
  344. 'alt_title': '',
  345. },
  346. 'playlist_mincount': 15,
  347. }]
  348. def _real_extract(self, url):
  349. display_id, series_id = self._match_valid_url(url).group('display_id', 'id')
  350. data = self._download_json(SERIES_API % f'/serie/{display_id}_{series_id}', display_id)
  351. entries = [{
  352. '_type': 'url',
  353. 'url': f'https://www.dr.dk/drtv{season.get("path")}',
  354. 'ie_key': DRTVSeasonIE.ie_key(),
  355. 'title': season.get('title'),
  356. 'alt_title': season.get('contextualTitle'),
  357. 'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
  358. 'season_number': traverse_obj(data, ('entries', 0, 'item', 'seasonNumber')),
  359. } for season in traverse_obj(data, ('entries', 0, 'item', 'show', 'seasons', 'items'))]
  360. return {
  361. '_type': 'playlist',
  362. 'id': series_id,
  363. 'display_id': display_id,
  364. 'title': traverse_obj(data, ('entries', 0, 'item', 'title')),
  365. 'alt_title': traverse_obj(data, ('entries', 0, 'item', 'contextualTitle')),
  366. 'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
  367. 'entries': entries,
  368. }