]>
Commit | Line | Data |
---|---|---|
1 | from .common import InfoExtractor | |
2 | from ..utils import int_or_none, qualities, traverse_obj, url_or_none | |
3 | ||
4 | ||
5 | class NprIE(InfoExtractor): | |
6 | _VALID_URL = r'https?://(?:www\.)?npr\.org/(?:sections/[^/]+/)?\d{4}/\d{2}/\d{2}/(?P<id>\d+)' | |
7 | _TESTS = [{ | |
8 | 'url': 'https://www.npr.org/sections/allsongs/2015/10/21/449974205/new-music-from-beach-house-chairlift-cmj-discoveries-and-more', | |
9 | 'info_dict': { | |
10 | 'id': '449974205', | |
11 | 'title': 'New Music From Beach House, Chairlift, CMJ Discoveries And More', | |
12 | }, | |
13 | 'playlist_count': 7, | |
14 | }, { | |
15 | 'url': 'https://www.npr.org/sections/deceptivecadence/2015/10/09/446928052/music-from-the-shadows-ancient-armenian-hymns-and-piano-jazz', | |
16 | 'info_dict': { | |
17 | 'id': '446928052', | |
18 | 'title': "Songs We Love: Tigran Hamasyan, 'Your Mercy is Boundless'", | |
19 | }, | |
20 | 'playlist': [{ | |
21 | 'md5': '12fa60cb2d3ed932f53609d4aeceabf1', | |
22 | 'info_dict': { | |
23 | 'id': '446929930', | |
24 | 'ext': 'mp3', | |
25 | 'title': 'Your Mercy is Boundless (Bazum en Qo gtutyunqd)', | |
26 | 'duration': 402, | |
27 | }, | |
28 | }], | |
29 | }, { | |
30 | # multimedia, not media title | |
31 | 'url': 'https://www.npr.org/2017/06/19/533198237/tigers-jaw-tiny-desk-concert', | |
32 | 'info_dict': { | |
33 | 'id': '533198237', | |
34 | 'title': 'Tigers Jaw: Tiny Desk Concert', | |
35 | }, | |
36 | 'playlist': [{ | |
37 | 'md5': '12fa60cb2d3ed932f53609d4aeceabf1', | |
38 | 'info_dict': { | |
39 | 'id': '533201718', | |
40 | 'ext': 'mp4', | |
41 | 'title': 'Tigers Jaw: Tiny Desk Concert', | |
42 | 'duration': 402, | |
43 | }, | |
44 | }], | |
45 | 'expected_warnings': ['Failed to download m3u8 information'], | |
46 | }, { | |
47 | # multimedia, no formats, stream | |
48 | 'url': 'https://www.npr.org/2020/02/14/805476846/laura-stevenson-tiny-desk-concert', | |
49 | 'only_matching': True, | |
50 | }, { | |
51 | 'url': 'https://www.npr.org/2022/03/15/1084896560/bonobo-tiny-desk-home-concert', | |
52 | 'info_dict': { | |
53 | 'id': '1086468851', | |
54 | 'ext': 'mp4', | |
55 | 'title': 'Bonobo: Tiny Desk (Home) Concert', | |
56 | 'duration': 1061, | |
57 | 'thumbnail': r're:^https?://media.npr.org/assets/img/.*\.jpg$', | |
58 | }, | |
59 | }] | |
60 | ||
61 | def _real_extract(self, url): | |
62 | playlist_id = self._match_id(url) | |
63 | ||
64 | story = self._download_json( | |
65 | 'http://api.npr.org/query', playlist_id, query={ | |
66 | 'id': playlist_id, | |
67 | 'fields': 'audio,multimedia,title', | |
68 | 'format': 'json', | |
69 | 'apiKey': 'MDAzMzQ2MjAyMDEyMzk4MTU1MDg3ZmM3MQ010', | |
70 | })['list']['story'][0] | |
71 | playlist_title = story.get('title', {}).get('$text') | |
72 | ||
73 | KNOWN_FORMATS = ('threegp', 'm3u8', 'smil', 'mp4', 'mp3') | |
74 | quality = qualities(KNOWN_FORMATS) | |
75 | ||
76 | entries = [] | |
77 | for media in story.get('audio', []) + story.get('multimedia', []): | |
78 | media_id = media['id'] | |
79 | ||
80 | formats = [] | |
81 | for format_id, formats_entry in media.get('format', {}).items(): | |
82 | if not formats_entry: | |
83 | continue | |
84 | if isinstance(formats_entry, list): | |
85 | formats_entry = formats_entry[0] | |
86 | format_url = formats_entry.get('$text') | |
87 | if not format_url: | |
88 | continue | |
89 | if format_id in KNOWN_FORMATS: | |
90 | if format_id == 'm3u8': | |
91 | formats.extend(self._extract_m3u8_formats( | |
92 | format_url, media_id, 'mp4', 'm3u8_native', | |
93 | m3u8_id='hls', fatal=False)) | |
94 | elif format_id == 'smil': | |
95 | smil_formats = self._extract_smil_formats( | |
96 | format_url, media_id, transform_source=lambda s: s.replace( | |
97 | 'rtmp://flash.npr.org/ondemand/', 'https://ondemand.npr.org/'), | |
98 | fatal=False) | |
99 | self._check_formats(smil_formats, media_id) | |
100 | formats.extend(smil_formats) | |
101 | else: | |
102 | formats.append({ | |
103 | 'url': format_url, | |
104 | 'format_id': format_id, | |
105 | 'quality': quality(format_id), | |
106 | }) | |
107 | for stream_id, stream_entry in media.get('stream', {}).items(): | |
108 | if not isinstance(stream_entry, dict): | |
109 | continue | |
110 | if stream_id != 'hlsUrl': | |
111 | continue | |
112 | stream_url = url_or_none(stream_entry.get('$text')) | |
113 | if not stream_url: | |
114 | continue | |
115 | formats.extend(self._extract_m3u8_formats( | |
116 | stream_url, stream_id, 'mp4', 'm3u8_native', | |
117 | m3u8_id='hls', fatal=False)) | |
118 | ||
119 | if not formats: | |
120 | raw_json_ld = self._yield_json_ld(self._download_webpage(url, playlist_id), playlist_id, fatal=False) | |
121 | m3u8_url = traverse_obj(list(raw_json_ld), (..., 'subjectOf', ..., 'embedUrl'), get_all=False) | |
122 | formats = self._extract_m3u8_formats(m3u8_url, media_id, 'mp4', m3u8_id='hls', fatal=False) | |
123 | ||
124 | entries.append({ | |
125 | 'id': media_id, | |
126 | 'title': media.get('title', {}).get('$text') or playlist_title, | |
127 | 'thumbnail': media.get('altImageUrl', {}).get('$text'), | |
128 | 'duration': int_or_none(media.get('duration', {}).get('$text')), | |
129 | 'formats': formats, | |
130 | }) | |
131 | ||
132 | return self.playlist_result(entries, playlist_id, playlist_title) |