aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/yt_dlp_plugins/extractor/radiko_podcast.py
blob: 93e14085158813caafdfe39fa130a4a7ac603db8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from yt_dlp.extractor.common import InfoExtractor
from yt_dlp.utils import (
	clean_html,
	traverse_obj,
	url_or_none,
	str_or_none,
)

# nice simple one for a change...
# the app uses a similar system to regular programmes, thankfully the site doesn't
# but it does need protobufs to get more than 20 items...

class _RadikoPodcastBaseIE(InfoExtractor):

	def _extract_episode(self, episode_info):
		return {
			**traverse_obj(episode_info, {
				"id": ("id", {str_or_none}),
				"url": ("audio", "url"),
				"duration": ("audio", "durationSec"),

				"title": "title",
				"description": ("description", {clean_html}),
				"timestamp": ("startAt", "seconds"),

				"series": "channelTitle",
				"series_id": "channelId",
				"channel": "stationName",
				"uploader": "stationName",
			}),
			"thumbnail": traverse_obj(episode_info, ("imageUrl", {url_or_none}))
				or traverse_obj(episode_info, ("channelImageUrl", {url_or_none})),

			# so that --download-archive still works if you download from the playlist page
			"webpage_url": "https://radiko.jp/podcast/episodes/{id}".format(id=episode_info.get("id")),
			'extractor_key': RadikoPodcastEpisodeIE.ie_key(),
			'extractor': 'RadikoPodcastEpisode',
		}


class RadikoPodcastEpisodeIE(_RadikoPodcastBaseIE):
	_VALID_URL = r"https?://radiko\.jp/podcast/episodes/(?P<id>[a-f0-9-]+)"

	_TESTS = [{
		"url": "https://radiko.jp/podcast/episodes/cc8cf709-a50b-4846-aa0e-91ab10cf8bff",
		"info_dict": {
			"id": "cc8cf709-a50b-4846-aa0e-91ab10cf8bff",
			"ext": "mp3",
			'title': '2025.6.26 おしゃべり技術くん',
			'description': 'md5:1c4048025f68d6da053dd879a5d62304',
			'duration': 717,
			'thumbnail': 'https://podcast-static.cf.radiko.jp/09f27a48-ae04-4ce7-a024-572460e46eb7-20240214160012.png',
			'series': 'おしゃべり技術くん',
			'series_id': '09f27a48-ae04-4ce7-a024-572460e46eb7',
			'timestamp': 1751554800,
			'upload_date': '20250703',
		},
	}]

	def _real_extract(self, url):
		video_id = self._match_id(url)
		webpage = self._download_webpage(url, video_id)
		next_data = self._search_nextjs_data(webpage, video_id)["props"]["pageProps"]

		episode_info = next_data["podcastEpisode"]

		return self._extract_episode(episode_info)


class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
	_VALID_URL = r"https?://radiko\.jp/podcast/channels/(?P<id>[a-f0-9-]+)"

	_TESTS = [{
		"url": "https://radiko.jp/podcast/channels/09f27a48-ae04-4ce7-a024-572460e46eb7",
		"info_dict": {
			"id": "09f27a48-ae04-4ce7-a024-572460e46eb7"
		},
		'playlist_mincount': 20,
		'expected_warnings': ['Currently this extractor can only extract the latest 20 episodes'],
	}]

	def _real_extract(self, url):
		video_id = self._match_id(url)
		webpage = self._download_webpage(url, video_id)
		next_data = self._search_nextjs_data(webpage, video_id)["props"]["pageProps"]

		channel_info = next_data["podcastChannel"]
		episode_list_response = next_data["listPodcastEpisodesResponse"]


		def entries():
			for episode in episode_list_response["episodesList"]:
				yield self._extract_episode(episode)

		if traverse_obj(episode_list_response, "hasNextPage"):
			self.report_warning(f'Currently this extractor can only extract the latest {len(episode_list_response["episodesList"])} episodes')

		# TODO: GRPC/protobuf stuff to get the next page
		# https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes
		# see さらに表示 button on site


		return {
			"_type": "playlist",
			"id": video_id,
			**traverse_obj(channel_info, {
				"playlist_title": "title",
				"playlist_id": "id",
				"playlist_description": ("description", {clean_html}),
				"playlist_thumbnail": ("imageUrl", {url_or_none}),

			}),
			"entries": entries(),
		}