diff options
-rwxr-xr-x | yt_dlp_plugins/extractor/radiko.py | 494 |
1 files changed, 248 insertions, 246 deletions
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py index 9373867..5f48c2b 100755 --- a/yt_dlp_plugins/extractor/radiko.py +++ b/yt_dlp_plugins/extractor/radiko.py @@ -17,7 +17,7 @@ from yt_dlp.utils import ( class _RadikoBaseIE(InfoExtractor): - _FULL_KEY = base64.b64decode(''' + _FULL_KEY = base64.b64decode(""" fAu/s1ySbQBAyfugPCOniGTrMcOu5XqKcup3tmrZUAvx3MGtIIZl7wHokm07yxzL/oR9jdgWhi+e WYVoBIiAG4hDOP5H0Og3Qtd9KFnW8s0N4vNN2DzQ1Y4PqDq3HsQszf4ZaDTkyt4FFW9fPqKUtnVR LfXd/TGk0XeAvuKtj/qFcvzZQWcr+WrFGndFQK1TIT7/i8l2lw+OKIY9Bp42yw3eJj2+dqOkSQVm @@ -299,7 +299,7 @@ class _RadikoBaseIE(InfoExtractor): AIGJsNowPXOhASDGPsPJUzfmOao60x8+IaWolOjzRnrLKkrJJf5kJwMiULlK8JXGHYXyJUxz3RYP SqsZiZLVEQdbbUoeUeqTq/wp8W8sKg34mrq2nRWoqYrRgq/T8/kyVmbOoc8w58IDQw2mcHMBI/2e O7qEwbphJYjPyKR0D817R5ee7saTLNS7mOBCyoU0zU1bN7CbtrKmrg== - ''') + """) _COORDINATES = { # source: https://github.com/jackyzy823/rajiko/blob/master/background.js @@ -374,35 +374,35 @@ class _RadikoBaseIE(InfoExtractor): }, "10.0.0": { "sdk": "29", - "builds": ["5933585", "6969601", "7023426" , "7070703"] + "builds": ["5933585", "6969601", "7023426", "7070703"] }, "11.0.0": { - "sdk":"30" , + "sdk": "30", "builds": ["RP1A.201005.006", "RQ1A.201205.011", "RQ1A.210105.002"] }, "12.0.0": { - "sdk": "31" , + "sdk": "31", "builds": ["SD1A.210817.015.A4", "SD1A.210817.019.B1", "SD1A.210817.037", "SQ1D.220105.007"] }} - - _APP_VERSIONS = ["7.5.0", "7.4.17", "7.4.16", "7.4.15", "7.4.14", "7.4.13", "7.4.12", "7.4.11", "7.4.10", "7.4.9", "7.4.8", "7.4.7", "7.4.6","7.4.5","7.4.4","7.4.3","7.4.2","7.4.1","7.4.0","7.3.8","7.3.7","7.3.6","7.3.1","7.3.0","7.2.11","7.2.10"] - + + _APP_VERSIONS = ["7.5.0", "7.4.17", "7.4.16", "7.4.15", "7.4.14", "7.4.13", "7.4.12", "7.4.11", "7.4.10", "7.4.9", "7.4.8", "7.4.7", "7.4.6", "7.4.5", "7.4.4", "7.4.3", "7.4.2", "7.4.1", "7.4.0", "7.3.8", "7.3.7", "7.3.6", "7.3.1", "7.3.0", "7.2.11", "7.2.10"] + _region = None _user = None - + def _index_regions(self): region_data = {} - tree = self._download_xml('https://radiko.jp/v3/station/region/full.xml', None, note='Indexing regions') + tree = self._download_xml("https://radiko.jp/v3/station/region/full.xml", None, note="Indexing regions") for stations in tree: for station in stations: - area = station.find('area_id').text - station_id = station.find('id').text + area = station.find("area_id").text + station_id = station.find("id").text region_data[station_id] = area - self.cache.store('rajiko', 'region_index', region_data) + self.cache.store("rajiko", "region_index", region_data) return region_data - + def _get_coords(self, area_id): latlong = self._COORDINATES[area_id] lat = latlong[0] @@ -411,79 +411,80 @@ class _RadikoBaseIE(InfoExtractor): lat = lat + random.random() / 40.0 * (random.choice([1, -1])) long = long + random.random() / 40.0 * (random.choice([1, -1])) return f"{round(lat, 6)},{round(long, 6)},gps" - + def _generate_random_info(self): version_key = random.choice(list(self._ANDROID_VERSIONS.keys())) - version = self._ANDROID_VERSIONS[version_key] # hack because random.choice didnt work how i expected it to + version = self._ANDROID_VERSIONS[version_key] # hack because random.choice didnt work how i expected it to sdk = version["sdk"] build = random.choice(version["builds"]) model = random.choice(self._MODELS) - + info = { - 'X-Radiko-App': 'aSmartPhone7a', + "X-Radiko-App": "aSmartPhone7a", "X-Radiko-App-Version": random.choice(self._APP_VERSIONS), "X-Radiko-Device": f"{sdk}.{model}", "X-Radiko-User": secrets.token_hex(16), "User-Agent": f"Dalvik/2.1.0 (Linux; U; Android {version};{model}/{build})", } return info - + def _get_station_region(self, station): - regions = self.cache.load('rajiko', 'region_index') + regions = self.cache.load("rajiko", "region_index") if regions is None or station not in regions: regions = self._index_regions() return regions[station] - + def _negotiate_token(self, station_region): info = self._generate_random_info() - response, auth1_handle = self._download_webpage_handle('https://radiko.jp/v2/api/auth1', None, - 'Authenticating: step 1', headers = self._generate_random_info()) - + response, auth1_handle = self._download_webpage_handle("https://radiko.jp/v2/api/auth1", None, + "Authenticating: step 1", headers=self._generate_random_info()) + self.write_debug(response) - + auth1_header = auth1_handle.info() - auth_token = auth1_header['X-Radiko-AuthToken'] - key_length = int(auth1_header['X-Radiko-KeyLength']) - key_offset = int(auth1_header['X-Radiko-KeyOffset']) - + auth_token = auth1_header["X-Radiko-AuthToken"] + key_length = int(auth1_header["X-Radiko-KeyLength"]) + key_offset = int(auth1_header["X-Radiko-KeyOffset"]) + raw_partial_key = self._FULL_KEY[key_offset:key_offset + key_length] partial_key = base64.b64encode(raw_partial_key) - + headers = { **info, - 'X-Radiko-AuthToken': auth_token, - 'X-Radiko-Location': self._get_coords(station_region), - 'X-Radiko-Connection': "wifi", - 'X-Radiko-Partialkey': partial_key, + "X-Radiko-AuthToken": auth_token, + "X-Radiko-Location": self._get_coords(station_region), + "X-Radiko-Connection": "wifi", + "X-Radiko-Partialkey": partial_key, } - - auth2 = self._download_webpage('https://radiko.jp/v2/api/auth2', station_region, - "Authenticating: step 2", headers = headers) - + + auth2 = self._download_webpage("https://radiko.jp/v2/api/auth2", station_region, + "Authenticating: step 2", headers=headers) + self.write_debug(auth2.strip()) - actual_region, region_kanji, region_english = auth2.split(',') - + actual_region, region_kanji, region_english = auth2.split(",") + if actual_region != station_region: self.report_warning(f"Didn't get the right region: expected {station_region}, got {actual_region}") - + # this should never happen + token = { - 'X-Radiko-AreaId': actual_region, - 'X-Radiko-AuthToken': auth_token, + "X-Radiko-AreaId": actual_region, + "X-Radiko-AuthToken": auth_token, } - + self._user = headers["X-Radiko-User"] - self.cache.store('rajiko-tokens', station_region, {"token": token, "user": self._user}) + self.cache.store("rajiko-tokens", station_region, {"token": token, "user": self._user}) return token def _auth(self, station_region): - cachedata = self.cache.load('rajiko-tokens', station_region) + cachedata = self.cache.load("rajiko-tokens", station_region) self.write_debug(cachedata) if cachedata is not None: token = cachedata.get("token") self._user = cachedata.get("user") - response = self._download_webpage('https://radiko.jp/v2/api/auth_check', station_region, 'Checking cached token', - headers = token, expected_status = 401) + response = self._download_webpage("https://radiko.jp/v2/api/auth_check", station_region, "Checking cached token", + headers=token, expected_status=401) self.write_debug(response) if response != "OK": token = self._negotiate_token(station_region) @@ -492,47 +493,46 @@ class _RadikoBaseIE(InfoExtractor): return token def _get_station_meta(self, region, station_id): - region = self._download_xml(f'https://radiko.jp/v3/station/list/{region}.xml', region, note="Downloading station listings") - for station in region.findall('station'): + region = self._download_xml(f"https://radiko.jp/v3/station/list/{region}.xml", region, note="Downloading station listings") + for station in region.findall("station"): if station.find("id").text == station_id: - station_name = station.find('name').text - station_url = url_or_none(station.find('href').text) + station_name = station.find("name").text + station_url = url_or_none(station.find("href").text) return { - 'title': station_name, - 'channel': station_name, - 'channel_id': station_id, - 'channel_url': station_url, - 'thumbnail': url_or_none(station.find('banner').text), - 'alt_title': station.find('ascii_name').text, - 'uploader_url': station_url, - 'id': station_id, + "title": station_name, + "channel": station_name, + "channel_id": station_id, + "channel_url": station_url, + "thumbnail": url_or_none(station.find("banner").text), + "alt_title": station.find("ascii_name").text, + "uploader_url": station_url, + "id": station_id, } - def _int2bool(self,i): + def _int2bool(self, i): i = int(i) return True if i == 1 else False - + def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None): # smartphone formats api = always happy path - url_data = self._download_xml(f'https://radiko.jp/v3/station/stream/aSmartPhone7a/{station}.xml', - station, note='Downloading stream information') - + url_data = self._download_xml(f"https://radiko.jp/v3/station/stream/aSmartPhone7a/{station}.xml", + station, note="Downloading stream information") + urls = [] formats = [] - + for i in url_data: url = i.find("playlist_create_url").text if url in urls: continue - - if self._int2bool(i.get('timefree')) == timefree: -# if True: + + if self._int2bool(i.get("timefree")) == timefree: urls.append(url) playlist_url = update_url_query(url, { - 'station_id': station, - 'l': '15', - 'lsid': self._user, - 'type': 'b', + "station_id": station, + "l": "15", + "lsid": self._user, + "type": "b", }) if timefree: playlist_url = update_url_query(playlist_url, { @@ -544,58 +544,59 @@ class _RadikoBaseIE(InfoExtractor): domain = urllib.parse.urlparse(playlist_url).netloc formats += self._extract_m3u8_formats( playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data, - note=f'Downloading m3u8 information from {domain}', + note=f"Downloading m3u8 information from {domain}", ) return formats + class RadikoLiveIE(_RadikoBaseIE): - _VALID_URL = r'https?://(?:www\.)?radiko\.jp/#!/live/(?P<id>[A-Z0-9-]+)' + _VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/live/(?P<id>[A-Z0-9-]+)" _TESTS = [{ # JP13 (Tokyo) - 'url': 'https://radiko.jp/#!/live/FMT', - 'info_dict': { - 'id': 'FMT', - 'ext': 'm4a', - 'live_status': 'is_live', - 'alt_title': 'TOKYO FM', - 'title': 're:^TOKYO FM.+$', - 'thumbnail': 'https://radiko.jp/res/banner/FMT/20220512162447.jpg', - 'uploader_url': 'https://www.tfm.co.jp/', - 'channel_url': 'https://www.tfm.co.jp/', - 'channel': 'TOKYO FM', - 'channel_id': 'FMT', + "url": "https://radiko.jp/#!/live/FMT", + "info_dict": { + "id": "FMT", + "ext": "m4a", + "live_status": "is_live", + "alt_title": "TOKYO FM", + "title": "re:^TOKYO FM.+$", + "thumbnail": "https://radiko.jp/res/banner/FMT/20220512162447.jpg", + "uploader_url": "https://www.tfm.co.jp/", + "channel_url": "https://www.tfm.co.jp/", + "channel": "TOKYO FM", + "channel_id": "FMT", }, }, { # JP1 (Hokkaido) - 'url': 'https://radiko.jp/#!/live/NORTHWAVE', - 'info_dict': { - 'id': 'NORTHWAVE', - 'ext': 'm4a', - 'uploader_url': 'https://www.fmnorth.co.jp/', - 'alt_title': 'FM NORTH WAVE', - 'title': 're:^FM NORTH WAVE.+$', - 'live_status': 'is_live', - 'thumbnail': 'https://radiko.jp/res/banner/NORTHWAVE/20150731161543.png', - 'channel': 'FM NORTH WAVE', - 'channel_url': 'https://www.fmnorth.co.jp/', - 'channel_id': 'NORTHWAVE', + "url": "https://radiko.jp/#!/live/NORTHWAVE", + "info_dict": { + "id": "NORTHWAVE", + "ext": "m4a", + "uploader_url": "https://www.fmnorth.co.jp/", + "alt_title": "FM NORTH WAVE", + "title": "re:^FM NORTH WAVE.+$", + "live_status": "is_live", + "thumbnail": "https://radiko.jp/res/banner/NORTHWAVE/20150731161543.png", + "channel": "FM NORTH WAVE", + "channel_url": "https://www.fmnorth.co.jp/", + "channel_id": "NORTHWAVE", }, }, { # ALL (all prefectures) - # api still specifies a prefecture though, in this case JP12 (Chiba), so that's what it auths as - 'url': 'https://radiko.jp/#!/live/HOUSOU-DAIGAKU', - 'info_dict': { - 'id': 'HOUSOU-DAIGAKU', - 'ext': 'm4a', - 'title': 're:^放送大学.+$', - 'live_status': 'is_live', - 'uploader_url': 'https://www.ouj.ac.jp/', - 'alt_title': 'HOUSOU-DAIGAKU', - 'thumbnail': 'https://radiko.jp/res/banner/HOUSOU-DAIGAKU/20150805145127.png', - 'channel': '放送大学', - 'channel_url': 'https://www.ouj.ac.jp/', - 'channel_id': 'HOUSOU-DAIGAKU', + # api still specifies a prefecture though, in this case JP12 (Chiba), so that"s what it auths as + "url": "https://radiko.jp/#!/live/HOUSOU-DAIGAKU", + "info_dict": { + "id": "HOUSOU-DAIGAKU", + "ext": "m4a", + "title": "re:^放送大学.+$", + "live_status": "is_live", + "uploader_url": "https://www.ouj.ac.jp/", + "alt_title": "HOUSOU-DAIGAKU", + "thumbnail": "https://radiko.jp/res/banner/HOUSOU-DAIGAKU/20150805145127.png", + "channel": "放送大学", + "channel_url": "https://www.ouj.ac.jp/", + "channel_id": "HOUSOU-DAIGAKU", }, }] @@ -607,115 +608,114 @@ class RadikoLiveIE(_RadikoBaseIE): formats = self._get_station_formats(station, False, auth_data) return { - 'is_live': True, - 'id': station, + "is_live": True, + "id": station, **station_meta, - 'formats': formats, + "formats": formats, } class RadikoTimeFreeIE(_RadikoBaseIE): - _VALID_URL = r'https?://(?:www\.)?radiko\.jp/#!/ts/(?P<station>[A-Z0-9-]+)/(?P<id>\d+)' + _VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/ts/(?P<station>[A-Z0-9-]+)/(?P<id>\d+)" _TESTS = [{ - 'url': 'https://radiko.jp/#!/ts/INT/20230505230000', - 'info_dict': { - 'title': 'TOKYO MOON', - 'ext': 'm4a', - 'id': 'INT-20230505230000', - 'live_status': 'was_live', - 'tags': ['松浦俊夫'], - 'description': 'md5:804d83142a1ef1dfde48c44fb531482a', - 'duration': 3600, - 'thumbnail': 'https://radiko.jp/res/program/DEFAULT_IMAGE/INT/72b3a65f-c3ee-4892-a327-adec52076d51.jpeg', - 'cast': ['松浦\u3000俊夫'], - 'series': 'Tokyo Moon', - 'channel_id': 'INT', - 'uploader_url': 'https://www.interfm.co.jp/', - 'channel': 'interfm', - 'channel_url': 'https://www.interfm.co.jp/', - 'timestamp': 1683295200, - 'upload_date': '20230505', - 'release_date': '20230505', - 'release_timestamp': 1683298800, + "url": "https://radiko.jp/#!/ts/INT/20230505230000", + "info_dict": { + "title": "TOKYO MOON", + "ext": "m4a", + "id": "INT-20230505230000", + "live_status": "was_live", + "tags": ["松浦俊夫"], + "description": "md5:804d83142a1ef1dfde48c44fb531482a", + "duration": 3600, + "thumbnail": "https://radiko.jp/res/program/DEFAULT_IMAGE/INT/72b3a65f-c3ee-4892-a327-adec52076d51.jpeg", + "cast": ["松浦\u3000俊夫"], + "series": "Tokyo Moon", + "channel_id": "INT", + "uploader_url": "https://www.interfm.co.jp/", + "channel": "interfm", + "channel_url": "https://www.interfm.co.jp/", + "timestamp": 1683295200, + "upload_date": "20230505", + "release_date": "20230505", + "release_timestamp": 1683298800, }, - },{ - 'url': 'https://radiko.jp/#!/ts/NORTHWAVE/20230507173000', - 'info_dict': { - 'title': '角松敏生 My BLUES LIFE', - 'id': 'NORTHWAVE-20230507173000', - 'ext': 'm4a', - 'channel_id': 'NORTHWAVE', - 'thumbnail': 'https://radiko.jp/res/program/DEFAULT_IMAGE/NORTHWAVE/cwqcdppldk.jpg', - 'uploader_url': 'https://www.fmnorth.co.jp/', - 'duration': 1800, - 'channel': 'FM NORTH WAVE', - 'channel_url': 'https://www.fmnorth.co.jp/', - 'live_status': 'was_live', - 'tags': ['ノースウェーブ', '角松敏生', '人気アーティストトーク'], - 'cast': ['角松\u3000敏生'], - 'series': '角松敏生 My BLUES LIFE', - 'description': 'md5:027860a5731c04779b6720047c7b8b59', - 'upload_date': '20230507', - 'release_timestamp': 1683450000, - 'timestamp': 1683448200, - 'release_date': '20230507', + }, { + "url": "https://radiko.jp/#!/ts/NORTHWAVE/20230507173000", + "info_dict": { + "title": "角松敏生 My BLUES LIFE", + "id": "NORTHWAVE-20230507173000", + "ext": "m4a", + "channel_id": "NORTHWAVE", + "thumbnail": "https://radiko.jp/res/program/DEFAULT_IMAGE/NORTHWAVE/cwqcdppldk.jpg", + "uploader_url": "https://www.fmnorth.co.jp/", + "duration": 1800, + "channel": "FM NORTH WAVE", + "channel_url": "https://www.fmnorth.co.jp/", + "live_status": "was_live", + "tags": ["ノースウェーブ", "角松敏生", "人気アーティストトーク"], + "cast": ["角松\u3000敏生"], + "series": "角松敏生 My BLUES LIFE", + "description": "md5:027860a5731c04779b6720047c7b8b59", + "upload_date": "20230507", + "release_timestamp": 1683450000, + "timestamp": 1683448200, + "release_date": "20230507", }, - },{ + }, { # late-night show, see comment in _unfuck_day - 'url': 'https://radiko.jp/#!/ts/TBS/20230506030000', - 'info_dict': { - 'id': 'TBS-20230506030000', - 'ext': 'm4a', - 'title': 'CITY CHILL CLUB', - 'cast': ['イハラカンタロウ'], - 'thumbnail': 'https://radiko.jp/res/program/DEFAULT_IMAGE/TBS/xxeimdxszs.jpg', - 'description': 'md5:f60b1012f0606b336660416598d82043', - 'tags': ['CCC905', '音楽との出会いが楽しめる', '人気アーティストトーク', '音楽プロデューサー出演', 'ドライブ中におすすめ', '寝る前におすすめ', '学生におすすめ'], - 'channel': 'TBSラジオ', - 'uploader_url': 'https://www.tbsradio.jp/', - 'channel_id': 'TBS', - 'channel_url': 'https://www.tbsradio.jp/', - 'duration': 7200, - 'series': 'CITY CHILL CLUB', - 'live_status': 'was_live', - 'timestamp': 1683309600, - 'upload_date': '20230505', - 'release_timestamp': 1683316800, - 'release_date': '20230505', + "url": "https://radiko.jp/#!/ts/TBS/20230506030000", + "info_dict": { + "id": "TBS-20230506030000", + "ext": "m4a", + "title": "CITY CHILL CLUB", + "cast": ["イハラカンタロウ"], + "thumbnail": "https://radiko.jp/res/program/DEFAULT_IMAGE/TBS/xxeimdxszs.jpg", + "description": "md5:f60b1012f0606b336660416598d82043", + "tags": ["CCC905", "音楽との出会いが楽しめる", "人気アーティストトーク", "音楽プロデューサー出演", "ドライブ中におすすめ", "寝る前におすすめ", "学生におすすめ"], + "channel": "TBSラジオ", + "uploader_url": "https://www.tbsradio.jp/", + "channel_id": "TBS", + "channel_url": "https://www.tbsradio.jp/", + "duration": 7200, + "series": "CITY CHILL CLUB", + "live_status": "was_live", + "timestamp": 1683309600, + "upload_date": "20230505", + "release_timestamp": 1683316800, + "release_date": "20230505", }, - },{ + }, { # early-morning show, same reason - 'url': 'https://radiko.jp/#!/ts/TBS/20230504050000', - 'info_dict': - { - 'title': '生島ヒロシのおはよう定食・一直線', - 'id': 'TBS-20230504050000', - 'ext': 'm4a', - 'cast': ['生島\u3000ヒロシ', '齋藤\u3000孝'], - 'channel': 'TBSラジオ', - 'thumbnail': 'https://radiko.jp/res/program/DEFAULT_IMAGE/TBS/ch3vcvtc5e.jpg', - 'description': 'md5:26dba9e22df6883c072067cdc5ac0511', - 'series': '生島ヒロシのおはよう定食・一直線', - 'tags': ['生島ヒロシ', '健康', '檀れい', '朝のニュースを効率良く'], - 'channel_url': 'https://www.tbsradio.jp/', - 'uploader_url': 'https://www.tbsradio.jp/', - 'channel_id': 'TBS', - 'duration': 5400, - 'live_status': 'was_live', - 'release_timestamp': 1683149400, - 'release_date': '20230503', - 'upload_date': '20230503', - 'timestamp': 1683144000, + "url": "https://radiko.jp/#!/ts/TBS/20230504050000", + "info_dict": { + "title": "生島ヒロシのおはよう定食・一直線", + "id": "TBS-20230504050000", + "ext": "m4a", + "cast": ["生島\u3000ヒロシ", "齋藤\u3000孝"], + "channel": "TBSラジオ", + "thumbnail": "https://radiko.jp/res/program/DEFAULT_IMAGE/TBS/ch3vcvtc5e.jpg", + "description": "md5:26dba9e22df6883c072067cdc5ac0511", + "series": "生島ヒロシのおはよう定食・一直線", + "tags": ["生島ヒロシ", "健康", "檀れい", "朝のニュースを効率良く"], + "channel_url": "https://www.tbsradio.jp/", + "uploader_url": "https://www.tbsradio.jp/", + "channel_id": "TBS", + "duration": 5400, + "live_status": "was_live", + "release_timestamp": 1683149400, + "release_date": "20230503", + "upload_date": "20230503", + "timestamp": 1683144000, }, }] - + _JST = datetime.timezone(datetime.timedelta(hours=9)) - + def _timestring_to_datetime(self, time): return datetime.datetime(int(time[:4]), int(time[4:6]), int(time[6:8]), hour=int(time[8:10]), minute=int(time[10:12]), second=int(time[12:14]), tzinfo=self._JST) - + def _unfuck_day(self, time): # api counts 05:00 -> 28:59 (04:59 next day) as all the same day # like the 30-hour day, 06:00 -> 29:59 (05:59) @@ -730,60 +730,60 @@ class RadikoTimeFreeIE(_RadikoBaseIE): return time return time[:8] - + def _get_programme_meta(self, station_id, start_time): day = self._unfuck_day(start_time) - meta = self._download_json(f'https://radiko.jp/v4/program/station/date/{day}/{station_id}.json', station_id, + meta = self._download_json(f"https://radiko.jp/v4/program/station/date/{day}/{station_id}.json", station_id, note="Downloading programme data") - programmes = traverse_obj(meta, ('stations', lambda _, v: v['station_id'] == station_id, - 'programs', 'program'), get_all=False) + programmes = traverse_obj(meta, ("stations", lambda _, v: v["station_id"] == station_id, + "programs", "program"), get_all=False) for prog in programmes: - if prog['ft'] <= start_time < prog['to']: - actual_start = prog['ft'] - if len(prog.get('person')) > 0: - cast = [person.get("name") for person in prog.get('person')] + if prog["ft"] <= start_time < prog["to"]: + actual_start = prog["ft"] + if len(prog.get("person")) > 0: + cast = [person.get("name") for person in prog.get("person")] else: - cast = [prog.get('performer')] + cast = [prog.get("performer")] return { - 'id': join_nonempty(station_id, actual_start), - 'timestamp': unified_timestamp(f'{actual_start}+0900'), # hack to account for timezone - 'release_timestamp': unified_timestamp(f'{prog["to"]}+0900'), - 'cast': cast, - 'description': clean_html(join_nonempty('summary', 'description', from_dict=prog, delim='\n')), + "id": join_nonempty(station_id, actual_start), + "timestamp": unified_timestamp(f"{actual_start}+0900"), # hack to account for timezone + "release_timestamp": unified_timestamp(f"{prog['to']}+0900"), + "cast": cast, + "description": clean_html(join_nonempty("summary", "description", from_dict=prog, delim="\n")), **traverse_obj(prog, { - 'title': 'title', - 'duration': 'dur', - 'thumbnail': 'img', - 'series': 'season_name', - 'tags': 'tag', - } - )}, [prog.get('ft'),prog.get("to")] - + "title": "title", + "duration": "dur", + "thumbnail": "img", + "series": "season_name", + "tags": "tag", + } + )}, (prog.get("ft"), prog.get("to")) + def _real_extract(self, url): - station, start_time = self._match_valid_url(url).group('station', 'id') + station, start_time = self._match_valid_url(url).group("station", "id") meta, times = self._get_programme_meta(station, start_time) - + noformats_expected = False noformats_msg = "No video formats found!" noformats_force = False live_status = "was_live" - + start_datetime = self._timestring_to_datetime(times[0]) end_datetime = self._timestring_to_datetime(times[1]) now = datetime.datetime.now(tz=self._JST) - + if end_datetime < now - datetime.timedelta(days=7): noformats_expected = True noformats_msg = "Programme is no longer available." elif start_datetime > now: noformats_expected = True noformats_msg = "Programme has not aired yet." - live_status = 'is_upcoming' + live_status = "is_upcoming" elif start_datetime <= now < end_datetime: - live_status = 'is_upcoming' + live_status = "is_upcoming" noformats_expected = True noformats_msg = "Programme has not finished airing yet." noformats_force = True @@ -794,36 +794,38 @@ class RadikoTimeFreeIE(_RadikoBaseIE): formats = self._get_station_formats(station, True, auth_data, start_at=times[0], end_at=times[1]) if len(formats) == 0 or noformats_force: - self.raise_no_formats(noformats_msg, video_id=meta['id'], expected=noformats_expected) + self.raise_no_formats(noformats_msg, video_id=meta["id"], expected=noformats_expected) formats = [] - return {**station_meta, - 'alt_title': None, + return { + **station_meta, + "alt_title": None, **meta, - 'formats': formats, - 'live_status': live_status, - 'container': 'm4a_dash', # force fixup, AAC-only HLS - } + "formats": formats, + "live_status": live_status, + "container": "m4a_dash", # force fixup, AAC-only HLS + } + class RadikoSearchIE(_RadikoBaseIE): - _VALID_URL = r'https?://(?:www\.)?radiko\.jp/#!/search/(?:timeshift|live)\?' - + _VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/search/(?:timeshift|live)\?" + def _strip_date(self, date): return date.replace(" ", "").replace("-", "").replace(":", "") - + def _real_extract(self, url): url = url.replace("/#!/", "/!/", 1) # urllib.parse interprets the path as just one giant fragment because of the #, so we hack it away queries = parse_qs(url) - + search_url = update_url_query("https://radiko.jp/v3/api/program/search", { **queries, - 'uid': secrets.token_hex(16), - 'app_id': 'pc', + "uid": secrets.token_hex(16), + "app_id": "pc", }) data = self._download_json(search_url, None) - - results = traverse_obj(data, ('data',..., { + + results = traverse_obj(data, ("data", ..., { "station": "station_id", "time": ("start_time", {self._strip_date}) })) |