From be540fe77d00f22cf6a4f0f6865a109cf8011bdc Mon Sep 17 00:00:00 2001
From: garret
Date: Sun, 11 Aug 2024 11:27:57 +0100
Subject: persons: only check the extractor arg once
---
yt_dlp_plugins/extractor/radiko.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 7d06d37..618e1cb 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -719,10 +719,10 @@ class RadikoPersonIE(InfoExtractor):
person_api = self._download_json(person_api_url, person_id)
def entries():
+ key_station_only = len(self._configuration_arg("key_station_only", ie_key="rajiko")) > 0
for episode in person_api.get("data"):
- if len(self._configuration_arg("key_station_only", ie_key="rajiko")) > 0:
- if episode.get("key_station_id") != episode.get("station_id"):
- continue
+ if key_station_only and episode.get("key_station_id") != episode.get("station_id"):
+ continue
share_url = traverse_obj(episode, ("radiko_url", ("pc", "sp", "android", "ios", "app"),
{url_or_none}), get_all=False)
--
cgit v1.2.3-70-g09d2
From 032c7951143e6452aeda34863bb38fed7ad26d3e Mon Sep 17 00:00:00 2001
From: garret
Date: Sun, 11 Aug 2024 11:29:02 +0100
Subject: fix RadikoSiteTime test (forgot)
changed func in eb94441420845970d48fe7b4e30cc5eed78ac95a , forgot to
change the test also
---
yt_dlp_plugins/extractor/radiko_time.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko_time.py b/yt_dlp_plugins/extractor/radiko_time.py
index 9e77008..4d51ce5 100755
--- a/yt_dlp_plugins/extractor/radiko_time.py
+++ b/yt_dlp_plugins/extractor/radiko_time.py
@@ -79,7 +79,7 @@ if __name__ == "__main__":
# cursed (no seconds) - seems to do -1s
assert RadikoSiteTime('202308240100').timestring() == "20230824005959"
# broadcast day starts at 05:00, ends at 04:59 (29:59)
- assert RadikoSiteTime('20230824030000').broadcast_day() == '20230823'
+ assert RadikoSiteTime('20230824030000').broadcast_day_string() == '20230823'
assert RadikoSiteTime('20230824130000').broadcast_day_end() == datetime.datetime(2023, 8, 25, 5, 0, 0, tzinfo=JST)
assert RadikoSiteTime('20230824030000').broadcast_day_end() == datetime.datetime(2023, 8, 24, 5, 0, 0, tzinfo=JST)
# checking timezone
--
cgit v1.2.3-70-g09d2
From b5298403470347bfba79130c49bb7dca7ee906d8 Mon Sep 17 00:00:00 2001
From: garret
Date: Sun, 11 Aug 2024 11:43:12 +0100
Subject: move misc stuff to "misc" folder
the tests arent tests really, probably shouldnt give that impression
+ maybe now they wont get automatically included in the build
---
how to do a release | 46 ----------------------------------------------
misc/how to do a release | 46 ++++++++++++++++++++++++++++++++++++++++++++++
misc/randominfo.py | 12 ++++++++++++
misc/test-tokens.py | 26 ++++++++++++++++++++++++++
tests/test_randominfo.py | 14 --------------
tests/test_tokens.py | 26 --------------------------
6 files changed, 84 insertions(+), 86 deletions(-)
delete mode 100644 how to do a release
create mode 100644 misc/how to do a release
create mode 100755 misc/randominfo.py
create mode 100755 misc/test-tokens.py
delete mode 100755 tests/test_randominfo.py
delete mode 100755 tests/test_tokens.py
diff --git a/how to do a release b/how to do a release
deleted file mode 100644
index ca99af6..0000000
--- a/how to do a release
+++ /dev/null
@@ -1,46 +0,0 @@
-putting this here because i'll forget how to do it otherwise
-
-update the pyproject.toml
-tag it in git, eg v1.0
-
-## build the builds
-python3 -m build
-
-and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
-because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
-update the pip index html
-
-## update the changelog file
-
-~/site2/yt-dlp-rajiko/CHANGELOG
-
-```
-version number
-date (git log v1.0 --pretty --date=rfc2822)
-url: whl download link
-sha256: sha256 of the whl
-brief summary of the release
-can span multiple lines
-
-bullet points of changes, 1 per line
-simple present tense, third person singular - continue "this release...", eg..
-fixes a bug where the computer would explode
-makes downloading 5000x faster
-```
-
-./generate_changelog.py to make the new rss feed
-
-## update the website
-
-move the previous release into the "Previous releases"
-update the sha256 (just sha256 command in the pip dir)
-update the whl link
-repeat for japanese version
-
-now push to the server
-
-## update github
-
-paste the changelog output into a github release, upload the new builds
-
-and thats probably all
diff --git a/misc/how to do a release b/misc/how to do a release
new file mode 100644
index 0000000..ca99af6
--- /dev/null
+++ b/misc/how to do a release
@@ -0,0 +1,46 @@
+putting this here because i'll forget how to do it otherwise
+
+update the pyproject.toml
+tag it in git, eg v1.0
+
+## build the builds
+python3 -m build
+
+and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
+because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
+update the pip index html
+
+## update the changelog file
+
+~/site2/yt-dlp-rajiko/CHANGELOG
+
+```
+version number
+date (git log v1.0 --pretty --date=rfc2822)
+url: whl download link
+sha256: sha256 of the whl
+brief summary of the release
+can span multiple lines
+
+bullet points of changes, 1 per line
+simple present tense, third person singular - continue "this release...", eg..
+fixes a bug where the computer would explode
+makes downloading 5000x faster
+```
+
+./generate_changelog.py to make the new rss feed
+
+## update the website
+
+move the previous release into the "Previous releases"
+update the sha256 (just sha256 command in the pip dir)
+update the whl link
+repeat for japanese version
+
+now push to the server
+
+## update github
+
+paste the changelog output into a github release, upload the new builds
+
+and thats probably all
diff --git a/misc/randominfo.py b/misc/randominfo.py
new file mode 100755
index 0000000..bdb7660
--- /dev/null
+++ b/misc/randominfo.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+from yt_dlp_plugins.extractor import radiko
+from yt_dlp import YoutubeDL
+
+
+ie = radiko._RadikoBaseIE()
+ydl = YoutubeDL(auto_init=False)
+ie.set_downloader(ydl)
+
+info = ie._generate_random_info()
+print("random device info")
+print(info)
diff --git a/misc/test-tokens.py b/misc/test-tokens.py
new file mode 100755
index 0000000..ba6475f
--- /dev/null
+++ b/misc/test-tokens.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import unittest
+
+from yt_dlp_plugins.extractor import radiko
+from yt_dlp import YoutubeDL
+
+
+class test_tokens(unittest.TestCase):
+
+ def setUp(self):
+ self.ie = radiko._RadikoBaseIE()
+ ydl = YoutubeDL(auto_init=False)
+ self.ie.set_downloader(ydl)
+
+ def test_area(self):
+ # check areas etc work
+ for i in range(1, 48):
+ area = "JP" + str(i)
+ with self.subTest(f"Negotiating token for {area}", area=area):
+ token = self.ie._negotiate_token(area)
+ self.assertEqual(token.get("X-Radiko-AreaId"), area)
+
+
+if __name__ == '__main__':
+ unittest.main()
+ # may wish to set failfast=True
diff --git a/tests/test_randominfo.py b/tests/test_randominfo.py
deleted file mode 100755
index bd71fdc..0000000
--- a/tests/test_randominfo.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-
-from yt_dlp_plugins.extractor import radiko
-from yt_dlp import YoutubeDL
-
-
-ie = radiko._RadikoBaseIE()
-ydl = YoutubeDL(auto_init=False)
-ie.set_downloader(ydl)
-
-info = ie._generate_random_info()
-print("random device info")
-print(info)
diff --git a/tests/test_tokens.py b/tests/test_tokens.py
deleted file mode 100755
index ba6475f..0000000
--- a/tests/test_tokens.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-
-from yt_dlp_plugins.extractor import radiko
-from yt_dlp import YoutubeDL
-
-
-class test_tokens(unittest.TestCase):
-
- def setUp(self):
- self.ie = radiko._RadikoBaseIE()
- ydl = YoutubeDL(auto_init=False)
- self.ie.set_downloader(ydl)
-
- def test_area(self):
- # check areas etc work
- for i in range(1, 48):
- area = "JP" + str(i)
- with self.subTest(f"Negotiating token for {area}", area=area):
- token = self.ie._negotiate_token(area)
- self.assertEqual(token.get("X-Radiko-AreaId"), area)
-
-
-if __name__ == '__main__':
- unittest.main()
- # may wish to set failfast=True
--
cgit v1.2.3-70-g09d2
From 8285e34af4f17eb5355205ff3651bf30b827d501 Mon Sep 17 00:00:00 2001
From: garret
Date: Sun, 11 Aug 2024 11:50:50 +0100
Subject: fix build so it only does what i want, not just not doing what i dont
want
---
pyproject.toml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 096238e..a0bcc03 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,7 +19,7 @@ requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
-exclude = ["tests"]
+include = ["yt_dlp_plugins.extractor"]
[tool.setuptools.package-data]
-"yt_dlp_plugins.extractor" = ["*.bin"]
+"yt_dlp_plugins.extractor" = ["*.bin", "*.jpg"]
--
cgit v1.2.3-70-g09d2
From bd51e96ddabc48de64ac91970d4bdf2f576b0509 Mon Sep 17 00:00:00 2001
From: garret
Date: Sun, 11 Aug 2024 11:58:31 +0100
Subject: get rid of unnecessary "else"s (+ fix indentation)
---
yt_dlp_plugins/extractor/radiko.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 618e1cb..fa5a15c 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -230,9 +230,9 @@ class _RadikoBaseIE(InfoExtractor):
"meta": meta
})
return meta
- else:
- self.to_screen(f"{station_id}: Using cached station metadata")
- return cachedata.get("meta")
+
+ self.to_screen(f"{station_id}: Using cached station metadata")
+ return cachedata.get("meta")
def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None):
device = self._configuration_arg('device', ['aSmartPhone7a'], casesense=True, ie_key="rajiko")[0] # aSmartPhone7a formats = always happy path
@@ -274,13 +274,13 @@ class _RadikoBaseIE(InfoExtractor):
entry_protocol = 'm3u8'
if domain in self._DOESNT_WORK_WITH_FFMPEG:
- self.write_debug(f"skipping {domain} (known not working)")
- continue
- elif domain in self._DELIVERED_ONDEMAND:
- # override the defaults for delivered as on-demand
- delivered_live = False
- preference = 1
- entry_protocol = None
+ self.write_debug(f"skipping {domain} (known not working)")
+ continue
+ if domain in self._DELIVERED_ONDEMAND:
+ # override the defaults for delivered as on-demand
+ delivered_live = False
+ preference = 1
+ entry_protocol = None
formats += self._extract_m3u8_formats(
playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data,
--
cgit v1.2.3-70-g09d2
From 2fda4016aa082da057d08627c10f3ec49475c8b9 Mon Sep 17 00:00:00 2001
From: garret
Date: Sun, 11 Aug 2024 12:08:39 +0100
Subject: update timefree tests
---
yt_dlp_plugins/extractor/radiko.py | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index fa5a15c..6c27973 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -375,22 +375,22 @@ class RadikoLiveIE(_RadikoBaseIE):
class RadikoTimeFreeIE(_RadikoBaseIE):
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/ts/(?P[A-Z0-9-_]+)/(?P\d+)"
_TESTS = [{
- "url": "https://radiko.jp/#!/ts/INT/20240802230000",
+ "url": "https://radiko.jp/#!/ts/INT/20240809230000",
"info_dict": {
"live_status": "was_live",
"ext": "m4a",
- "id": "INT-20240802230000",
+ "id": "INT-20240809230000",
"title": "TOKYO MOON",
"series": "Tokyo Moon",
"description": "md5:20e68d2f400a391fa34d4e7c8c702cb8",
- "chapters": "count:15",
+ "chapters": "count:14",
"thumbnail": "https://program-static.cf.radiko.jp/ehwtw6mcvy.jpg",
- "upload_date": "20240802",
- "timestamp": 1722607200.0,
- "release_date": "20240802",
- "release_timestamp": 1722610800.0,
+ "upload_date": "20240809",
+ "timestamp": 1723212000.0,
+ "release_date": "20240809",
+ "release_timestamp": 1723215600.0,
"duration": 3600,
"channel": "interfm",
@@ -405,22 +405,22 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
},
}, {
# late-night/early-morning show to test broadcast day checking
- "url": "https://radiko.jp/#!/ts/TBS/20240803033000",
+ "url": "https://radiko.jp/#!/ts/TBS/20240810033000",
"info_dict": {
"live_status": "was_live",
"ext": "m4a",
- "id": "TBS-20240803033000",
+ "id": "TBS-20240810033000",
"title": "CITY CHILL CLUB",
"series": "CITY CHILL CLUB",
"description": "md5:3fba2c1125059bed27247c0be90e58fa",
- "chapters": "count:24",
+ "chapters": "count:22",
"thumbnail": "https://program-static.cf.radiko.jp/ku7t4ztnaq.jpg",
- "upload_date": "20240802",
- "timestamp": 1722623400.0,
- "release_date": "20240802",
- "release_timestamp": 1722628800.0,
+ "upload_date": "20240809",
+ "timestamp": 1723228200.0,
+ "release_date": "20240809",
+ "release_timestamp": 1723233600.0,
"duration": 5400,
"channel": "TBSラジオ",
--
cgit v1.2.3-70-g09d2
From 3cf478a58d81a37cc49a9055ba72582dd2d74365 Mon Sep 17 00:00:00 2001
From: garret
Date: Mon, 26 Aug 2024 16:13:21 +0100
Subject: add useful information back to the readme
---
README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 46 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 1a59bfc..31b8601 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,54 @@
yt-dlp-rajiko is an improved [radiko.jp](https://radiko.jp) extractor plugin for yt-dlp.
-[Please see the website for more information, including installation and usage instructions](https://427738.xyz/yt-dlp-rajiko/).
+## Installation
-If the website is down, an archived copy may be available on [the Internet Archive's Wayback Machine](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/).
+[Download the Python wheel](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.whl) or `pip install
+--extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/ yt-dlp-rajiko`
+
+Requires yt-dlp 2023.06.22 or above.
+
+Use the pip command if you installed yt-dlp with pip. If you installed
+yt-dlp with `pipx`, use `pipx inject --index-url
+https://427738.xyz/yt-dlp-rajiko/pip/ yt-dlp yt-dlp-rajiko` to install
+the plugin in yt-dlp's environment.
+
+Otherwise, download the wheel, and place it in one of these locations:
+
+ - `~/.config/yt-dlp/plugins/` (on Linux and Mac)
+ - `%appdata%/yt-dlp/plugins/` (on Windows)
+ - anywhere else listed in [the yt-dlp
+ documentation](https://github.com/yt-dlp/yt-dlp#installing-plugins).
+
+You'll have to create those folders if they don't already exist.
More information about yt-dlp plugins is available from [yt-dlp's documentation](https://github.com/yt-dlp/yt-dlp#plugins).
-[詳しくは公式サイトをご覧ください](https://427738.xyz/yt-dlp-rajiko/index.ja.html)
+## Usage
+
+simply:
+
+ # timefree download
+ yt-dlp 'https://radiko.jp/#!/ts/INT/20240308230000'
+ # live recording
+ yt-dlp 'https://radiko.jp/#!/live/CCL'
+ # live shorthand
+ yt-dlp 'https://radiko.jp/#FMT'
+
+You can somewhat automate downloading programmes by using the search
+page.
+
+ # all programmes related to Toshiki Kadomatsu
+ yt-dlp 'https://radiko.jp/#!/search/live?key=角松敏生&filter=past®ion_id=all'
+ # specific programme from Osaka
+ yt-dlp 'https://radiko.jp/#!/search/live?key=world%20jazz%20warehouse&filter=past&area_id=JP27'
+
+Just copying from the browser URL bar should work with no changes.
+
+----
+
+[Please see the website for more information](https://427738.xyz/yt-dlp-rajiko/).
+If the website is down, an archived copy may be available on [the Internet Archive's Wayback Machine](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/).
-サイトが無くなった場合は、おそらく[Internet ArchiveのWayback Machineにアーカイブされています](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/index.ja.html)。
+[日本語訳は公式サイトをご覧ください。](https://427738.xyz/yt-dlp-rajiko/index.ja.html)
+サイトが無くなった場合は、多分[Internet ArchiveのWayback Machineにアーカイブされています](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/index.ja.html)。
--
cgit v1.2.3-70-g09d2
From 50808dd36c37fa06d575a08a277c423be1577d7f Mon Sep 17 00:00:00 2001
From: garret
Date: Mon, 26 Aug 2024 16:13:37 +0100
Subject: new step on the release checklist
---
misc/how to do a release | 1 +
1 file changed, 1 insertion(+)
diff --git a/misc/how to do a release b/misc/how to do a release
index ca99af6..f5eb85f 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -9,6 +9,7 @@ python3 -m build
and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
update the pip index html
+update the dl/ "latest" symlinks
## update the changelog file
--
cgit v1.2.3-70-g09d2
From 9f8c0e50a8bd4ddc982b7e4b74c918b5138741c5 Mon Sep 17 00:00:00 2001
From: garret
Date: Mon, 26 Aug 2024 16:14:33 +0100
Subject: add changelog generating script
---
misc/generate_changelog.py | 116 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 116 insertions(+)
create mode 100755 misc/generate_changelog.py
diff --git a/misc/generate_changelog.py b/misc/generate_changelog.py
new file mode 100755
index 0000000..1bce073
--- /dev/null
+++ b/misc/generate_changelog.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+import email.utils
+import feedgenerator
+
+def parse_changelog(lines):
+ got_version = False
+ got_date = False
+ got_url = False
+ done_remarks = False
+ releases = []
+ release = {}
+ release_remarks = []
+ release_changes = []
+ current_change = ""
+
+ for idx, line in enumerate(lines):
+ line = line.rstrip()
+
+ if not got_version:
+ got_version = True
+ release["version"] = line
+ continue
+
+ if not got_date:
+ release["date"] = email.utils.parsedate_to_datetime(line)
+ got_date = True
+ continue
+
+ key, sep, val = line.partition(": ")
+ if key in ["url", "sha256", "released"] and val != "":
+ release[key] = val
+ continue
+
+ if not done_remarks:
+ if line == "":
+ done_remarks = True
+ release["remarks"] = release_remarks
+ release_remarks = []
+ continue
+ else:
+ release_remarks.append(line)
+ continue
+
+ if line != "":
+ release_changes.append(line.rstrip())
+
+ if idx + 1 != len(lines):
+ continue
+
+ release["changes"] = release_changes
+ if release.get("released") != "no":
+ releases.append(release)
+
+ got_version = False
+ got_date = False
+ done_remarks = False
+ release = {}
+ release_changes = []
+
+ return releases
+
+def generate_rss_feed(releases):
+ feed = feedgenerator.Rss201rev2Feed(
+ title="yt-dlp-rajiko changelog",
+ description="Notifications for new yt-dlp-rajiko releases, with changelogs",
+ link="https://427738.xyz/yt-dlp-rajiko/",
+ language="en-GB",
+ ttl=180, # 3 hours
+ )
+
+ for release in releases:
+ title = "yt-dlp-rajiko " + release["version"] + " has been released"
+ description = ""
+ description += "
"
+ for remark in release["remarks"]:
+ description += remark
+ description += " "
+ description += "
"
+ description += "
This release:
\n"
+ description += "
"
+ for change in release["changes"]:
+ description += "
"
+ description += change
+ description += "
\n"
+ description += "
"
+
+ if release.get("url"):
+ if release["version"] != "1.0":
+ description += "\n
If you use pip, you should be able to upgrade with pip install yt-dlp-rajiko --upgrade --extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/. "
+ description += "If you installed manually, you can download the updated .whl from this post's link."
+ if release.get("sha256"):
+ description += " The SHA256 checksum should be "
+ description += release.get("sha256")
+ description += "."
+ description += "
"
+ else:
+ description += '\n
Please see the homepage for initial installation instructions.
'
+
+ feed.add_item(
+ title=title,
+ description=description,
+ link=release.get("url"),
+ pubdate=release["date"]
+ )
+ return feed
+
+if __name__ == "__main__":
+ with open("CHANGELOG") as f:
+ releases = parse_changelog(f.readlines())
+
+ feed = generate_rss_feed(releases)
+ feed_contents = feed.writeString("utf-8")
+ feed_contents = feed_contents.replace("\n
Date: Mon, 26 Aug 2024 22:06:48 +0100
Subject: remove self._region and self._user variables
region wasnt used at all
we can make the token stuff into a dict and include the user as well
...which is what i was already doing in the cache
one day i want to change the cache format to use better var names
but i will deal with that later
---
yt_dlp_plugins/extractor/radiko.py | 31 +++++++++++++------------------
1 file changed, 13 insertions(+), 18 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 6c27973..f9d9dd4 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -84,9 +84,6 @@ class _RadikoBaseIE(InfoExtractor):
_DELIVERED_ONDEMAND = ('radiko.jp',)
_DOESNT_WORK_WITH_FFMPEG = ('tf-f-rpaa-radiko.smartstream.ne.jp', 'si-f-radiko.smartstream.ne.jp')
- _region = None
- _user = None
-
def _index_regions(self):
region_data = {}
@@ -175,30 +172,28 @@ class _RadikoBaseIE(InfoExtractor):
self.report_warning(auth2.strip())
self.report_warning(auth2_headers)
- token = {
- "X-Radiko-AreaId": actual_region,
- "X-Radiko-AuthToken": auth_token,
+ auth_data = {
+ "token": {
+ "X-Radiko-AreaId": actual_region,
+ "X-Radiko-AuthToken": auth_token,
+ },
+ "user": auth2_headers["X-Radiko-User"],
}
- self._user = auth2_headers["X-Radiko-User"]
if not region_mismatch:
- self.cache.store("rajiko", station_region, {
- "token": token,
- "user": self._user,
- })
- return token
+ self.cache.store("rajiko", station_region, auth_data)
+ return auth_data
def _auth(self, station_region):
cachedata = self.cache.load("rajiko", station_region)
self.write_debug(cachedata)
if cachedata is not None:
- token = cachedata.get("token")
- self._user = cachedata.get("user")
+ auth_headers = cachedata.get("token")
response = self._download_webpage("https://radiko.jp/v2/api/auth_check", station_region, "Checking cached token",
- headers=token, expected_status=401)
+ headers=auth_headers, expected_status=401)
self.write_debug(response)
if response == "OK":
- return token
+ return cachedata
return self._negotiate_token(station_region)
def _get_station_meta(self, region, station_id):
@@ -253,7 +248,7 @@ class _RadikoBaseIE(InfoExtractor):
playlist_url = update_url_query(url, {
"station_id": station,
"l": "15", # l = length, ie how many seconds in the live m3u8 (max 300)
- "lsid": self._user,
+ "lsid": auth_data["user"],
"type": "b", # it is a mystery
})
@@ -283,7 +278,7 @@ class _RadikoBaseIE(InfoExtractor):
entry_protocol = None
formats += self._extract_m3u8_formats(
- playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data,
+ playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data["token"],
live=delivered_live, preference=preference, entry_protocol=entry_protocol,
note=f"Downloading m3u8 information from {domain}")
return formats
--
cgit v1.2.3-70-g09d2
From b1d5e613cda58eb4503abda367c2770588ad7fe9 Mon Sep 17 00:00:00 2001
From: garret
Date: Tue, 17 Sep 2024 01:12:59 +0100
Subject: personsIE: fix API usage that excluded programmes aired today
api ignores end_at_lt, it just sets start_at to the start of the broadcast day
the idea was to only get stuff that's actually finished,
so only stuff where the end is before now
with start_at it'll get ongoing stuff as well,
but that's better than not getting the day at all lol
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index f9d9dd4..caaa020 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -709,7 +709,7 @@ class RadikoPersonIE(InfoExtractor):
person_api_url = update_url_query("https://api.radiko.jp/program/api/v1/programs", {
"person_id": person_id,
"start_at_gte": min_start.isoformat(),
- "end_at_lt": now.isoformat(),
+ "start_at_lt": now.isoformat(),
})
person_api = self._download_json(person_api_url, person_id)
--
cgit v1.2.3-70-g09d2
From e9655881313b9b7ca1e402a82f51bbeed8a81e04 Mon Sep 17 00:00:00 2001
From: garret
Date: Tue, 17 Sep 2024 01:18:09 +0100
Subject: prepare for "timefree 30"
https://prtimes.jp/main/html/rd/p/000000032.000007490.html
"radiko to launch new service "Timefree 30" this autumn, allows listening
to programmes from the past 30 days, no 3-hour time limit"
probably can't spoof having the plan, that's fine
it might work if you pass cookies of a timefree30 account though,
so i'm adapting the time stuff to account for that
the plan doesn't exist yet, and i don't know how i would go about
detecting it yet, so i'm just hardcoding to False for now
---
yt_dlp_plugins/extractor/radiko.py | 4 ++--
yt_dlp_plugins/extractor/radiko_time.py | 11 +++++++++++
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index caaa020..d12f203 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -493,7 +493,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
end = times[1]
now = datetime.datetime.now(tz=rtime.JST)
- if end.broadcast_day_end() < now - datetime.timedelta(days=7):
+ if end.expiry(False) < now:
self.raise_no_formats("Programme is no longer available.", video_id=meta["id"], expected=True)
elif start > now:
self.raise_no_formats("Programme has not aired yet.", video_id=meta["id"], expected=True)
@@ -701,7 +701,7 @@ class RadikoPersonIE(InfoExtractor):
now = rtime.RadikoTime.now(tz=rtime.JST)
- min_start = (now - datetime.timedelta(days=7)).broadcast_day_start()
+ min_start = rtime.earliest_available(False)
# we set the earliest time as the earliest we can get,
# so, the start of the broadcast day 1 week ago
# that way we can get everything we can actually download, including stuff that aired at eg "26:00"
diff --git a/yt_dlp_plugins/extractor/radiko_time.py b/yt_dlp_plugins/extractor/radiko_time.py
index 4d51ce5..b383098 100755
--- a/yt_dlp_plugins/extractor/radiko_time.py
+++ b/yt_dlp_plugins/extractor/radiko_time.py
@@ -36,6 +36,17 @@ class RadikoTime(datetime.datetime):
dt = datetime.datetime(date.year, date.month, date.day, 5, 0, 0, tzinfo=JST)
return dt
+ def expiry(self, tf30):
+ available_days = 30 if tf30 else 7
+ return self.broadcast_day_end() + datetime.timedelta(days=available_days)
+ # IF SOMETHING CHANGES HERE YOU NEED TO UPDATE ↓↓earliest_available↓↓ AS WELL!
+
+
+def earliest_available(tf30):
+ available_days = 30 if tf30 else 7
+ return (RadikoTime.now(tz=JST) - datetime.timedelta(days=available_days)).broadcast_day_start()
+ # IF SOMETHING CHANGES HERE YOU NEED TO UPDATE ↑↑expiry↑↑ AS WELL!
+
class RadikoSiteTime(RadikoTime):
--
cgit v1.2.3-70-g09d2
From 8805fd0326aff58332c626107625e80a597b6e45 Mon Sep 17 00:00:00 2001
From: garret
Date: Fri, 1 Nov 2024 09:10:15 +0000
Subject: more right-thinking way of handling timefree 30
still hardcoded to not work though
---
yt_dlp_plugins/extractor/radiko.py | 12 ++++++++----
yt_dlp_plugins/extractor/radiko_time.py | 13 ++++---------
2 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index d12f203..6b8a4c3 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -492,9 +492,13 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
start = times[0]
end = times[1]
now = datetime.datetime.now(tz=rtime.JST)
+ expiry_free, expiry_tf30 = end.expiry()
+ have_tf30 = False
- if end.expiry(False) < now:
+ if expiry_tf30 < now:
self.raise_no_formats("Programme is no longer available.", video_id=meta["id"], expected=True)
+ elif not have_tf30 and expiry_free < now:
+ self.raise_login_required("Programme is only available with a Timefree 30 subscription")
elif start > now:
self.raise_no_formats("Programme has not aired yet.", video_id=meta["id"], expected=True)
live_status = "is_upcoming"
@@ -701,9 +705,9 @@ class RadikoPersonIE(InfoExtractor):
now = rtime.RadikoTime.now(tz=rtime.JST)
- min_start = rtime.earliest_available(False)
- # we set the earliest time as the earliest we can get,
- # so, the start of the broadcast day 1 week ago
+ min_start = now - datetime.timedelta(days=30).broadcast_day_start()
+ # we set the earliest time as the earliest we can get (or at least, that it's possible to get),
+ # so, the start of the broadcast day 30 days ago
# that way we can get everything we can actually download, including stuff that aired at eg "26:00"
person_api_url = update_url_query("https://api.radiko.jp/program/api/v1/programs", {
diff --git a/yt_dlp_plugins/extractor/radiko_time.py b/yt_dlp_plugins/extractor/radiko_time.py
index b383098..d2084aa 100755
--- a/yt_dlp_plugins/extractor/radiko_time.py
+++ b/yt_dlp_plugins/extractor/radiko_time.py
@@ -36,16 +36,11 @@ class RadikoTime(datetime.datetime):
dt = datetime.datetime(date.year, date.month, date.day, 5, 0, 0, tzinfo=JST)
return dt
- def expiry(self, tf30):
- available_days = 30 if tf30 else 7
- return self.broadcast_day_end() + datetime.timedelta(days=available_days)
- # IF SOMETHING CHANGES HERE YOU NEED TO UPDATE ↓↓earliest_available↓↓ AS WELL!
+ def expiry(self):
+ free = self.broadcast_day_end() + datetime.timedelta(days=7)
+ tf30 = self.broadcast_day_end() + datetime.timedelta(days=30)
-
-def earliest_available(tf30):
- available_days = 30 if tf30 else 7
- return (RadikoTime.now(tz=JST) - datetime.timedelta(days=available_days)).broadcast_day_start()
- # IF SOMETHING CHANGES HERE YOU NEED TO UPDATE ↑↑expiry↑↑ AS WELL!
+ return free, tf30
class RadikoSiteTime(RadikoTime):
--
cgit v1.2.3-70-g09d2
From dc2c2e2413ce4c698dae3141f94a9fdd3f9207c0 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 22 Nov 2024 11:45:41 +0000
Subject: fix broken time calculation in PersonsIE
the .broadcast_day_start() was trying to run on the datetime.timedelta,
not the RadikoTime that we calculate
so it just
ERROR: 'datetime.timedelta' object has no attribute 'broadcast_day_start'
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 6b8a4c3..c6cea37 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -705,7 +705,7 @@ class RadikoPersonIE(InfoExtractor):
now = rtime.RadikoTime.now(tz=rtime.JST)
- min_start = now - datetime.timedelta(days=30).broadcast_day_start()
+ min_start = (now - datetime.timedelta(days=30)).broadcast_day_start()
# we set the earliest time as the earliest we can get (or at least, that it's possible to get),
# so, the start of the broadcast day 30 days ago
# that way we can get everything we can actually download, including stuff that aired at eg "26:00"
--
cgit v1.2.3-70-g09d2
From d84e4dbe4f4b1be71e17072dc2a2f3685b9a2997 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 22 Nov 2024 11:40:28 +0000
Subject: support logging in with accounts for timefree30
---
yt_dlp_plugins/extractor/radiko.py | 44 +++++++++++++++++++++++++++++++++-----
1 file changed, 39 insertions(+), 5 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index c6cea37..dc51dd9 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -7,12 +7,14 @@ import pkgutil
from yt_dlp.extractor.common import InfoExtractor
from yt_dlp.utils import (
+ ExtractorError,
OnDemandPagedList,
clean_html,
int_or_none,
join_nonempty,
parse_qs,
traverse_obj,
+ urlencode_postdata,
url_or_none,
update_url_query,
)
@@ -229,8 +231,16 @@ class _RadikoBaseIE(InfoExtractor):
self.to_screen(f"{station_id}: Using cached station metadata")
return cachedata.get("meta")
- def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None):
- device = self._configuration_arg('device', ['aSmartPhone7a'], casesense=True, ie_key="rajiko")[0] # aSmartPhone7a formats = always happy path
+ def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, tf30_override=False):
+ config_device = traverse_obj(self._configuration_arg('device', casesense=True, ie_key="rajiko"), 0)
+
+ if not tf30_override:
+ device = config_device or "aSmartPhone7a" # the only one that works for timefree with this is the on-demand one
+ # that's good imo - we just get the one that works, and don't bother with probing the rest as well
+ else:
+ device = config_device or "pc_html5" # the on-demand one doesnt work with timefree30 stuff sadly
+ # so just use pc_html5 which has everything
+
url_data = self._download_xml(f"https://radiko.jp/v3/station/stream/{device}/{station}.xml",
station, note=f"Downloading {device} stream information")
@@ -368,6 +378,7 @@ class RadikoLiveIE(_RadikoBaseIE):
class RadikoTimeFreeIE(_RadikoBaseIE):
+ _NETRC_MACHINE = "rajiko"
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/ts/(?P[A-Z0-9-_]+)/(?P\d+)"
_TESTS = [{
"url": "https://radiko.jp/#!/ts/INT/20240809230000",
@@ -430,6 +441,29 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
},
}]
+ _has_tf30 = None
+
+ def _perform_login(self, username, password):
+ try:
+ login_info = self._download_json('https://radiko.jp/ap/member/webapi/member/login', None, note='Logging in',
+ data=urlencode_postdata({'mail': username, 'pass': password}))
+ self._has_tf30 = '2' in login_info.get('privileges')
+ # areafree = 1, timefree30 = 2, double plan = both
+ except ExtractorError as error:
+ if isinstance(error.cause, HTTPError) and error.cause.status == 401:
+ raise ExtractorError('Invalid username and/or password', expected=True)
+ raise
+
+ def _check_tf30(self):
+ if self._has_tf30 is not None:
+ return self._has_tf30
+ if self._get_cookies('https://radiko.jp').get('radiko_session') is None:
+ return
+ account_info = self._download_json('https://radiko.jp/ap/member/webapi/v2/member/login/check',
+ None, note='Checking account status from cookies', expected_status=400)
+ self._has_tf30 = account_info.get('timefreeplus') == '1'
+ return self._has_tf30
+
def _get_programme_meta(self, station_id, url_time):
day = url_time.broadcast_day_string()
meta = self._download_json(f"https://radiko.jp/v4/program/station/date/{day}/{station_id}.json", station_id,
@@ -493,11 +527,11 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
end = times[1]
now = datetime.datetime.now(tz=rtime.JST)
expiry_free, expiry_tf30 = end.expiry()
- have_tf30 = False
if expiry_tf30 < now:
self.raise_no_formats("Programme is no longer available.", video_id=meta["id"], expected=True)
- elif not have_tf30 and expiry_free < now:
+ needs_tf30 = expiry_free < now
+ if needs_tf30 and not self._check_tf30():
self.raise_login_required("Programme is only available with a Timefree 30 subscription")
elif start > now:
self.raise_no_formats("Programme has not aired yet.", video_id=meta["id"], expected=True)
@@ -510,7 +544,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
station_meta = self._get_station_meta(region, station)
chapters = self._extract_chapters(station, start, end, video_id=meta["id"])
auth_data = self._auth(region)
- formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end)
+ formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, tf30_override=needs_tf30)
return {
**station_meta,
--
cgit v1.2.3-70-g09d2
From 6661c60ece44309a6a28802d3d55f76d7c332148 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 22 Nov 2024 21:59:45 +0000
Subject: Force new token if cached one doesn't have tf30
---
yt_dlp_plugins/extractor/radiko.py | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index dc51dd9..93f0c93 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -86,6 +86,8 @@ class _RadikoBaseIE(InfoExtractor):
_DELIVERED_ONDEMAND = ('radiko.jp',)
_DOESNT_WORK_WITH_FFMPEG = ('tf-f-rpaa-radiko.smartstream.ne.jp', 'si-f-radiko.smartstream.ne.jp')
+ _has_tf30 = None
+
def _index_regions(self):
region_data = {}
@@ -180,16 +182,20 @@ class _RadikoBaseIE(InfoExtractor):
"X-Radiko-AuthToken": auth_token,
},
"user": auth2_headers["X-Radiko-User"],
+ "tf30": self._has_tf30,
}
if not region_mismatch:
self.cache.store("rajiko", station_region, auth_data)
return auth_data
- def _auth(self, station_region):
+ def _auth(self, station_region, tf30=False):
cachedata = self.cache.load("rajiko", station_region)
self.write_debug(cachedata)
if cachedata is not None:
+ if tf30 and not cachedata.get("tf30"):
+ return self._negotiate_token(station_region, tf30=True)
+
auth_headers = cachedata.get("token")
response = self._download_webpage("https://radiko.jp/v2/api/auth_check", station_region, "Checking cached token",
headers=auth_headers, expected_status=401)
@@ -231,10 +237,10 @@ class _RadikoBaseIE(InfoExtractor):
self.to_screen(f"{station_id}: Using cached station metadata")
return cachedata.get("meta")
- def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, tf30_override=False):
+ def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, tf30=False):
config_device = traverse_obj(self._configuration_arg('device', casesense=True, ie_key="rajiko"), 0)
- if not tf30_override:
+ if not tf30:
device = config_device or "aSmartPhone7a" # the only one that works for timefree with this is the on-demand one
# that's good imo - we just get the one that works, and don't bother with probing the rest as well
else:
@@ -441,8 +447,6 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
},
}]
- _has_tf30 = None
-
def _perform_login(self, username, password):
try:
login_info = self._download_json('https://radiko.jp/ap/member/webapi/member/login', None, note='Logging in',
@@ -543,8 +547,8 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
region = self._get_station_region(station)
station_meta = self._get_station_meta(region, station)
chapters = self._extract_chapters(station, start, end, video_id=meta["id"])
- auth_data = self._auth(region)
- formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, tf30_override=needs_tf30)
+ auth_data = self._auth(region, tf30=needs_tf30)
+ formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, tf30=needs_tf30)
return {
**station_meta,
--
cgit v1.2.3-70-g09d2
From 8c2f5d394d28a683e8f3e6ddf233be84f1ae73c3 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 22 Nov 2024 22:16:19 +0000
Subject: add debug logs for account sign-in process
---
yt_dlp_plugins/extractor/radiko.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 93f0c93..3996c60 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -194,6 +194,7 @@ class _RadikoBaseIE(InfoExtractor):
self.write_debug(cachedata)
if cachedata is not None:
if tf30 and not cachedata.get("tf30"):
+ self.write_debug("Cached token doesn't have timefree 30, getting a new one")
return self._negotiate_token(station_region, tf30=True)
auth_headers = cachedata.get("token")
@@ -453,6 +454,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
data=urlencode_postdata({'mail': username, 'pass': password}))
self._has_tf30 = '2' in login_info.get('privileges')
# areafree = 1, timefree30 = 2, double plan = both
+ self.write_debug({**login_info, "radiko_session": "PRIVATE", "member_ukey": "PRIVATE"})
except ExtractorError as error:
if isinstance(error.cause, HTTPError) and error.cause.status == 401:
raise ExtractorError('Invalid username and/or password', expected=True)
@@ -465,6 +467,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
return
account_info = self._download_json('https://radiko.jp/ap/member/webapi/v2/member/login/check',
None, note='Checking account status from cookies', expected_status=400)
+ self.write_debug({**account_info, "user_key": "PRIVATE"})
self._has_tf30 = account_info.get('timefreeplus') == '1'
return self._has_tf30
--
cgit v1.2.3-70-g09d2
From 1b7830f9e2bf97eaeaee1e6a3936e4361481e961 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 22 Nov 2024 22:45:07 +0000
Subject: clearer variable names/comments + fix getting tf30 token
---
yt_dlp_plugins/extractor/radiko.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 3996c60..9c70a1b 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -182,20 +182,20 @@ class _RadikoBaseIE(InfoExtractor):
"X-Radiko-AuthToken": auth_token,
},
"user": auth2_headers["X-Radiko-User"],
- "tf30": self._has_tf30,
+ "has_tf30": self._has_tf30,
}
if not region_mismatch:
self.cache.store("rajiko", station_region, auth_data)
return auth_data
- def _auth(self, station_region, tf30=False):
+ def _auth(self, station_region, need_tf30=False):
cachedata = self.cache.load("rajiko", station_region)
self.write_debug(cachedata)
if cachedata is not None:
- if tf30 and not cachedata.get("tf30"):
+ if need_tf30 and not cachedata.get("has_tf30"):
self.write_debug("Cached token doesn't have timefree 30, getting a new one")
- return self._negotiate_token(station_region, tf30=True)
+ return self._negotiate_token(station_region)
auth_headers = cachedata.get("token")
response = self._download_webpage("https://radiko.jp/v2/api/auth_check", station_region, "Checking cached token",
@@ -238,11 +238,11 @@ class _RadikoBaseIE(InfoExtractor):
self.to_screen(f"{station_id}: Using cached station metadata")
return cachedata.get("meta")
- def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, tf30=False):
+ def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, tf30_override=False):
config_device = traverse_obj(self._configuration_arg('device', casesense=True, ie_key="rajiko"), 0)
- if not tf30:
- device = config_device or "aSmartPhone7a" # the only one that works for timefree with this is the on-demand one
+ if not tf30_override:
+ device = config_device or "aSmartPhone7a" # this device only gives us the on-demand one for timefree
# that's good imo - we just get the one that works, and don't bother with probing the rest as well
else:
device = config_device or "pc_html5" # the on-demand one doesnt work with timefree30 stuff sadly
@@ -537,8 +537,8 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
if expiry_tf30 < now:
self.raise_no_formats("Programme is no longer available.", video_id=meta["id"], expected=True)
- needs_tf30 = expiry_free < now
- if needs_tf30 and not self._check_tf30():
+ need_tf30 = expiry_free < now
+ if need_tf30 and not self._check_tf30():
self.raise_login_required("Programme is only available with a Timefree 30 subscription")
elif start > now:
self.raise_no_formats("Programme has not aired yet.", video_id=meta["id"], expected=True)
@@ -550,8 +550,8 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
region = self._get_station_region(station)
station_meta = self._get_station_meta(region, station)
chapters = self._extract_chapters(station, start, end, video_id=meta["id"])
- auth_data = self._auth(region, tf30=needs_tf30)
- formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, tf30=needs_tf30)
+ auth_data = self._auth(region, need_tf30=need_tf30)
+ formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, tf30_override=need_tf30)
return {
**station_meta,
--
cgit v1.2.3-70-g09d2
From 5b069ee39394d927fa553902cd9767a7d5b1bfc2 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Tue, 21 Jan 2025 17:53:06 +0000
Subject: missed an import
---
yt_dlp_plugins/extractor/radiko.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 9c70a1b..3839aa9 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -6,6 +6,7 @@ import urllib.parse
import pkgutil
from yt_dlp.extractor.common import InfoExtractor
+from yt_dlp.networking.exceptions import HTTPError
from yt_dlp.utils import (
ExtractorError,
OnDemandPagedList,
--
cgit v1.2.3-70-g09d2
From 1dde2b60989352f9190f3d30f644c30cb2bf2a46 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Wed, 22 Jan 2025 13:07:47 +0000
Subject: probably about time for a release i reckon
---
misc/how to do a release | 24 +++++++-----------------
pyproject.toml | 2 +-
2 files changed, 8 insertions(+), 18 deletions(-)
diff --git a/misc/how to do a release b/misc/how to do a release
index f5eb85f..0f1ee88 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -13,23 +13,13 @@ update the dl/ "latest" symlinks
## update the changelog file
-~/site2/yt-dlp-rajiko/CHANGELOG
-
-```
-version number
-date (git log v1.0 --pretty --date=rfc2822)
-url: whl download link
-sha256: sha256 of the whl
-brief summary of the release
-can span multiple lines
-
-bullet points of changes, 1 per line
-simple present tense, third person singular - continue "this release...", eg..
-fixes a bug where the computer would explode
-makes downloading 5000x faster
-```
-
-./generate_changelog.py to make the new rss feed
+not bothering with the script any more, i just want to spout
+
+write in html, paste into the feed xml like
+make sure to set the link, date
+include the pip instructions, sha256sum etc
## update the website
diff --git a/pyproject.toml b/pyproject.toml
index a0bcc03..cf1716e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.1"
+version = "1.2"
description = "improved radiko.jp extractor for yt-dlp"
authors = [
{ name="garret1317" },
--
cgit v1.2.3-70-g09d2
From c5e0527b4a1a37c6aa3431e3936618e5644de218 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 6 Feb 2025 15:37:26 +0000
Subject: update release instructions
---
misc/how to do a release | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/misc/how to do a release b/misc/how to do a release
index 0f1ee88..ad238db 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -23,8 +23,12 @@ include the pip instructions, sha256sum etc
## update the website
+!!!!!!!!!!!
move the previous release into the "Previous releases"
update the sha256 (just sha256 command in the pip dir)
+!!!!!!!!!!!
+(forgot last time)
+
update the whl link
repeat for japanese version
--
cgit v1.2.3-70-g09d2
From d7aaaaf00397634eeaf7dec7b91d57d1b64bd30a Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 6 Mar 2025 07:31:41 +0000
Subject: filter out "cul_area_id" in search params - HTTP Error 415
site used to use "cul_area_id" in the search url, now it uses "cur_area_id" (with an r)
and it outright rejects the old one with HTTP Error 415: Unsupported Media Type
probably better to patch in the extractor than to break peoples scripts etc - site does this too
idk why they felt the need to break the old one
---
yt_dlp_plugins/extractor/radiko.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 3839aa9..9ca353b 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -609,6 +609,11 @@ class RadikoSearchIE(InfoExtractor):
# urllib.parse interprets the path as just one giant fragment because of the #, so we hack it away
queries = parse_qs(url)
+ if queries.get("cul_area_id"):
+ queries["cur_area_id"] = queries.pop("cul_area_id")
+ # site used to use "cul_area_id" in the search url, now it uses "cur_area_id" (with an r)
+ # and outright rejects the old one with HTTP Error 415: Unsupported Media Type
+
search_url = update_url_query("https://radiko.jp/v3/api/program/search", {
**queries,
"uid": "".join(random.choices("0123456789abcdef", k=32)),
--
cgit v1.2.3-70-g09d2
From 23effbcd8c423018b2b104125509806b36ee0985 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 6 Mar 2025 07:33:54 +0000
Subject: switch to different search API url observed on site
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
no change in the response structure*, just the URL afaict (stuff doesnt break at least)
interestingly the headers have started including GRPC stuff though
*but the search _results_ seem to have changed somewhat
had to change my search for ジャズ倶楽部 to specifically ジャズ倶楽部 CRT
because loads of results had "ジャズ・クラブ" - not katakana クラブ not kanji 倶楽部
maybe its not changed and i just got unlucky lol
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 9ca353b..a06b1e4 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -614,7 +614,7 @@ class RadikoSearchIE(InfoExtractor):
# site used to use "cul_area_id" in the search url, now it uses "cur_area_id" (with an r)
# and outright rejects the old one with HTTP Error 415: Unsupported Media Type
- search_url = update_url_query("https://radiko.jp/v3/api/program/search", {
+ search_url = update_url_query("https://api.annex-cf.radiko.jp/v1/programs/legacy/perl/program/search", {
**queries,
"uid": "".join(random.choices("0123456789abcdef", k=32)),
"app_id": "pc",
--
cgit v1.2.3-70-g09d2
From 2fa0266dd79ebeef5a1b78c9a194b79851196b26 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 6 Mar 2025 07:44:33 +0000
Subject: hotfix release time
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index cf1716e..d34d6f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.2"
+version = "1.3"
description = "improved radiko.jp extractor for yt-dlp"
authors = [
{ name="garret1317" },
--
cgit v1.2.3-70-g09d2
From 1083eada23729aa799826f517c282f36db6e9d3c Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 27 Mar 2025 20:08:55 +0000
Subject: add new alliance-stream-radiko.smartstream.ne.jp format to ffmpeg
blacklist
github issue #25
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index a06b1e4..3f28f6b 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -85,7 +85,7 @@ class _RadikoBaseIE(InfoExtractor):
_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"]
_DELIVERED_ONDEMAND = ('radiko.jp',)
- _DOESNT_WORK_WITH_FFMPEG = ('tf-f-rpaa-radiko.smartstream.ne.jp', 'si-f-radiko.smartstream.ne.jp')
+ _DOESNT_WORK_WITH_FFMPEG = ('tf-f-rpaa-radiko.smartstream.ne.jp', 'si-f-radiko.smartstream.ne.jp', 'alliance-stream-radiko.smartstream.ne.jp')
_has_tf30 = None
--
cgit v1.2.3-70-g09d2
From fff631e63dfaa8bf3ec33c3fe4b5d04031b508bf Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 30 Mar 2025 16:20:53 +0100
Subject: add extractor arg to disable the blacklist
---
yt_dlp_plugins/extractor/radiko.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 3f28f6b..9a74f34 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -256,6 +256,7 @@ class _RadikoBaseIE(InfoExtractor):
formats = []
timefree_int = 1 if timefree else 0
+ do_blacklist_streams = not len(self._configuration_arg("no_stream_blacklist", ie_key="rajiko")) > 0
for element in url_data.findall(f".//url[@timefree='{timefree_int}'][@areafree='0']/playlist_create_url"):
# find s with matching timefree and no areafree, then get their
url = element.text
@@ -286,7 +287,8 @@ class _RadikoBaseIE(InfoExtractor):
preference = -1
entry_protocol = 'm3u8'
- if domain in self._DOESNT_WORK_WITH_FFMPEG:
+
+ if domain in self._DOESNT_WORK_WITH_FFMPEG and do_blacklist_streams:
self.write_debug(f"skipping {domain} (known not working)")
continue
if domain in self._DELIVERED_ONDEMAND:
--
cgit v1.2.3-70-g09d2
From 2f1bcb85c51bc3c623ace03cabbd88d033eb4720 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 30 Mar 2025 16:21:30 +0100
Subject: release 1.4
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index d34d6f1..b83ccd9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.3"
+version = "1.4"
description = "improved radiko.jp extractor for yt-dlp"
authors = [
{ name="garret1317" },
--
cgit v1.2.3-70-g09d2
From b879d27a7b4fa15ecb41483c25f762eb5d4cb6fa Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 17 May 2025 19:29:02 +0100
Subject: update "type" comment with new information
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 9a74f34..c9d2756 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -268,7 +268,7 @@ class _RadikoBaseIE(InfoExtractor):
"station_id": station,
"l": "15", # l = length, ie how many seconds in the live m3u8 (max 300)
"lsid": auth_data["user"],
- "type": "b", # it is a mystery
+ "type": "b", # a/b = in-region, c = areafree
})
if timefree:
--
cgit v1.2.3-70-g09d2
From c028c876287e7dbbd3fe7e1350f36c03d84b40f5 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 17 May 2025 22:20:20 +0100
Subject: add quick-downloading support for as-live formats
closes #24
---
yt_dlp_plugins/extractor/radiko.py | 23 ++++++++++++++----
yt_dlp_plugins/extractor/radiko_hacks.py | 41 ++++++++++++++++++++++++++++++++
2 files changed, 59 insertions(+), 5 deletions(-)
create mode 100644 yt_dlp_plugins/extractor/radiko_hacks.py
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index c9d2756..e371878 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -21,6 +21,7 @@ from yt_dlp.utils import (
)
import yt_dlp_plugins.extractor.radiko_time as rtime
+import yt_dlp_plugins.extractor.radiko_hacks as hacks
class _RadikoBaseIE(InfoExtractor):
@@ -287,7 +288,6 @@ class _RadikoBaseIE(InfoExtractor):
preference = -1
entry_protocol = 'm3u8'
-
if domain in self._DOESNT_WORK_WITH_FFMPEG and do_blacklist_streams:
self.write_debug(f"skipping {domain} (known not working)")
continue
@@ -297,10 +297,23 @@ class _RadikoBaseIE(InfoExtractor):
preference = 1
entry_protocol = None
- formats += self._extract_m3u8_formats(
- playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data["token"],
- live=delivered_live, preference=preference, entry_protocol=entry_protocol,
- note=f"Downloading m3u8 information from {domain}")
+ if delivered_live and timefree:
+ chunks = hacks._generate_as_live_chunks(playlist_url, start_at, end_at)
+
+ formats.append({
+ "url": playlist_url, # fallback to live for ffmpeg etc
+ "format_id": join_nonempty(domain, "chunked"),
+ "live": False,
+ "hls_media_playlist_data": hacks._playlist_from_chunks(self, chunks, domain, auth_data["token"]),
+ "preference": preference,
+ "ext": "m4a",
+ })
+ else:
+
+ formats += self._extract_m3u8_formats(
+ playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data["token"],
+ live=delivered_live, preference=preference, entry_protocol=entry_protocol,
+ note=f"Downloading m3u8 information from {domain}")
return formats
diff --git a/yt_dlp_plugins/extractor/radiko_hacks.py b/yt_dlp_plugins/extractor/radiko_hacks.py
new file mode 100644
index 0000000..7b20654
--- /dev/null
+++ b/yt_dlp_plugins/extractor/radiko_hacks.py
@@ -0,0 +1,41 @@
+import datetime
+
+from yt_dlp.extractor.common import InfoExtractor
+from yt_dlp.utils import (
+ join_nonempty,
+ update_url_query,
+ traverse_obj,
+)
+
+# "hacks" as in great jank/schizo shit that works anyway
+
+def _generate_as_live_chunks(playlist_url, start_at, end_at):
+ chunks = []
+ chunk_length = 300 # max the api allows
+
+ duration = int(end_at.timestamp() - start_at.timestamp())
+ cursor = 0
+ while cursor < duration:
+ chunk_length = min(chunk_length, duration - cursor)
+ chunk_start = start_at + datetime.timedelta(seconds=cursor)
+ chunk_url = update_url_query(playlist_url, {
+ "seek": chunk_start.timestring(),
+ "l": chunk_length,
+ })
+ chunks.append(chunk_url)
+ cursor += chunk_length
+
+ return chunks
+
+def _playlist_from_chunks(self, chunks, src_id, headers={}):
+ playlist = ""
+ for i, chunk in enumerate(chunks):
+ i +=1 # for more friendly cli output, it gets reset each loop so it shouldnt effect anything
+ chunk_id = join_nonempty(src_id, i)
+ base_format = self._extract_m3u8_formats(
+ chunk, chunk_id, fatal=False, headers=headers,
+ note=f"Preparing {src_id} chunk {i}"
+ )
+ m3u8_url = traverse_obj(base_format, (..., "url",), get_all=False)
+ playlist += self._download_webpage(m3u8_url, chunk_id, note=f"Getting {src_id} chunk {i} fragments")
+ return playlist
--
cgit v1.2.3-70-g09d2
From 8040cd476aea513884a8897efb2f0253ed34d6db Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 17 May 2025 22:22:19 +0100
Subject: bump yt-dlp version requirement
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 31b8601..35906ab 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ yt-dlp-rajiko is an improved [radiko.jp](https://radiko.jp) extractor plugin for
[Download the Python wheel](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.whl) or `pip install
--extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/ yt-dlp-rajiko`
-Requires yt-dlp 2023.06.22 or above.
+Requires yt-dlp 2025.02.19 or above.
Use the pip command if you installed yt-dlp with pip. If you installed
yt-dlp with `pipx`, use `pipx inject --index-url
--
cgit v1.2.3-70-g09d2
From 3d9a1d81293797a68dd8b2b4a45ad31418180477 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 17 May 2025 22:39:44 +0100
Subject: add extractor arg to disable as-live workaround
---
yt_dlp_plugins/extractor/radiko.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index e371878..b20f9bb 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -258,6 +258,7 @@ class _RadikoBaseIE(InfoExtractor):
timefree_int = 1 if timefree else 0
do_blacklist_streams = not len(self._configuration_arg("no_stream_blacklist", ie_key="rajiko")) > 0
+ do_as_live_chunks = not len(self._configuration_arg("no_as_live_chunks", ie_key="rajiko")) > 0
for element in url_data.findall(f".//url[@timefree='{timefree_int}'][@areafree='0']/playlist_create_url"):
# find s with matching timefree and no areafree, then get their
url = element.text
@@ -297,7 +298,7 @@ class _RadikoBaseIE(InfoExtractor):
preference = 1
entry_protocol = None
- if delivered_live and timefree:
+ if delivered_live and timefree and do_as_live_chunks:
chunks = hacks._generate_as_live_chunks(playlist_url, start_at, end_at)
formats.append({
--
cgit v1.2.3-70-g09d2
From d56d025e635dfe83dd1897956e99349f8b125ead Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 18 May 2025 12:10:06 +0100
Subject: track real chunk duration to prevent dupes when chunks are longer
than expected
closes #24 for real this time
---
yt_dlp_plugins/extractor/radiko.py | 7 ++--
yt_dlp_plugins/extractor/radiko_hacks.py | 56 +++++++++++++++++++++++---------
2 files changed, 45 insertions(+), 18 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index b20f9bb..e8174c6 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -299,13 +299,16 @@ class _RadikoBaseIE(InfoExtractor):
entry_protocol = None
if delivered_live and timefree and do_as_live_chunks:
- chunks = hacks._generate_as_live_chunks(playlist_url, start_at, end_at)
+
+ chunks_playlist = hacks._generate_as_live_playlist(
+ self, playlist_url, start_at, end_at, domain, auth_data["token"]
+ )
formats.append({
"url": playlist_url, # fallback to live for ffmpeg etc
"format_id": join_nonempty(domain, "chunked"),
"live": False,
- "hls_media_playlist_data": hacks._playlist_from_chunks(self, chunks, domain, auth_data["token"]),
+ "hls_media_playlist_data": chunks_playlist,
"preference": preference,
"ext": "m4a",
})
diff --git a/yt_dlp_plugins/extractor/radiko_hacks.py b/yt_dlp_plugins/extractor/radiko_hacks.py
index 7b20654..eb68de1 100644
--- a/yt_dlp_plugins/extractor/radiko_hacks.py
+++ b/yt_dlp_plugins/extractor/radiko_hacks.py
@@ -1,4 +1,5 @@
import datetime
+import re
from yt_dlp.extractor.common import InfoExtractor
from yt_dlp.utils import (
@@ -9,33 +10,56 @@ from yt_dlp.utils import (
# "hacks" as in great jank/schizo shit that works anyway
-def _generate_as_live_chunks(playlist_url, start_at, end_at):
- chunks = []
+def _generate_as_live_playlist(self, playlist_url, start_at, end_at, domain, headers={}):
+ playlist = ""
chunk_length = 300 # max the api allows
duration = int(end_at.timestamp() - start_at.timestamp())
cursor = 0
+ chunk_num = 1
while cursor < duration:
chunk_length = min(chunk_length, duration - cursor)
+
chunk_start = start_at + datetime.timedelta(seconds=cursor)
chunk_url = update_url_query(playlist_url, {
"seek": chunk_start.timestring(),
"l": chunk_length,
})
- chunks.append(chunk_url)
- cursor += chunk_length
- return chunks
+ chunk_playlist, real_chunk_length = _get_chunk_playlist(self, chunk_url, domain, chunk_num, headers)
+
+ playlist += chunk_playlist
+ cursor += real_chunk_length
+ chunk_num += 1
-def _playlist_from_chunks(self, chunks, src_id, headers={}):
- playlist = ""
- for i, chunk in enumerate(chunks):
- i +=1 # for more friendly cli output, it gets reset each loop so it shouldnt effect anything
- chunk_id = join_nonempty(src_id, i)
- base_format = self._extract_m3u8_formats(
- chunk, chunk_id, fatal=False, headers=headers,
- note=f"Preparing {src_id} chunk {i}"
- )
- m3u8_url = traverse_obj(base_format, (..., "url",), get_all=False)
- playlist += self._download_webpage(m3u8_url, chunk_id, note=f"Getting {src_id} chunk {i} fragments")
return playlist
+
+def _get_chunk_playlist(self, chunk_url, src_id, chunk_num, headers={}):
+ EXTINF_duration = re.compile("^#EXTINF:([\d.]+),", flags=re.MULTILINE)
+
+ playlist = ""
+ chunk_id = join_nonempty(src_id, chunk_num)
+ base_format = self._extract_m3u8_formats(
+ chunk_url, chunk_id, fatal=False, headers=headers,
+ note=f"Preparing {src_id} chunk {chunk_num}"
+ )
+ m3u8_url = traverse_obj(base_format, (..., "url",), get_all=False)
+ playlist = self._download_webpage(m3u8_url, chunk_id, note=f"Getting {src_id} chunk {chunk_num} fragments")
+
+ real_duration = 0
+ for i in EXTINF_duration.findall(playlist):
+ real_duration += float(i)
+ real_duration = round(real_duration)
+
+ # playlists can sometimes be longer than they should
+ # wowza stream does some strange things
+ # it goes along fine with every fragment 5s long as normal
+ # and then during the ad break it does one with a different length (2s here)
+ # i assume so they have a clean split to do ad insertion in? idk
+
+ # but anyway now the chunks aren't always a clean 5mins long
+ # and we get a repeated fragment going into the next chunk
+
+ # so to work around this, we track the real duration from the #EXTINF tags
+
+ return playlist, real_duration
--
cgit v1.2.3-70-g09d2
From e9790a7589dd9b2b4ebb2c27763ff56ba37e4647 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 18 May 2025 12:49:37 +0100
Subject: update readme
---
README.md | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 35906ab..7519d82 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
yt-dlp-rajiko is an improved [radiko.jp](https://radiko.jp) extractor plugin for yt-dlp.
+[日本語訳はこちら](https://427738.xyz/yt-dlp-rajiko/index.ja.html)
+
## Installation
[Download the Python wheel](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.whl) or `pip install
@@ -46,10 +48,12 @@ page.
Just copying from the browser URL bar should work with no changes.
+## notes about this repository
+
+Generally you should use the release versions.
+`master` branch usually works, but should be considered experimental and may have bugs
+
----
[Please see the website for more information](https://427738.xyz/yt-dlp-rajiko/).
If the website is down, an archived copy may be available on [the Internet Archive's Wayback Machine](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/).
-
-[日本語訳は公式サイトをご覧ください。](https://427738.xyz/yt-dlp-rajiko/index.ja.html)
-サイトが無くなった場合は、多分[Internet ArchiveのWayback Machineにアーカイブされています](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/index.ja.html)。
--
cgit v1.2.3-70-g09d2
From bd7dcd8b5b70824ac8131f15008f82fb79b10325 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 18 May 2025 12:51:23 +0100
Subject: bump licence year
---
LICENCE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/LICENCE b/LICENCE
index 9041052..ba3d837 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,6 +1,6 @@
BSD Zero Clause License
-Copyright (c) 2023, 2024 garret1317
+Copyright (c) 2023, 2024, 2025 garret1317
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
--
cgit v1.2.3-70-g09d2
From d2b9505aa7bf0128e1c2f6bc062f41afa149c41c Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 19 May 2025 02:05:46 +0100
Subject: cleanup m3u8 request headers into a variable
---
yt_dlp_plugins/extractor/radiko.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index e8174c6..acad1c9 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -298,10 +298,12 @@ class _RadikoBaseIE(InfoExtractor):
preference = 1
entry_protocol = None
+ auth_headers = auth_data["token"]
+
if delivered_live and timefree and do_as_live_chunks:
chunks_playlist = hacks._generate_as_live_playlist(
- self, playlist_url, start_at, end_at, domain, auth_data["token"]
+ self, playlist_url, start_at, end_at, domain, auth_headers
)
formats.append({
@@ -315,7 +317,7 @@ class _RadikoBaseIE(InfoExtractor):
else:
formats += self._extract_m3u8_formats(
- playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_data["token"],
+ playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_headers,
live=delivered_live, preference=preference, entry_protocol=entry_protocol,
note=f"Downloading m3u8 information from {domain}")
return formats
--
cgit v1.2.3-70-g09d2
From 6822dcb17ae217d0edef74970f4dff3fd0982381 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 19 May 2025 02:06:31 +0100
Subject: chunked: properly fallback to live for ffmpeg etc
---
yt_dlp_plugins/extractor/radiko.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index acad1c9..06c85fa 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -307,12 +307,14 @@ class _RadikoBaseIE(InfoExtractor):
)
formats.append({
- "url": playlist_url, # fallback to live for ffmpeg etc
"format_id": join_nonempty(domain, "chunked"),
- "live": False,
"hls_media_playlist_data": chunks_playlist,
"preference": preference,
"ext": "m4a",
+
+ # fallback to live for ffmpeg etc
+ "url": playlist_url,
+ "http_headers": auth_headers,
})
else:
--
cgit v1.2.3-70-g09d2
From 0baa458aae29d78214d05f5c28272866bd838082 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 19 May 2025 10:56:25 +0100
Subject: add (partial) downloads to gitignore
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index 04a43b4..23d37d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ __pycache__
*.pyc
wiki/
dist/
+*.m4a*
+*.m3u8
--
cgit v1.2.3-70-g09d2
From f857c544a0d1c56d3d5b7a05e18afd44a34ed17d Mon Sep 17 00:00:00 2001
From: garret1317
Date: Wed, 21 May 2025 16:15:54 +0100
Subject: make tracklist downloading non-fatal so MAJAL works
MUSIC AWARDS JAPAN AUDIO LIVE pop-up stream
---
yt_dlp_plugins/extractor/radiko.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 06c85fa..5f22451 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -532,10 +532,12 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
"start_time_gte": start.isoformat(),
"end_time_lt": end.isoformat(),
})
- data = self._download_json(api_url, video_id, note="Downloading tracklist").get("data")
+ data_json = self._download_json(
+ api_url, video_id, note="Downloading tracklist", errnote="Downloading tracklist", fatal=False
+ )
chapters = []
- for track in data:
+ for track in traverse_obj(data_json, "data") or []:
artist = traverse_obj(track, ("artist", "name")) or track.get("artist_name")
chapters.append({
"title": join_nonempty(artist, track.get("title"), delim=" - "),
--
cgit v1.2.3-70-g09d2
From a659b48a20b6241a68fc046c86c9ad0f71c9bd86 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 24 May 2025 19:28:00 +0100
Subject: make SearchIE return an id in advance, for download archives
to prevent un-needed extractions when somethings in the archive already
absolutely kicking myself that it was this simple and i only just did it
---
yt_dlp_plugins/extractor/radiko.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 5f22451..8f84bd7 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -619,15 +619,25 @@ class RadikoSearchIE(InfoExtractor):
}]
def _strip_date(self, date):
+ # lazy way of making a timestring (from eg 2025-05-20 01:00:00)
return date.replace(" ", "").replace("-", "").replace(":", "")
def _pagefunc(self, url, idx):
url = update_url_query(url, {"page_idx": idx})
data = self._download_json(url, None, note=f"Downloading page {idx+1}")
- return [self.url_result("https://radiko.jp/#!/ts/{station}/{time}".format(
- station = i.get("station_id"), time = self._strip_date(i.get("start_time"))))
- for i in data.get("data")]
+ results = []
+ for r in data.get("data"):
+ station = r.get("station_id")
+ timestring = self._strip_date(r.get("start_time"))
+
+ results.append(
+ self.url_result(
+ f"https://radiko.jp/#!/ts/{station}/{timestring}",
+ id=join_nonempty(station, timestring)
+ )
+ )
+ return results
def _real_extract(self, url):
url = url.replace("/#!/", "/!/", 1)
--
cgit v1.2.3-70-g09d2
From ff7d954fd1ecb7b99e5a2afe1b4fe3b9a760aec0 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 24 May 2025 20:40:08 +0100
Subject: update release instructions
---
misc/how to do a release | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/misc/how to do a release b/misc/how to do a release
index ad238db..cfd495b 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -8,8 +8,8 @@ python3 -m build
and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
-update the pip index html
-update the dl/ "latest" symlinks
+
+run script to update the pip index html and the dl/ "latest" symlinks
## update the changelog file
@@ -19,6 +19,7 @@ write in html, paste into the feed xml like
make sure to set the link, date
+(git log --pretty --date=rfc2822)
include the pip instructions, sha256sum etc
## update the website
--
cgit v1.2.3-70-g09d2
From c3beebb5011bca8cc1574bee63fc282e34d42835 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 24 May 2025 19:41:06 +0100
Subject: release 1.5
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index b83ccd9..4a606d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.4"
+version = "1.5"
description = "improved radiko.jp extractor for yt-dlp"
authors = [
{ name="garret1317" },
--
cgit v1.2.3-70-g09d2
From 438bf8f69595db9b33d81b0c820c0989f68c2f57 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 24 May 2025 21:08:08 +0100
Subject: update readme
---
README.md | 51 +++++++++++++++++++++++++++++++++------------------
1 file changed, 33 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 7519d82..e93f475 100644
--- a/README.md
+++ b/README.md
@@ -30,30 +30,45 @@ More information about yt-dlp plugins is available from [yt-dlp's documentation]
## Usage
simply:
+```
+# timefree download
+yt-dlp 'https://radiko.jp/#!/ts/INT/20240308230000'
+# live recording
+yt-dlp 'https://radiko.jp/#!/live/CCL'
+# live shorthand
+yt-dlp 'https://radiko.jp/#FMT'
+```
- # timefree download
- yt-dlp 'https://radiko.jp/#!/ts/INT/20240308230000'
- # live recording
- yt-dlp 'https://radiko.jp/#!/live/CCL'
- # live shorthand
- yt-dlp 'https://radiko.jp/#FMT'
+You can semi-automatically grab the latest episodes of programmes by using the search page, like this:
+```
+# single-station programme yt-dlp
+'https://radiko.jp/#!/search/live?key=World Jazz Warehouse&filter=past'
+# area filtering also works
+yt-dlp 'https://radiko.jp/#!/search/live?key=tokyo speakeasy&filter=past&area_id=JP13'
+```
+(though of course you still need to be there to press the button)
-You can somewhat automate downloading programmes by using the search
-page.
+If you can reliably get it in the search, you can somewhat-automate downloading it.
+If there's a programme that airs on multiple stations, the best way to filter down to the station you want is to use the search's 地域 (region) filter.
- # all programmes related to Toshiki Kadomatsu
- yt-dlp 'https://radiko.jp/#!/search/live?key=角松敏生&filter=past®ion_id=all'
- # specific programme from Osaka
- yt-dlp 'https://radiko.jp/#!/search/live?key=world%20jazz%20warehouse&filter=past&area_id=JP27'
+You can also get programmes that a person has appeared in, using the links from those little boxes on the side:
+`yt-dlp 'https://radiko.jp/persons/3363'`
-Just copying from the browser URL bar should work with no changes.
+you can limit it to the "key station" only, like so:
+`yt-dlp 'https://radiko.jp/persons/33635' --extractor-args rajiko:key-station-only`
+
+This is meant as an alternative to the region filter, for networked programmes where the same thing airs on multiple stations.
+Currently /persons/ URLs are the only place where that option works, as the information just isn't available for normal search results.
+
+As a general rule, just copying from the browser URL bar should work with no changes. (if it doesn't, [let me know!](https://github.com/garret1317/yt-dlp-rajiko/issues))
+
+(Apparently on Windows it won't work unless you use "double quotes", but on Linux it won't work unless you use 'single quotes'. If the command doesnt work with one quote type then try the other.)
+
+**[You can find more usage tips on the website](https://427738.xyz/yt-dlp-rajiko/)**
## notes about this repository
+this is just where the source code and bug tracker live. most of the info is on the website.
+
Generally you should use the release versions.
`master` branch usually works, but should be considered experimental and may have bugs
-
-----
-
-[Please see the website for more information](https://427738.xyz/yt-dlp-rajiko/).
-If the website is down, an archived copy may be available on [the Internet Archive's Wayback Machine](https://web.archive.org/web/*/https://427738.xyz/yt-dlp-rajiko/).
--
cgit v1.2.3-70-g09d2
From 83ff35b140638c665547f55b1e4b876482ec3ce8 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 25 May 2025 12:16:13 +0100
Subject: make ShareIE return an id in advance as well
in my excitement i may have prematurely released (har har) v1.5 with the
return-id-in-advance trick only added to SearchIE
oh well, it can be in ShareIE for next release
adding to ShareIE has knock-on effects for PersonsIE as it returns a share url
two birds with one stone
---
yt_dlp_plugins/extractor/radiko.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 8f84bd7..fc09724 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -711,7 +711,10 @@ class RadikoShareIE(InfoExtractor):
station = traverse_obj(queries, ("sid", 0))
time = traverse_obj(queries, ("t", 0))
time = rtime.RadikoShareTime(time).timestring()
- return self.url_result(f"https://radiko.jp/#!/ts/{station}/{time}", RadikoTimeFreeIE)
+ return self.url_result(
+ f"https://radiko.jp/#!/ts/{station}/{time}", RadikoTimeFreeIE,
+ id=join_nonempty(station, time)
+ )
class RadikoStationButtonIE(InfoExtractor):
--
cgit v1.2.3-70-g09d2
From 596c845c9d439e67d4a39faf7dc9f6e7b1d8d86e Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 25 May 2025 17:32:28 +0100
Subject: switch programme data api to api.radiko.jp as used on site
identical content
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index fc09724..b9bb056 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -497,7 +497,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
def _get_programme_meta(self, station_id, url_time):
day = url_time.broadcast_day_string()
- meta = self._download_json(f"https://radiko.jp/v4/program/station/date/{day}/{station_id}.json", station_id,
+ meta = self._download_json(f"https://api.radiko.jp/program/v4/date/{day}/station/{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)
--
cgit v1.2.3-70-g09d2
From 543de39a20b8a4c19baf468e0a4946a4610d30de Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 30 May 2025 21:47:52 +0100
Subject: fix SyntaxWarning: invalid escape sequence '\d' in py3.12
---
yt_dlp_plugins/extractor/radiko_hacks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko_hacks.py b/yt_dlp_plugins/extractor/radiko_hacks.py
index eb68de1..6486034 100644
--- a/yt_dlp_plugins/extractor/radiko_hacks.py
+++ b/yt_dlp_plugins/extractor/radiko_hacks.py
@@ -35,7 +35,7 @@ def _generate_as_live_playlist(self, playlist_url, start_at, end_at, domain, hea
return playlist
def _get_chunk_playlist(self, chunk_url, src_id, chunk_num, headers={}):
- EXTINF_duration = re.compile("^#EXTINF:([\d.]+),", flags=re.MULTILINE)
+ EXTINF_duration = re.compile(r"^#EXTINF:([\d.]+),", flags=re.MULTILINE)
playlist = ""
chunk_id = join_nonempty(src_id, chunk_num)
--
cgit v1.2.3-70-g09d2
From bc161b9a04a735ac43dc44bccbd6e4d5639ab2bd Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 1 Jun 2025 12:12:42 +0100
Subject: set device to pc_html5 for live as well
aSmartPhone7a removed working stream
---
yt_dlp_plugins/extractor/radiko.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index b9bb056..f58e744 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -240,10 +240,10 @@ class _RadikoBaseIE(InfoExtractor):
self.to_screen(f"{station_id}: Using cached station metadata")
return cachedata.get("meta")
- def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, tf30_override=False):
+ def _get_station_formats(self, station, timefree, auth_data, start_at=None, end_at=None, use_pc_html5=False):
config_device = traverse_obj(self._configuration_arg('device', casesense=True, ie_key="rajiko"), 0)
- if not tf30_override:
+ if not use_pc_html5:
device = config_device or "aSmartPhone7a" # this device only gives us the on-demand one for timefree
# that's good imo - we just get the one that works, and don't bother with probing the rest as well
else:
@@ -398,7 +398,7 @@ class RadikoLiveIE(_RadikoBaseIE):
region = self._get_station_region(station)
station_meta = self._get_station_meta(region, station)
auth_data = self._auth(region)
- formats = self._get_station_formats(station, False, auth_data)
+ formats = self._get_station_formats(station, False, auth_data, use_pc_html5=True)
return {
"is_live": True,
@@ -577,7 +577,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
station_meta = self._get_station_meta(region, station)
chapters = self._extract_chapters(station, start, end, video_id=meta["id"])
auth_data = self._auth(region, need_tf30=need_tf30)
- formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, tf30_override=need_tf30)
+ formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, use_pc_html5=need_tf30)
return {
**station_meta,
--
cgit v1.2.3-70-g09d2
From 2e5964818ca891880f6d2e00aeb7d613710483a4 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 1 Jun 2025 17:15:30 +0100
Subject: release 1.6 (hotfix)
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 4a606d2..4d57eb8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.5"
+version = "1.6"
description = "improved radiko.jp extractor for yt-dlp"
authors = [
{ name="garret1317" },
--
cgit v1.2.3-70-g09d2
From 12801d5e2648b1e9cec2a9c3dfa5a77668ed638b Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 1 Jun 2025 17:40:44 +0100
Subject: update release instructions
---
misc/how to do a release | 16 ++++------------
1 file changed, 4 insertions(+), 12 deletions(-)
diff --git a/misc/how to do a release b/misc/how to do a release
index cfd495b..785533e 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -10,11 +10,10 @@ and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-raji
because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
run script to update the pip index html and the dl/ "latest" symlinks
+this also updates the sha256s on the site
## update the changelog file
-not bothering with the script any more, i just want to spout
-
write in html, paste into the feed xml like
@@ -22,21 +21,14 @@ make sure to set the link, date
(git log --pretty --date=rfc2822)
include the pip instructions, sha256sum etc
-## update the website
-
-!!!!!!!!!!!
-move the previous release into the "Previous releases"
-update the sha256 (just sha256 command in the pip dir)
-!!!!!!!!!!!
-(forgot last time)
-
-update the whl link
-repeat for japanese version
now push to the server
## update github
paste the changelog output into a github release, upload the new builds
+change link at the bottom to just "below"
+
+post in the radiko thread on 5ch if i can be bothered
and thats probably all
--
cgit v1.2.3-70-g09d2
From 50bfb27bd17236ccdc5dadf87b461c418edf2508 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 5 Jun 2025 18:06:37 +0100
Subject: add useful scripts used in release/development
---
misc/generate_changelog.py | 116 -----------------------------------------
misc/generate_html.py | 84 +++++++++++++++++++++++++++++
misc/old_generate_changelog.py | 116 +++++++++++++++++++++++++++++++++++++++++
misc/streammon.py | 53 +++++++++++++++++++
4 files changed, 253 insertions(+), 116 deletions(-)
delete mode 100755 misc/generate_changelog.py
create mode 100755 misc/generate_html.py
create mode 100755 misc/old_generate_changelog.py
create mode 100755 misc/streammon.py
diff --git a/misc/generate_changelog.py b/misc/generate_changelog.py
deleted file mode 100755
index 1bce073..0000000
--- a/misc/generate_changelog.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python3
-import email.utils
-import feedgenerator
-
-def parse_changelog(lines):
- got_version = False
- got_date = False
- got_url = False
- done_remarks = False
- releases = []
- release = {}
- release_remarks = []
- release_changes = []
- current_change = ""
-
- for idx, line in enumerate(lines):
- line = line.rstrip()
-
- if not got_version:
- got_version = True
- release["version"] = line
- continue
-
- if not got_date:
- release["date"] = email.utils.parsedate_to_datetime(line)
- got_date = True
- continue
-
- key, sep, val = line.partition(": ")
- if key in ["url", "sha256", "released"] and val != "":
- release[key] = val
- continue
-
- if not done_remarks:
- if line == "":
- done_remarks = True
- release["remarks"] = release_remarks
- release_remarks = []
- continue
- else:
- release_remarks.append(line)
- continue
-
- if line != "":
- release_changes.append(line.rstrip())
-
- if idx + 1 != len(lines):
- continue
-
- release["changes"] = release_changes
- if release.get("released") != "no":
- releases.append(release)
-
- got_version = False
- got_date = False
- done_remarks = False
- release = {}
- release_changes = []
-
- return releases
-
-def generate_rss_feed(releases):
- feed = feedgenerator.Rss201rev2Feed(
- title="yt-dlp-rajiko changelog",
- description="Notifications for new yt-dlp-rajiko releases, with changelogs",
- link="https://427738.xyz/yt-dlp-rajiko/",
- language="en-GB",
- ttl=180, # 3 hours
- )
-
- for release in releases:
- title = "yt-dlp-rajiko " + release["version"] + " has been released"
- description = ""
- description += "
"
- for remark in release["remarks"]:
- description += remark
- description += " "
- description += "
"
- description += "
This release:
\n"
- description += "
"
- for change in release["changes"]:
- description += "
"
- description += change
- description += "
\n"
- description += "
"
-
- if release.get("url"):
- if release["version"] != "1.0":
- description += "\n
If you use pip, you should be able to upgrade with pip install yt-dlp-rajiko --upgrade --extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/. "
- description += "If you installed manually, you can download the updated .whl from this post's link."
- if release.get("sha256"):
- description += " The SHA256 checksum should be "
- description += release.get("sha256")
- description += "."
- description += "
"
- else:
- description += '\n
Please see the homepage for initial installation instructions.
+
+
+""")
+
+latest_tarball = tarballs[-1]
+latest_wheel = wheels[-1]
+print(latest_tarball, latest_wheel)
+
+os.remove("yt_dlp_rajiko-latest.tar.gz")
+os.symlink(latest_tarball, "yt_dlp_rajiko-latest.tar.gz")
+
+os.remove("yt_dlp_rajiko-latest.whl")
+os.symlink(latest_wheel, "yt_dlp_rajiko-latest.whl")
+
+site_sha256.reverse()
+
+latest_list = site_sha256[:2]
+previous_list = site_sha256[2:]
+
+latest = "\n".join(["", "", "\n".join(latest_list), "", ""])
+
+previous = "\n".join(["", "", "\n".join(previous_list), "", ""])
+
+for i in ["../../index.html", "../../index.ja.html"]:
+ with open(i, "r+") as f:
+ page = f.read()
+
+ page = re.sub(r".+", latest, page, flags=re.DOTALL)
+ page = re.sub(r".+", previous, page, flags=re.DOTALL)
+
+ f.seek(0)
+ f.truncate(0)
+ f.write(page)
diff --git a/misc/old_generate_changelog.py b/misc/old_generate_changelog.py
new file mode 100755
index 0000000..1bce073
--- /dev/null
+++ b/misc/old_generate_changelog.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+import email.utils
+import feedgenerator
+
+def parse_changelog(lines):
+ got_version = False
+ got_date = False
+ got_url = False
+ done_remarks = False
+ releases = []
+ release = {}
+ release_remarks = []
+ release_changes = []
+ current_change = ""
+
+ for idx, line in enumerate(lines):
+ line = line.rstrip()
+
+ if not got_version:
+ got_version = True
+ release["version"] = line
+ continue
+
+ if not got_date:
+ release["date"] = email.utils.parsedate_to_datetime(line)
+ got_date = True
+ continue
+
+ key, sep, val = line.partition(": ")
+ if key in ["url", "sha256", "released"] and val != "":
+ release[key] = val
+ continue
+
+ if not done_remarks:
+ if line == "":
+ done_remarks = True
+ release["remarks"] = release_remarks
+ release_remarks = []
+ continue
+ else:
+ release_remarks.append(line)
+ continue
+
+ if line != "":
+ release_changes.append(line.rstrip())
+
+ if idx + 1 != len(lines):
+ continue
+
+ release["changes"] = release_changes
+ if release.get("released") != "no":
+ releases.append(release)
+
+ got_version = False
+ got_date = False
+ done_remarks = False
+ release = {}
+ release_changes = []
+
+ return releases
+
+def generate_rss_feed(releases):
+ feed = feedgenerator.Rss201rev2Feed(
+ title="yt-dlp-rajiko changelog",
+ description="Notifications for new yt-dlp-rajiko releases, with changelogs",
+ link="https://427738.xyz/yt-dlp-rajiko/",
+ language="en-GB",
+ ttl=180, # 3 hours
+ )
+
+ for release in releases:
+ title = "yt-dlp-rajiko " + release["version"] + " has been released"
+ description = ""
+ description += "
"
+ for remark in release["remarks"]:
+ description += remark
+ description += " "
+ description += "
"
+ description += "
This release:
\n"
+ description += "
"
+ for change in release["changes"]:
+ description += "
"
+ description += change
+ description += "
\n"
+ description += "
"
+
+ if release.get("url"):
+ if release["version"] != "1.0":
+ description += "\n
If you use pip, you should be able to upgrade with pip install yt-dlp-rajiko --upgrade --extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/. "
+ description += "If you installed manually, you can download the updated .whl from this post's link."
+ if release.get("sha256"):
+ description += " The SHA256 checksum should be "
+ description += release.get("sha256")
+ description += "."
+ description += "
"
+ else:
+ description += '\n
Please see the homepage for initial installation instructions.
'
+
+ feed.add_item(
+ title=title,
+ description=description,
+ link=release.get("url"),
+ pubdate=release["date"]
+ )
+ return feed
+
+if __name__ == "__main__":
+ with open("CHANGELOG") as f:
+ releases = parse_changelog(f.readlines())
+
+ feed = generate_rss_feed(releases)
+ feed_contents = feed.writeString("utf-8")
+ feed_contents = feed_contents.replace("\n 1:
+ PATH = sys.argv[1]
+else:
+ PATH = ""
+
+devices = ('pc_html5', 'aSmartPhone7a', 'aSmartPhone8')
+stations = ('FMT', 'CCL', 'NORTHWAVE', 'TBS')
+
+for device in devices:
+ for station in stations:
+ url = STREAMS_API.format(device=device, station=station)
+ now = s.get(url).text
+
+ filename = f"{PATH}{station}-{device}.xml"
+ with open(filename, "a+") as f:
+ f.seek(0)
+ past = f.read()
+
+ modtime = datetime.fromtimestamp(os.path.getmtime(filename))
+ diff = difflib.unified_diff(
+ past.splitlines(), now.splitlines(),
+ fromfile=url, tofile=url,
+ fromfiledate=str(modtime), tofiledate=str(datetime.now()),
+ )
+
+ diff_str = "\n".join(diff)
+ if diff_str != "":
+ f.truncate(0)
+ f.write(now)
+
+ s.post(DISCORD_WEBHOOK, json={
+ "embeds": [{
+ "type": "rich",
+ "title": f"Streams changed: {station} {device}",
+ "description": "\n".join(("```diff", diff_str, "```"))
+ }]
+ })
--
cgit v1.2.3-70-g09d2
From b2a004a66e697bbb24af60ebc8221986fa519c19 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 6 Jun 2025 02:22:30 +0100
Subject: fix search only returning one result
rather embarrassing bug introduced in a659b48a20b6241a68fc046c86c9ad0f71c9bd86 / v1.5
only caught it now because i only listen to weekly programmes so i only get 1 result anyway
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index f58e744..ca9ab6f 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -637,7 +637,7 @@ class RadikoSearchIE(InfoExtractor):
id=join_nonempty(station, timestring)
)
)
- return results
+ return results
def _real_extract(self, url):
url = url.replace("/#!/", "/!/", 1)
--
cgit v1.2.3-70-g09d2
From e30e9ae869aa2cc837c4dd6ce10581a10b33a056 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 9 Jun 2025 04:28:36 +0100
Subject: add more stuff to pyproject.toml
---
pyproject.toml | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 4d57eb8..bea1e8a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,15 @@
[project]
name = "yt-dlp-rajiko"
version = "1.6"
-description = "improved radiko.jp extractor for yt-dlp"
+description = "improved radiko.jp extractor for yt-dlp (fast and areafree)"
+
+readme = "README.md"
+license-files = ["LICENCE"]
+
authors = [
{ name="garret1317" },
]
+
requires-python = ">=3.8"
classifiers = [
"License :: OSI Approved :: Zero-Clause BSD (0BSD)",
@@ -13,6 +18,8 @@ classifiers = [
[project.urls]
Homepage = "https://427738.xyz/yt-dlp-rajiko/"
+"Source Code" = "https://github.com/garret1317/yt-dlp-rajiko/"
+"Release Notes" = "https://427738.xyz/yt-dlp-rajiko/CHANGELOG.xml"
[build-system]
requires = ["setuptools>=61.0"]
--
cgit v1.2.3-70-g09d2
From f70e21a523074455a7ca0193e5c722bfe579aae6 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 9 Jun 2025 05:48:17 +0100
Subject: release 1.7
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index bea1e8a..2a13f3f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.6"
+version = "1.7"
description = "improved radiko.jp extractor for yt-dlp (fast and areafree)"
readme = "README.md"
--
cgit v1.2.3-70-g09d2
From 7c5754a50966fe2a374353b5908e667142985788 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 9 Jun 2025 06:20:50 +0100
Subject: remove command from brackets to make it easier to copy
---
misc/how to do a release | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/misc/how to do a release b/misc/how to do a release
index 785533e..82ea9ec 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -18,7 +18,9 @@ write in html, paste into the feed xml like
make sure to set the link, date
-(git log --pretty --date=rfc2822)
+to get date use:
+git log --pretty --date=rfc2822
+
include the pip instructions, sha256sum etc
--
cgit v1.2.3-70-g09d2
From 957b10b831015d55f376a34707b851f533146c03 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 9 Jun 2025 07:29:22 +0100
Subject: Extract station logo as thumbnail for live now that banner is gone
well not technically _gone_, but replaced with a useless placeholder image
---
yt_dlp_plugins/extractor/radiko.py | 39 ++++++++++++++++++++------------------
1 file changed, 21 insertions(+), 18 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index ca9ab6f..c87b1b7 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -216,6 +216,17 @@ class _RadikoBaseIE(InfoExtractor):
station = region.find(f'.//station/id[.="{station_id}"]/..') # a with an of our station_id
station_name = station.find("name").text
station_url = url_or_none(station.find("href").text)
+
+ thumbnails = []
+ for logo in station.findall("logo"):
+ thumbnails.append({
+ "url": logo.text,
+ **traverse_obj(logo.attrib, ({
+ "width": ("width", {int_or_none}),
+ "height": ("height", {int_or_none}),
+ }))
+ })
+
meta = {
"id": station_id,
"title": station_name,
@@ -229,7 +240,7 @@ class _RadikoBaseIE(InfoExtractor):
"uploader_id": station_id,
"uploader_url": station_url,
- "thumbnail": url_or_none(station.find("banner").text),
+ "thumbnails": thumbnails,
}
self.cache.store("rajiko", station_id, {
"expiry": (now + datetime.timedelta(days=1)).timestamp(),
@@ -340,7 +351,7 @@ class RadikoLiveIE(_RadikoBaseIE):
"id": "FMT",
"title": "re:^TOKYO FM.+$",
"alt_title": "TOKYO FM",
- "thumbnail": "https://radiko.jp/res/banner/FMT/20220512162447.jpg",
+ "thumbnail": "https://radiko.jp/v2/static/station/logo/FMT/lrtrim/688x160.png",
"channel": "TOKYO FM",
"channel_id": "FMT",
@@ -360,7 +371,7 @@ class RadikoLiveIE(_RadikoBaseIE):
"id": "NORTHWAVE",
"title": "re:^FM NORTH WAVE.+$",
"alt_title": "FM NORTH WAVE",
- "thumbnail": "https://radiko.jp/res/banner/NORTHWAVE/20150731161543.png",
+ "thumbnail": "https://radiko.jp/v2/static/station/logo/NORTHWAVE/lrtrim/688x160.png",
"uploader": "FM NORTH WAVE",
"uploader_url": "https://www.fmnorth.co.jp/",
@@ -381,7 +392,7 @@ class RadikoLiveIE(_RadikoBaseIE):
"id": "RN1",
"title": "re:^ラジオNIKKEI第1.+$",
"alt_title": "RADIONIKKEI",
- "thumbnail": "https://radiko.jp/res/banner/RN1/20120802154152.png",
+ "thumbnail": "https://radiko.jp/v2/static/station/logo/RN1/lrtrim/688x160.png",
"channel": "ラジオNIKKEI第1",
"channel_url": "http://www.radionikkei.jp/",
@@ -581,7 +592,9 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
return {
**station_meta,
- "alt_title": None,
+ "alt_title": None, # override from station metadata
+ "thumbnails": None,
+
**meta,
"chapters": chapters,
"formats": formats,
@@ -727,19 +740,9 @@ class RadikoStationButtonIE(InfoExtractor):
"info_dict": {
"ext": "m4a",
'live_status': 'is_live',
-
"id": "QRR",
- "title": "re:^文化放送.+$",
- 'alt_title': 'JOQR BUNKA HOSO',
- 'thumbnail': 'https://radiko.jp/res/banner/QRR/20240423144553.png',
- 'channel': '文化放送',
- 'channel_id': 'QRR',
- 'channel_url': 'http://www.joqr.co.jp/',
- 'uploader': '文化放送',
- 'uploader_id': 'QRR',
- 'uploader_url': 'http://www.joqr.co.jp/',
-
- }
+ },
+ 'only_matching': True,
}]
_WEBPAGE_TESTS = [{
@@ -750,7 +753,7 @@ class RadikoStationButtonIE(InfoExtractor):
'id': 'CCL',
"title": "re:^FM COCOLO.+$",
'alt_title': 'FM COCOLO',
- 'thumbnail': 'https://radiko.jp/res/banner/CCL/20161014144826.png',
+ 'thumbnail': 'https://radiko.jp/v2/static/station/logo/CCL/lrtrim/688x160.png',
'channel': 'FM COCOLO',
'channel_id': 'CCL',
--
cgit v1.2.3-70-g09d2
From a77e8c29ed85378768b97f7fc61d1b1fb21aedaa Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 9 Jun 2025 07:51:32 +0100
Subject: set count -> mincount for persons key station test
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index c87b1b7..5b79fc6 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -782,7 +782,7 @@ class RadikoPersonIE(InfoExtractor):
},{
"url": "https://radiko.jp/persons/11421",
"params": {'extractor_args': {'rajiko': {'key_station_only': ['']}}},
- "playlist_count": 1,
+ "playlist_mincount": 1,
"info_dict": {
"id": "person-11421",
},
--
cgit v1.2.3-70-g09d2
From c50b59e9b7077a3bb6a12c15c754f120e8abaa37 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 16 Jun 2025 03:46:57 +0100
Subject: update instructions to upload to real pip as well
---
misc/how to do a release | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/misc/how to do a release b/misc/how to do a release
index 82ea9ec..6e91e14 100644
--- a/misc/how to do a release
+++ b/misc/how to do a release
@@ -23,9 +23,14 @@ git log --pretty --date=rfc2822
include the pip instructions, sha256sum etc
-
now push to the server
+!!NEW!!
+upload to pip proper as well
+go to dl/ dir and do
+twine upload yt_dlp_rajiko-1.x*
+
+
## update github
paste the changelog output into a github release, upload the new builds
--
cgit v1.2.3-70-g09d2
From 3a954526c532c3c36c804e9d525c65d69e2e4696 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 20 Jun 2025 21:00:21 +0100
Subject: add new shiny custom-ish test script
see comment for details
---
misc/test_extractors.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 113 insertions(+)
create mode 100755 misc/test_extractors.py
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
new file mode 100755
index 0000000..7cc681d
--- /dev/null
+++ b/misc/test_extractors.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+
+# programmes expire, so i have to update the times in the tests every time i run them
+# but thats a massive ballache, so i end up just not running them, which leads to cockups
+# so, this script has the tests automatically use the latest episode as you run it, by setting dynamically generated time values
+# everything else is always the same so it should be fine lol
+
+
+import datetime
+import os
+import sys
+import unittest
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.insert(0, "/home/g/Downloads/yt-dlp/") # TODO: un-hardcode. has to be the source/git repo because pip doesnt carry the tests
+
+from yt_dlp_plugins.extractor import radiko_time as rtime
+from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE
+
+
+MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
+weekdays = {0: "MON", 1: "TUE", 2: "WED", 3: "THU", 4: "FRI", 5: "SAT", 6: "SUN"}
+
+now = rtime.RadikoTime.now(tz = rtime.JST)
+UTC = datetime.timezone.utc
+
+def get_latest_airtimes(now, weekday, hour, minute, duration):
+ days_after_weekday = (7 - (now.weekday() - weekday)) % 7
+ latest_airdate = (now + datetime.timedelta(days=days_after_weekday)).replace(hour=hour, minute=minute, second=0, microsecond=0)
+ if (latest_airdate + duration) > now:
+ latest_airdate -= datetime.timedelta(days=7)
+ return latest_airdate, latest_airdate + duration
+
+def get_test_timefields(airtime, release_time):
+ return {
+ "timestamp": airtime.timestamp(),
+ "release_timestamp": release_time.timestamp(),
+ "upload_date": airtime.astimezone(UTC).strftime("%Y%m%d"),
+ "release_date": release_time.astimezone(UTC).strftime("%Y%m%d"),
+
+ "duration": (release_time - airtime).total_seconds(),
+ }
+
+
+
+
+
+RadikoTimeFreeIE._TESTS = []
+
+# TOKYO MOON - interfm - EVERY FRI 2300
+airtime, release_time = get_latest_airtimes(now, FRI, 23, 0, datetime.timedelta(hours=1))
+RadikoTimeFreeIE._TESTS.append({
+ "url": f"https://radiko.jp/#!/ts/INT/{airtime.timestring()}",
+ "info_dict": {
+ "ext": "m4a",
+ "id": f"INT-{airtime.timestring()}",
+
+ **get_test_timefields(airtime, release_time),
+
+ 'title': 'TOKYO MOON',
+ 'description': 'md5:4185349a530cfc4d0580e6996a511273',
+ 'uploader': 'interfm',
+ 'uploader_id': 'INT',
+ 'uploader_url': 'https://www.interfm.co.jp/',
+ 'channel': 'interfm',
+ 'channel_id': 'INT',
+ 'channel_url': 'https://www.interfm.co.jp/',
+ 'thumbnail': 'https://program-static.cf.radiko.jp/ehwtw6mcvy.jpg',
+ 'chapters': list,
+ 'tags': ['松浦俊夫', 'ジャズの魅力を楽しめる'],
+ 'cast': ['松浦\u3000俊夫'],
+ 'series': 'Tokyo Moon',
+ 'live_status': 'was_live',
+ }
+})
+
+
+
+
+
+
+IEs = [RadikoTimeFreeIE]
+
+import test.helper as th
+
+# override to only get testcases from our IEs
+
+def _new_gettestcases(include_onlymatching=False):
+ import yt_dlp.plugins as plugins
+ plugins.load_all_plugins()
+
+ for ie in IEs:
+ yield from ie.get_testcases(include_onlymatching)
+
+def _new_getwebpagetestcases():
+ import yt_dlp.plugins as plugins
+ plugins.load_all_plugins()
+
+ for ie in IEs:
+ for tc in ie.get_webpage_testcases():
+ tc.setdefault('add_ie', []).append('Generic')
+ yield tc
+
+th.gettestcases = _new_gettestcases
+th.getwebpagetestcases = _new_getwebpagetestcases
+
+import test.test_download as td
+
+class TestDownload(td.TestDownload):
+ pass
+
+if __name__ == "__main__":
+ unittest.main()
--
cgit v1.2.3-70-g09d2
From 73006bef2d0718607fb2926ce1ebd51281e2e7d5 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 22 Jun 2025 00:32:24 +0100
Subject: update readme
---
README.md | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index e93f475..1546867 100644
--- a/README.md
+++ b/README.md
@@ -2,24 +2,21 @@
yt-dlp-rajiko is an improved [radiko.jp](https://radiko.jp) extractor plugin for yt-dlp.
-[日本語訳はこちら](https://427738.xyz/yt-dlp-rajiko/index.ja.html)
-
## Installation
-[Download the Python wheel](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.whl) or `pip install
---extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/ yt-dlp-rajiko`
+[Download the Python wheel](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.whl) or `pip install yt-dlp-rajiko`
Requires yt-dlp 2025.02.19 or above.
Use the pip command if you installed yt-dlp with pip. If you installed
-yt-dlp with `pipx`, use `pipx inject --index-url
-https://427738.xyz/yt-dlp-rajiko/pip/ yt-dlp yt-dlp-rajiko` to install
+yt-dlp with `pipx`, use `pipx inject yt-dlp yt-dlp-rajiko` to install
the plugin in yt-dlp's environment.
Otherwise, download the wheel, and place it in one of these locations:
- `~/.config/yt-dlp/plugins/` (on Linux and Mac)
- `%appdata%/yt-dlp/plugins/` (on Windows)
+ - a `yt-dlp-plugins` folder next to your `yt-dlp.exe` (like this)
- anywhere else listed in [the yt-dlp
documentation](https://github.com/yt-dlp/yt-dlp#installing-plugins).
@@ -66,6 +63,13 @@ As a general rule, just copying from the browser URL bar should work with no cha
**[You can find more usage tips on the website](https://427738.xyz/yt-dlp-rajiko/)**
+## How do I get help?
+
+[Open an issue on github](https://github.com/garret1317/yt-dlp-rajiko/issues) or [send a message via the "hate mail" form](https://427738.xyz/hate-mail.html).
+You can also ping me [in the yt-dlp discord server](https://discord.gg/H5MNcFW63r), username `garret1317`.
+
+Please try to include a verbose log if you're reporting a problem, it helps with diagnosing the issue.
+
## notes about this repository
this is just where the source code and bug tracker live. most of the info is on the website.
--
cgit v1.2.3-70-g09d2
From dc4500705cd4d0599a95ba954cc980bfedfe2e53 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 22 Jun 2025 00:38:07 +0100
Subject: rename test-tokens script to test_areas
underscore so tab completion is less pain
---
misc/test-tokens.py | 26 --------------------------
misc/test_areas.py | 26 ++++++++++++++++++++++++++
2 files changed, 26 insertions(+), 26 deletions(-)
delete mode 100755 misc/test-tokens.py
create mode 100755 misc/test_areas.py
diff --git a/misc/test-tokens.py b/misc/test-tokens.py
deleted file mode 100755
index ba6475f..0000000
--- a/misc/test-tokens.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-
-from yt_dlp_plugins.extractor import radiko
-from yt_dlp import YoutubeDL
-
-
-class test_tokens(unittest.TestCase):
-
- def setUp(self):
- self.ie = radiko._RadikoBaseIE()
- ydl = YoutubeDL(auto_init=False)
- self.ie.set_downloader(ydl)
-
- def test_area(self):
- # check areas etc work
- for i in range(1, 48):
- area = "JP" + str(i)
- with self.subTest(f"Negotiating token for {area}", area=area):
- token = self.ie._negotiate_token(area)
- self.assertEqual(token.get("X-Radiko-AreaId"), area)
-
-
-if __name__ == '__main__':
- unittest.main()
- # may wish to set failfast=True
diff --git a/misc/test_areas.py b/misc/test_areas.py
new file mode 100755
index 0000000..ba6475f
--- /dev/null
+++ b/misc/test_areas.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import unittest
+
+from yt_dlp_plugins.extractor import radiko
+from yt_dlp import YoutubeDL
+
+
+class test_tokens(unittest.TestCase):
+
+ def setUp(self):
+ self.ie = radiko._RadikoBaseIE()
+ ydl = YoutubeDL(auto_init=False)
+ self.ie.set_downloader(ydl)
+
+ def test_area(self):
+ # check areas etc work
+ for i in range(1, 48):
+ area = "JP" + str(i)
+ with self.subTest(f"Negotiating token for {area}", area=area):
+ token = self.ie._negotiate_token(area)
+ self.assertEqual(token.get("X-Radiko-AreaId"), area)
+
+
+if __name__ == '__main__':
+ unittest.main()
+ # may wish to set failfast=True
--
cgit v1.2.3-70-g09d2
From ea171efac9b5d4a97cd59d316996c9559b264f79 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 22 Jun 2025 01:03:12 +0100
Subject: test script: move import
---
misc/test_extractors.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 7cc681d..4ae5313 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -15,8 +15,6 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, "/home/g/Downloads/yt-dlp/") # TODO: un-hardcode. has to be the source/git repo because pip doesnt carry the tests
from yt_dlp_plugins.extractor import radiko_time as rtime
-from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE
-
MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
weekdays = {0: "MON", 1: "TUE", 2: "WED", 3: "THU", 4: "FRI", 5: "SAT", 6: "SUN"}
@@ -44,9 +42,12 @@ def get_test_timefields(airtime, release_time):
+from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE
RadikoTimeFreeIE._TESTS = []
+
+
# TOKYO MOON - interfm - EVERY FRI 2300
airtime, release_time = get_latest_airtimes(now, FRI, 23, 0, datetime.timedelta(hours=1))
RadikoTimeFreeIE._TESTS.append({
--
cgit v1.2.3-70-g09d2
From 12aced7d2d34d3e2ba1b2929aa421adcaabef434 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 22 Jun 2025 01:03:45 +0100
Subject: Move RadikoTimeFreeIE tests to test script
---
misc/test_extractors.py | 25 ++++++++++++++++
yt_dlp_plugins/extractor/radiko.py | 61 +-------------------------------------
2 files changed, 26 insertions(+), 60 deletions(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 4ae5313..41dedb4 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -76,7 +76,32 @@ RadikoTimeFreeIE._TESTS.append({
})
+# late-night/v. early morning show, to test broadcast day handling
+# this should be monday 27:00 / tuesday 03:00
+airtime, release_time = get_latest_airtimes(now, TUE, 3, 0, datetime.timedelta(hours=2))
+RadikoTimeFreeIE._TESTS.append({
+ "url": f"https://radiko.jp/#!/ts/TBS/{airtime.timestring()}",
+ "info_dict": {
+ "ext": "m4a",
+ "id": f"TBS-{airtime.timestring()}",
+ **get_test_timefields(airtime, release_time),
+ 'title': 'CITY CHILL CLUB',
+ 'description': r"re:^目を閉じて…リラックスして[\S\s]+chill@tbs.co.jp$",
+ 'uploader': 'TBSラジオ',
+ 'uploader_id': 'TBS',
+ 'uploader_url': 'https://www.tbsradio.jp/',
+ 'channel': 'TBSラジオ',
+ 'channel_id': 'TBS',
+ 'channel_url': 'https://www.tbsradio.jp/',
+ 'thumbnail': 'https://program-static.cf.radiko.jp/nrf8fowbjo.jpg',
+ 'chapters': list,
+ 'tags': ['CCC905', '音楽との出会いが楽しめる', '人気アーティストトーク', '音楽プロデューサー出演', 'ドライブ中におすすめ', '寝る前におすすめ', '学生におすすめ'],
+ 'cast': list,
+ 'series': 'CITY CHILL CLUB',
+ 'live_status': 'was_live',
+ },
+})
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 5b79fc6..c4e4846 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -422,66 +422,7 @@ class RadikoLiveIE(_RadikoBaseIE):
class RadikoTimeFreeIE(_RadikoBaseIE):
_NETRC_MACHINE = "rajiko"
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/ts/(?P[A-Z0-9-_]+)/(?P\d+)"
- _TESTS = [{
- "url": "https://radiko.jp/#!/ts/INT/20240809230000",
- "info_dict": {
- "live_status": "was_live",
- "ext": "m4a",
- "id": "INT-20240809230000",
-
- "title": "TOKYO MOON",
- "series": "Tokyo Moon",
- "description": "md5:20e68d2f400a391fa34d4e7c8c702cb8",
- "chapters": "count:14",
- "thumbnail": "https://program-static.cf.radiko.jp/ehwtw6mcvy.jpg",
-
- "upload_date": "20240809",
- "timestamp": 1723212000.0,
- "release_date": "20240809",
- "release_timestamp": 1723215600.0,
- "duration": 3600,
-
- "channel": "interfm",
- "channel_id": "INT",
- "channel_url": "https://www.interfm.co.jp/",
- "uploader": "interfm",
- "uploader_id": "INT",
- "uploader_url": "https://www.interfm.co.jp/",
-
- "cast": ["松浦\u3000俊夫"],
- "tags": ["松浦俊夫"],
- },
- }, {
- # late-night/early-morning show to test broadcast day checking
- "url": "https://radiko.jp/#!/ts/TBS/20240810033000",
- "info_dict": {
- "live_status": "was_live",
- "ext": "m4a",
- "id": "TBS-20240810033000",
-
- "title": "CITY CHILL CLUB",
- "series": "CITY CHILL CLUB",
- "description": "md5:3fba2c1125059bed27247c0be90e58fa",
- "chapters": "count:22",
- "thumbnail": "https://program-static.cf.radiko.jp/ku7t4ztnaq.jpg",
-
- "upload_date": "20240809",
- "timestamp": 1723228200.0,
- "release_date": "20240809",
- "release_timestamp": 1723233600.0,
- "duration": 5400,
-
- "channel": "TBSラジオ",
- "channel_url": "https://www.tbsradio.jp/",
- "channel_id": "TBS",
- "uploader": "TBSラジオ",
- "uploader_url": "https://www.tbsradio.jp/",
- "uploader_id": "TBS",
-
- "tags": ["CCC905", "音楽との出会いが楽しめる", "人気アーティストトーク", "音楽プロデューサー出演", "ドライブ中におすすめ", "寝る前におすすめ", "学生におすすめ"],
- "cast": ["PES"],
- },
- }]
+ # TESTS use a custom-ish script that updates the airdates automatically, see misc/test_extractors.py
def _perform_login(self, username, password):
try:
--
cgit v1.2.3-70-g09d2
From 781ce8e7a74aa19df8793a3f29176d82c44f6b6b Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 22 Jun 2025 01:25:24 +0100
Subject: move ShareIE tests to test script
---
misc/test_extractors.py | 36 ++++++++++++++++++++++++++++++++++--
yt_dlp_plugins/extractor/radiko.py | 31 -------------------------------
2 files changed, 34 insertions(+), 33 deletions(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 41dedb4..7c1a2c7 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -42,7 +42,7 @@ def get_test_timefields(airtime, release_time):
-from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE
+from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE, RadikoShareIE
RadikoTimeFreeIE._TESTS = []
@@ -104,8 +104,40 @@ RadikoTimeFreeIE._TESTS.append({
})
+# testing 29-hour clock handling
+airtime, release_time = get_latest_airtimes(now, WED, 0, 0, datetime.timedelta(minutes=55))
+share_timestring = (airtime - datetime.timedelta(days=1)).strftime("%Y%m%d") + "240000"
-IEs = [RadikoTimeFreeIE]
+RadikoShareIE._TESTS = [{
+ "url": f"http://radiko.jp/share/?sid=FMT&t={share_timestring}",
+ "info_dict": {
+ "live_status": "was_live",
+ "ext": "m4a",
+ "id": f"FMT-{airtime.timestring()}",
+
+ **get_test_timefields(airtime, release_time),
+
+ "title": "JET STREAM",
+ "series": "JET STREAM",
+ "description": r"re:^JET STREAM・・・作家が描く世界への旅。[\s\S]+https://www.tfm.co.jp/f/jetstream/message$",
+ "chapters": list,
+ "thumbnail": "https://program-static.cf.radiko.jp/greinlrspi.jpg",
+
+ "channel": "TOKYO FM",
+ "channel_id": "FMT",
+ "channel_url": "https://www.tfm.co.jp/",
+ "uploader": "TOKYO FM",
+ "uploader_id": "FMT",
+ "uploader_url": "https://www.tfm.co.jp/",
+
+ "cast": ["福山雅治"],
+ "tags": ["福山雅治", "夜間飛行", "音楽との出会いが楽しめる", "朗読を楽しめる", "寝る前に聴きたい"],
+ },
+ }]
+
+
+
+IEs = [RadikoTimeFreeIE, RadikoShareIE]
import test.helper as th
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index c4e4846..2996290 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -628,37 +628,6 @@ class RadikoSearchIE(InfoExtractor):
class RadikoShareIE(InfoExtractor):
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/share/"
- _TESTS = [{
- # 29-hour time -> 24-hour time
- "url": "http://radiko.jp/share/?sid=FMT&t=20240802240000",
- "info_dict": {
- "live_status": "was_live",
- "ext": "m4a",
- "id": "FMT-20240803000000", # the time given (24:00) works out to 00:00 the next day
-
- "title": "JET STREAM",
- "series": "JET STREAM",
- "description": "md5:c1a2172036ebb7a54eeafb47e0a08a50",
- "chapters": "count:9",
- "thumbnail": "https://program-static.cf.radiko.jp/greinlrspi.jpg",
-
- "upload_date": "20240802",
- "timestamp": 1722610800.0,
- "release_date": "20240802",
- "release_timestamp": 1722614100.0,
- "duration": 3300,
-
- "channel": "TOKYO FM",
- "channel_id": "FMT",
- "channel_url": "https://www.tfm.co.jp/",
- "uploader": "TOKYO FM",
- "uploader_id": "FMT",
- "uploader_url": "https://www.tfm.co.jp/",
-
- "cast": ["福山雅治"],
- "tags": ["福山雅治", "夜間飛行", "音楽との出会いが楽しめる", "朗読を楽しめる", "寝る前に聴きたい"],
- }
- }]
def _real_extract(self, url):
queries = parse_qs(url)
--
cgit v1.2.3-70-g09d2
From ab56798ce8f62695eddaaee38bd99fbc9a2f2663 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 23 Jun 2025 15:48:45 +0100
Subject: run all extractor tests from test script
may as well run them all from one place even if theyre not defined in one place
+ this way i don't have to patch test.helper separately to get it to load the plugin
---
misc/test_extractors.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 7c1a2c7..18e9783 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -42,7 +42,7 @@ def get_test_timefields(airtime, release_time):
-from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE, RadikoShareIE
+from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE, RadikoShareIE, RadikoLiveIE, RadikoShareIE, RadikoPersonIE, RadikoStationButtonIE
RadikoTimeFreeIE._TESTS = []
@@ -137,7 +137,10 @@ RadikoShareIE._TESTS = [{
-IEs = [RadikoTimeFreeIE, RadikoShareIE]
+IEs = [
+ RadikoTimeFreeIE, RadikoShareIE,
+ RadikoLiveIE, RadikoShareIE, RadikoPersonIE, RadikoStationButtonIE
+]
import test.helper as th
--
cgit v1.2.3-70-g09d2
From 8930714a6288827d7ae5a2e0c7a1eeb07a1d4c00 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 10 Jul 2025 02:44:12 +0100
Subject: Update Tokyo Moon test
---
misc/test_extractors.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 18e9783..72e0181 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -59,7 +59,7 @@ RadikoTimeFreeIE._TESTS.append({
**get_test_timefields(airtime, release_time),
'title': 'TOKYO MOON',
- 'description': 'md5:4185349a530cfc4d0580e6996a511273',
+ 'description': r're:[\S\s]+Xハッシュタグは「#tokyomoon」$',
'uploader': 'interfm',
'uploader_id': 'INT',
'uploader_url': 'https://www.interfm.co.jp/',
--
cgit v1.2.3-70-g09d2
From 434e81e4da3b95d1a0cec115dbde1ab76069880f Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 10 Jul 2025 02:44:43 +0100
Subject: add automated tests with github actions
---
.github/workflows/download.yml | 46 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
create mode 100644 .github/workflows/download.yml
diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml
new file mode 100644
index 0000000..66274b3
--- /dev/null
+++ b/.github/workflows/download.yml
@@ -0,0 +1,46 @@
+name: Tests
+on: [push, pull_request]
+permissions:
+ contents: read
+
+jobs:
+ full:
+ name: Tests
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.10]
+ include:
+ # atleast one of each CPython/PyPy tests must be in windows
+ - os: windows-latest
+ python-version: '3.9'
+ - os: windows-latest
+ python-version: pypy-3.10
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ path: './yt-dlp-plugins/yt-dlp-rajiko/'
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: install ffmpeg
+ uses: AnimMouse/setup-ffmpeg@v1
+
+ - name: get yt-dlp source (for the test_download script we override)
+ uses: actions/checkout@v4
+ with:
+ path: './yt-dlp/'
+ repository: yt-dlp/yt-dlp
+
+ - name: Install yt-dlp from source (editable mode)
+ run: pip install -e ./yt-dlp/
+
+ - name: Run tests
+ env:
+ PYTHONPATH: ${{ github.workspace }}/yt-dlp${{ runner.os == 'Windows' && ';' || ':' }}${{ env.PYTHONPATH }}
+ run: python ./yt-dlp-plugins/yt-dlp-rajiko/misc/test_extractors.py
--
cgit v1.2.3-70-g09d2
From 794ec2b5256a8219a0e50f3e14165f0914189db4 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 10 Jul 2025 22:12:28 +0100
Subject: Add basic radiko podcast extractors
---
yt_dlp_plugins/extractor/radiko_podcast.py | 114 +++++++++++++++++++++++++++++
1 file changed, 114 insertions(+)
create mode 100644 yt_dlp_plugins/extractor/radiko_podcast.py
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
new file mode 100644
index 0000000..93e1408
--- /dev/null
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -0,0 +1,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[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[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(),
+ }
--
cgit v1.2.3-70-g09d2
From 968dfa48d277273ca0ae7be47872ec232f864b2d Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 10 Jul 2025 22:13:13 +0100
Subject: add podcast extractors to test script
also removed duplicate ShareIE
---
misc/test_extractors.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 72e0181..9289498 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -42,8 +42,14 @@ def get_test_timefields(airtime, release_time):
-from yt_dlp_plugins.extractor.radiko import RadikoTimeFreeIE, RadikoShareIE, RadikoLiveIE, RadikoShareIE, RadikoPersonIE, RadikoStationButtonIE
+from yt_dlp_plugins.extractor.radiko import (
+ RadikoTimeFreeIE, RadikoShareIE,
+ RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE
+)
+from yt_dlp_plugins.extractor.radiko_podcast import (
+ RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
+)
RadikoTimeFreeIE._TESTS = []
@@ -139,7 +145,8 @@ RadikoShareIE._TESTS = [{
IEs = [
RadikoTimeFreeIE, RadikoShareIE,
- RadikoLiveIE, RadikoShareIE, RadikoPersonIE, RadikoStationButtonIE
+ RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
+ RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
]
import test.helper as th
--
cgit v1.2.3-70-g09d2
From fad5614bf07dae50be5850f28e845af7893967dc Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 10 Jul 2025 23:13:39 +0100
Subject: fix channel/uploader field in podcast extractor
---
yt_dlp_plugins/extractor/radiko_podcast.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index 93e1408..af66f6a 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -25,8 +25,8 @@ class _RadikoPodcastBaseIE(InfoExtractor):
"series": "channelTitle",
"series_id": "channelId",
- "channel": "stationName",
- "uploader": "stationName",
+ "channel": "channelStationName",
+ "uploader": "channelStationName",
}),
"thumbnail": traverse_obj(episode_info, ("imageUrl", {url_or_none}))
or traverse_obj(episode_info, ("channelImageUrl", {url_or_none})),
@@ -54,6 +54,8 @@ class RadikoPodcastEpisodeIE(_RadikoPodcastBaseIE):
'series_id': '09f27a48-ae04-4ce7-a024-572460e46eb7',
'timestamp': 1751554800,
'upload_date': '20250703',
+ 'uploader': 'IBCラジオ',
+ 'channel': 'IBCラジオ',
},
}]
--
cgit v1.2.3-70-g09d2
From 489e31032bb62a690a4d3b8f029259a3e7e051f9 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 11 Jul 2025 07:41:06 +0100
Subject: add Podcast protobuf test/proof-of-concept
not going to add to the extractor just yet because its really complicated
---
misc/protostuff.py | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 146 insertions(+)
create mode 100755 misc/protostuff.py
diff --git a/misc/protostuff.py b/misc/protostuff.py
new file mode 100755
index 0000000..a461c02
--- /dev/null
+++ b/misc/protostuff.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+
+import protobug
+import base64
+import struct
+
+import random
+import requests
+
+@protobug.message
+class SignInRequest:
+ lsid: protobug.String = protobug.field(2)
+ area: protobug.String = protobug.field(3)
+
+@protobug.message
+class SignInResponse:
+ jwt: protobug.String = protobug.field(1)
+
+
+
+@protobug.message
+class ListPodcastEpisodesRequest:
+ channel_id: protobug.String = protobug.field(1)
+ dontknow: protobug.Int32 = protobug.field(2)
+ page_length: protobug.Int32 = protobug.field(4)
+ cursor: protobug.String = protobug.field(5, default=None)
+
+def add_grpc_header(protobuf_data):
+ compression_flag = 0
+ message_length = len(protobuf_data)
+ header = struct.pack('>BI', compression_flag, message_length)
+ return header + protobuf_data
+
+def strip_grpc_response(response):
+ return response[5:].rpartition(b"grpc-status:")[0]
+
+print("SIGNUP")
+# why do they have to make it so bloody complicated
+
+lsid = ''.join(random.choices('0123456789abcdef', k=32))
+big_funny = ("\n " + lsid).encode()
+
+signup = requests.post("https://api.annex.radiko.jp/radiko.UserService/SignUp", headers={
+ 'Origin': 'https://radiko.jp',
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+ }, data=( add_grpc_header(big_funny)),
+)
+
+print(signup.content)
+
+# youre meant to only do the sign up ^ once and then keep your id for later
+# so that you can V sign in and get the token for the API to work
+
+print("SIGNIN")
+
+si=add_grpc_header(protobug.dumps(SignInRequest(
+ lsid=lsid,
+ area="JP13",
+)))
+
+print(si)
+print(base64.b64encode(si))
+
+signin = requests.post("https://api.annex.radiko.jp/radiko.UserService/SignIn", headers={
+ 'Origin': 'https://radiko.jp',
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+}, data=si)
+
+print(signin.content)
+
+signin_result = protobug.loads(strip_grpc_response(signin.content), SignInResponse)
+
+
+headers = {
+ 'Origin': 'https://radiko.jp',
+ 'Authorization': f'Bearer {signin_result.jwt}',
+ 'x-annex-proto-version': '1.0.0',
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+}
+
+response = requests.post('https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes', headers=headers,
+ data=add_grpc_header(protobug.dumps(ListPodcastEpisodesRequest(
+ channel_id="09f27a48-ae04-4ce7-a024-572460e46eb7",
+ dontknow=1,
+ page_length=100, # site uses 20
+# cursor="ef693874-0ad2-48cc-8c52-ac4de31cbf54" # here you put the id of the last episode you've seen in the list
+ )))
+)
+
+print(response)
+
+episodes = strip_grpc_response(response.content)
+
+
+#with open("ListPodcastEpisodes.bin", "rb") as f:
+# response = strip_grpc(f.read())
+
+
+@protobug.message
+class Audio:
+ revision: protobug.Int32 = protobug.field(1)
+ url: protobug.String = protobug.field(2)
+ fileSize: protobug.Int64 = protobug.field(3)
+ durationSec: protobug.Int64 = protobug.field(4)
+ transcoded: protobug.Bool = protobug.field(5)
+
+@protobug.message
+class EpisodeStartAt:
+ seconds: protobug.UInt64 = protobug.field(1)
+ nanos: protobug.UInt64 = protobug.field(2, default=0)
+
+
+@protobug.message
+class PodcastEpisode:
+ id: protobug.String = protobug.field(1)
+ workspaceId: protobug.String = protobug.field(2)
+ channelId: protobug.String = protobug.field(3)
+ title: protobug.String = protobug.field(4)
+ description: protobug.String = protobug.field(5)
+ audio: Audio = protobug.field(8)
+ channelImageUrl: protobug.String = protobug.field(16)
+ channelTitle: protobug.String = protobug.field(17)
+ channelStationName: protobug.String = protobug.field(18)
+ channelAuthor: protobug.String = protobug.field(19)
+ channelThumbnailImageUrl: protobug.String = protobug.field(21)
+ channelStationType: protobug.UInt32 = protobug.field(22)
+ startAt: EpisodeStartAt = protobug.field(27)
+ isEnabled: protobug.Bool = protobug.field(29)
+ hasTranscription: protobug.Bool = protobug.field(32)
+
+@protobug.message
+class ListPodcastEpisodesResponse:
+ episodes: list[PodcastEpisode] = protobug.field(1)
+ hasNextPage: protobug.Bool | None = protobug.field(2, default=None)
+
+
+episodes_response = protobug.loads(episodes, ListPodcastEpisodesResponse)
+
+for e in episodes_response.episodes:
+ print(e.title, e.id)
--
cgit v1.2.3-70-g09d2
From bff766d60b340a8d48d689428727bd131558f2be Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 11 Jul 2025 08:16:11 +0100
Subject: make protobuf hasNextPage default to false, not None
---
misc/protostuff.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/misc/protostuff.py b/misc/protostuff.py
index a461c02..9e5c19f 100755
--- a/misc/protostuff.py
+++ b/misc/protostuff.py
@@ -86,7 +86,7 @@ headers = {
response = requests.post('https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes', headers=headers,
data=add_grpc_header(protobug.dumps(ListPodcastEpisodesRequest(
- channel_id="09f27a48-ae04-4ce7-a024-572460e46eb7",
+ channel_id="1c931755-4d85-46f3-814c-7c13d771cf3c",
dontknow=1,
page_length=100, # site uses 20
# cursor="ef693874-0ad2-48cc-8c52-ac4de31cbf54" # here you put the id of the last episode you've seen in the list
@@ -137,10 +137,11 @@ class PodcastEpisode:
@protobug.message
class ListPodcastEpisodesResponse:
episodes: list[PodcastEpisode] = protobug.field(1)
- hasNextPage: protobug.Bool | None = protobug.field(2, default=None)
+ hasNextPage: protobug.Bool = protobug.field(2, default=False)
episodes_response = protobug.loads(episodes, ListPodcastEpisodesResponse)
for e in episodes_response.episodes:
print(e.title, e.id)
+print(episodes_response.hasNextPage)
--
cgit v1.2.3-70-g09d2
From aa0c45719426215ee7f7fca56c0607fff7cbfd3b Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 12 Jul 2025 03:01:44 +0100
Subject: podcast protobuf test: add episode image urls
---
misc/protostuff.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/misc/protostuff.py b/misc/protostuff.py
index 9e5c19f..7ef0e95 100755
--- a/misc/protostuff.py
+++ b/misc/protostuff.py
@@ -86,9 +86,9 @@ headers = {
response = requests.post('https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes', headers=headers,
data=add_grpc_header(protobug.dumps(ListPodcastEpisodesRequest(
- channel_id="1c931755-4d85-46f3-814c-7c13d771cf3c",
+ channel_id="0ce1d2d7-5e07-4ec5-901a-d0eacdacc332",
dontknow=1,
- page_length=100, # site uses 20
+ page_length=200, # site uses 20
# cursor="ef693874-0ad2-48cc-8c52-ac4de31cbf54" # here you put the id of the last episode you've seen in the list
)))
)
@@ -98,8 +98,8 @@ print(response)
episodes = strip_grpc_response(response.content)
-#with open("ListPodcastEpisodes.bin", "rb") as f:
-# response = strip_grpc(f.read())
+with open("ListPodcastEpisodes.bin", "wb") as f:
+ f.write(episodes)
@protobug.message
@@ -123,17 +123,22 @@ class PodcastEpisode:
channelId: protobug.String = protobug.field(3)
title: protobug.String = protobug.field(4)
description: protobug.String = protobug.field(5)
+
audio: Audio = protobug.field(8)
channelImageUrl: protobug.String = protobug.field(16)
channelTitle: protobug.String = protobug.field(17)
channelStationName: protobug.String = protobug.field(18)
channelAuthor: protobug.String = protobug.field(19)
+
channelThumbnailImageUrl: protobug.String = protobug.field(21)
channelStationType: protobug.UInt32 = protobug.field(22)
startAt: EpisodeStartAt = protobug.field(27)
isEnabled: protobug.Bool = protobug.field(29)
hasTranscription: protobug.Bool = protobug.field(32)
+ imageUrl: protobug.String = protobug.field(7, default=None)
+ thumbnailImageUrl: protobug.String = protobug.field(20, default=None)
+
@protobug.message
class ListPodcastEpisodesResponse:
episodes: list[PodcastEpisode] = protobug.field(1)
@@ -142,6 +147,8 @@ class ListPodcastEpisodesResponse:
episodes_response = protobug.loads(episodes, ListPodcastEpisodesResponse)
+print(episodes_response)
+
for e in episodes_response.episodes:
print(e.title, e.id)
print(episodes_response.hasNextPage)
--
cgit v1.2.3-70-g09d2
From 42cd450ab7578f723a5590003f751052d0a83ad7 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 11 Aug 2025 09:10:14 +0100
Subject: Add podcast protobufs + functions to use them
---
yt_dlp_plugins/extractor/radiko_protobufs.py | 152 +++++++++++++++++++++++++++
1 file changed, 152 insertions(+)
create mode 100755 yt_dlp_plugins/extractor/radiko_protobufs.py
diff --git a/yt_dlp_plugins/extractor/radiko_protobufs.py b/yt_dlp_plugins/extractor/radiko_protobufs.py
new file mode 100755
index 0000000..2336f10
--- /dev/null
+++ b/yt_dlp_plugins/extractor/radiko_protobufs.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+try:
+ import protobug
+except ImportError:
+ protobug = None
+
+import base64
+import struct
+
+import random
+import requests
+
+if protobug: # i suppose it works lmao
+
+
+ def add_grpc_header(protobuf_data):
+ compression_flag = 0
+ message_length = len(protobuf_data)
+ header = struct.pack('>BI', compression_flag, message_length)
+ return header + protobuf_data
+
+ def strip_grpc_response(response):
+ return response[5:].rpartition(b"grpc-status:")[0]
+
+ def _download_grpc(self, url_or_request, video_id, response_message, note="Downloading GRPC information", *args, **kwargs):
+ urlh = self._request_webpage(url_or_request, video_id,
+ headers={
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+ **kwargs.pop('headers')
+ },
+ data=add_grpc_header(protobug.dumps(kwargs.pop('data'))), note=note,
+ *args, **kwargs,
+ )
+ response = urlh.read()
+
+ protobuf = strip_grpc_response(response)
+ if len(protobuf) > 0:
+ return protobug.loads(protobuf, response_message)
+
+
+ @protobug.message
+ class SignUpRequest:
+ lsid: protobug.String = protobug.field(1)
+
+ def sign_up(self):
+ lsid = ''.join(random.choices('0123456789abcdef', k=32))
+
+ signup = _download_grpc(self, "https://api.annex.radiko.jp/radiko.UserService/SignUp",
+ "UserService", None, note="Registering ID", headers={'Origin': 'https://radiko.jp'},
+ data=SignUpRequest(lsid=lsid),
+ )
+ # youre meant to only do the sign up ^ once and then keep your lsid for later
+ # so that you can sign in and get the token for the API to work
+ return lsid
+
+
+ @protobug.message
+ class SignInRequest:
+ lsid: protobug.String = protobug.field(2)
+ area: protobug.String = protobug.field(3)
+
+ @protobug.message
+ class SignInResponse:
+ jwt: protobug.String = protobug.field(1)
+
+
+ def sign_in(self, lsid):
+ sign_in = _download_grpc(self, "https://api.annex.radiko.jp/radiko.UserService/SignIn",
+ "UserService", SignInResponse, note="Getting auth token", headers={'Origin': 'https://radiko.jp'},
+ data=SignInRequest(lsid=lsid, area="JP13"),
+ )
+ return sign_in.jwt
+
+
+ def auth_userservice(self):
+ cachedata = self.cache.load("rajiko", "UserService")
+ if cachedata is not None:
+ lsid = cachedata.get("lsid")
+ else:
+ lsid = sign_up(self)
+ self.cache.store("rajiko", "UserService", {"lsid": lsid})
+ jwt = sign_in(self, lsid)
+ return jwt
+
+
+ @protobug.message
+ class ListPodcastEpisodesRequest:
+ channel_id: protobug.String = protobug.field(1)
+ dontknow: protobug.Int32 = protobug.field(2)
+ page_length: protobug.Int32 = protobug.field(4)
+ cursor: protobug.String = protobug.field(5, default=None)
+
+
+ @protobug.message
+ class Audio:
+ revision: protobug.Int32 = protobug.field(1)
+ url: protobug.String = protobug.field(2)
+ fileSize: protobug.Int64 = protobug.field(3)
+ durationSec: protobug.Int64 = protobug.field(4)
+ transcoded: protobug.Bool = protobug.field(5)
+
+ @protobug.message
+ class EpisodeStartAt:
+ seconds: protobug.UInt64 = protobug.field(1)
+ nanos: protobug.UInt64 = protobug.field(2, default=0)
+
+
+ @protobug.message
+ class PodcastEpisode:
+ id: protobug.String = protobug.field(1)
+ workspaceId: protobug.String = protobug.field(2)
+ channelId: protobug.String = protobug.field(3)
+ title: protobug.String = protobug.field(4)
+ description: protobug.String = protobug.field(5)
+
+ audio: Audio = protobug.field(8)
+ channelImageUrl: protobug.String = protobug.field(16)
+ channelTitle: protobug.String = protobug.field(17)
+ channelStationName: protobug.String = protobug.field(18)
+ channelAuthor: protobug.String = protobug.field(19)
+
+ channelThumbnailImageUrl: protobug.String = protobug.field(21)
+ channelStationType: protobug.UInt32 = protobug.field(22)
+ startAt: EpisodeStartAt = protobug.field(27)
+ isEnabled: protobug.Bool = protobug.field(29)
+ hasTranscription: protobug.Bool = protobug.field(32)
+
+ imageUrl: protobug.String = protobug.field(7, default=None)
+ thumbnailImageUrl: protobug.String = protobug.field(20, default=None)
+
+ @protobug.message
+ class ListPodcastEpisodesResponse:
+ episodes: list[PodcastEpisode] = protobug.field(1)
+ hasNextPage: protobug.Bool = protobug.field(2, default=False)
+
+
+ def get_podcast_episodes(self, channel_id, jwt, cursor, page_length=20):
+ # site uses 20 items
+ # cursor is the id of the last episode you've seen in the list
+
+ return _download_grpc(self, 'https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes',
+ channel_id, ListPodcastEpisodesResponse, note="Downloading episode listings",
+ headers={'Authorization': f'Bearer {jwt}'},
+ data=ListPodcastEpisodesRequest(
+ channel_id=channel_id,
+ dontknow=1,
+ page_length=page_length,
+ cursor=cursor,
+ )
+ )
--
cgit v1.2.3-70-g09d2
From ac94bad6ed14f32adfeceac35cc60d39680508dd Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 11 Aug 2025 09:22:50 +0100
Subject: Implement multi-page podcasts with protobug
needs core change in upstream to work (traverse_obj doesnt work with dataclasses)
---
yt_dlp_plugins/extractor/radiko_podcast.py | 39 ++++++++++++++++++------------
1 file changed, 24 insertions(+), 15 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index af66f6a..904bb62 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -6,9 +6,12 @@ from yt_dlp.utils import (
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...
+try:
+ import protobug
+ import yt_dlp_plugins.extractor.radiko_protobufs as pb
+except ImportError:
+ protobug = None
+
class _RadikoPodcastBaseIE(InfoExtractor):
@@ -32,7 +35,7 @@ class _RadikoPodcastBaseIE(InfoExtractor):
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")),
+ "webpage_url": "https://radiko.jp/podcast/episodes/{id}".format(id=traverse_obj(episode_info, "id")),
'extractor_key': RadikoPodcastEpisodeIE.ie_key(),
'extractor': 'RadikoPodcastEpisode',
}
@@ -82,29 +85,35 @@ class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
}]
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_id = self._match_id(url)
+ webpage = self._download_webpage(url, channel_id)
+ next_data = self._search_nextjs_data(webpage, channel_id)["props"]["pageProps"]
channel_info = next_data["podcastChannel"]
episode_list_response = next_data["listPodcastEpisodesResponse"]
def entries():
+ has_next_page = episode_list_response.get("hasNextPage")
for episode in episode_list_response["episodesList"]:
+ cursor = episode.get("id")
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
-
+ if has_next_page:
+ if protobug:
+ userservice_token = pb.auth_userservice(self)
+ while has_next_page:
+ page = pb.get_podcast_episodes(self, channel_id, userservice_token, cursor)
+ has_next_page = page.hasNextPage
+ for episode in page.episodes:
+ cursor = episode.id
+ yield self._extract_episode(episode)
+ else:
+ self.report_warning(f'Only extracting the latest {len(episode_list_response["episodesList"])} episodes. Install protobug for more.')
return {
"_type": "playlist",
- "id": video_id,
+ "id": channel_id,
**traverse_obj(channel_info, {
"playlist_title": "title",
"playlist_id": "id",
--
cgit v1.2.3-70-g09d2
From b97b4169b40bbf074ca3dcd9466ff9ee0a8070e0 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Mon, 11 Aug 2025 09:26:03 +0100
Subject: fix timefree test (description changed)
---
misc/test_extractors.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index 9289498..f6ee0dd 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -125,7 +125,7 @@ RadikoShareIE._TESTS = [{
"title": "JET STREAM",
"series": "JET STREAM",
- "description": r"re:^JET STREAM・・・作家が描く世界への旅。[\s\S]+https://www.tfm.co.jp/f/jetstream/message$",
+ "description": r"re:^JET STREAM・・・[\s\S]+https://www.tfm.co.jp/f/jetstream/message$",
"chapters": list,
"thumbnail": "https://program-static.cf.radiko.jp/greinlrspi.jpg",
--
cgit v1.2.3-70-g09d2
From d5f824093b0748889916a1ba820398aecaa184c8 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Wed, 13 Aug 2025 02:49:46 +0100
Subject: convert protobug obj to dict, for traverse_obj
---
yt_dlp_plugins/extractor/radiko_podcast.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index 904bb62..84bc288 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -6,6 +6,7 @@ from yt_dlp.utils import (
str_or_none,
)
+import dataclasses
try:
import protobug
import yt_dlp_plugins.extractor.radiko_protobufs as pb
@@ -107,7 +108,7 @@ class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
has_next_page = page.hasNextPage
for episode in page.episodes:
cursor = episode.id
- yield self._extract_episode(episode)
+ yield self._extract_episode(dataclasses.asdict(episode))
else:
self.report_warning(f'Only extracting the latest {len(episode_list_response["episodesList"])} episodes. Install protobug for more.')
--
cgit v1.2.3-70-g09d2
From 9e91cb5ee32a47eb05dc2d3885e13d274cdadd03 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Wed, 13 Aug 2025 07:29:19 +0100
Subject: ListPodcastEpisodesRequest: dontknow -> sort_by_latest
---
yt_dlp_plugins/extractor/radiko_protobufs.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko_protobufs.py b/yt_dlp_plugins/extractor/radiko_protobufs.py
index 2336f10..ff4531e 100755
--- a/yt_dlp_plugins/extractor/radiko_protobufs.py
+++ b/yt_dlp_plugins/extractor/radiko_protobufs.py
@@ -88,7 +88,7 @@ if protobug: # i suppose it works lmao
@protobug.message
class ListPodcastEpisodesRequest:
channel_id: protobug.String = protobug.field(1)
- dontknow: protobug.Int32 = protobug.field(2)
+ sort_by_latest: protobug.Bool = protobug.field(2)
page_length: protobug.Int32 = protobug.field(4)
cursor: protobug.String = protobug.field(5, default=None)
@@ -145,7 +145,7 @@ if protobug: # i suppose it works lmao
headers={'Authorization': f'Bearer {jwt}'},
data=ListPodcastEpisodesRequest(
channel_id=channel_id,
- dontknow=1,
+ sort_by_latest=True,
page_length=page_length,
cursor=cursor,
)
--
cgit v1.2.3-70-g09d2
From c229a64db275ddc7b87f5c23a8570a10e3e2cbd3 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Wed, 13 Aug 2025 12:04:47 +0100
Subject: add protobug as optional dependency
tbh i might just wait until its in yt-dlp proper,
would make packaging easier...
---
pyproject.toml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
index 2a13f3f..d92abe7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,9 @@ Homepage = "https://427738.xyz/yt-dlp-rajiko/"
"Source Code" = "https://github.com/garret1317/yt-dlp-rajiko/"
"Release Notes" = "https://427738.xyz/yt-dlp-rajiko/CHANGELOG.xml"
+[project.optional-dependencies]
+protobuf = ["protobug"]
+
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
--
cgit v1.2.3-70-g09d2
From 4da1432900abacc270d615314475c284c60b69ae Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 24 Aug 2025 12:13:13 +0100
Subject: set vcodec none for chunked formats
so they get marked as audio only
---
yt_dlp_plugins/extractor/radiko.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 2996290..9601c32 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -322,6 +322,7 @@ class _RadikoBaseIE(InfoExtractor):
"hls_media_playlist_data": chunks_playlist,
"preference": preference,
"ext": "m4a",
+ "vcodec": "none",
# fallback to live for ffmpeg etc
"url": playlist_url,
--
cgit v1.2.3-70-g09d2
From eb16055bdec43684c7322795ee6f4bddc9ffc2c4 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 24 Aug 2025 12:39:31 +0100
Subject: tests action: stop setup-ffmpeg getting too-new version
the version script gets version 8.0, which BtbN/FFmpeg-Builds doesn't build yet
so the download script tries to get a file that doesn't exist, and fails
if i set it to master then supposedly it won't use the version number and be fine
---
.github/workflows/download.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml
index 66274b3..2a39a3b 100644
--- a/.github/workflows/download.yml
+++ b/.github/workflows/download.yml
@@ -30,6 +30,8 @@ jobs:
- name: install ffmpeg
uses: AnimMouse/setup-ffmpeg@v1
+ with:
+ version: master
- name: get yt-dlp source (for the test_download script we override)
uses: actions/checkout@v4
--
cgit v1.2.3-70-g09d2
From 221cefe4810e0bcd5c6faf199ae2589f62edbb77 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Tue, 26 Aug 2025 00:37:05 +0100
Subject: Set ffmpeg args to not send Range header
github issue #29
---
yt_dlp_plugins/extractor/radiko.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 9601c32..fb705dd 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -317,7 +317,7 @@ class _RadikoBaseIE(InfoExtractor):
self, playlist_url, start_at, end_at, domain, auth_headers
)
- formats.append({
+ m3u8_formats = [{
"format_id": join_nonempty(domain, "chunked"),
"hls_media_playlist_data": chunks_playlist,
"preference": preference,
@@ -327,13 +327,20 @@ class _RadikoBaseIE(InfoExtractor):
# fallback to live for ffmpeg etc
"url": playlist_url,
"http_headers": auth_headers,
- })
+ }]
else:
- formats += self._extract_m3u8_formats(
+ m3u8_formats = self._extract_m3u8_formats(
playlist_url, station, m3u8_id=domain, fatal=False, headers=auth_headers,
live=delivered_live, preference=preference, entry_protocol=entry_protocol,
note=f"Downloading m3u8 information from {domain}")
+
+ for f in m3u8_formats:
+ # ffmpeg sends a Range header which some streams reject. here we disable that (and also some icecast header as well)
+ f['downloader_options'] = {'ffmpeg_args': ['-seekable', '0', '-http_seekable', '0', '-icy', '0']}
+ f['format_note'] = ", ".join(format_note)
+ formats.append(f)
+
return formats
--
cgit v1.2.3-70-g09d2
From a12ae8fe4636399ce4582c6cdb0bc67352b2caf3 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 16 Aug 2025 06:32:40 +0100
Subject: Flag and deprioritise formats with ad insertion
---
yt_dlp_plugins/extractor/radiko.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index fb705dd..1f5bf30 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -87,6 +87,7 @@ class _RadikoBaseIE(InfoExtractor):
_DELIVERED_ONDEMAND = ('radiko.jp',)
_DOESNT_WORK_WITH_FFMPEG = ('tf-f-rpaa-radiko.smartstream.ne.jp', 'si-f-radiko.smartstream.ne.jp', 'alliance-stream-radiko.smartstream.ne.jp')
+ _AD_INSERTION = ('si-f-radiko.smartstream.ne.jp', )
_has_tf30 = None
@@ -299,6 +300,7 @@ class _RadikoBaseIE(InfoExtractor):
delivered_live = True
preference = -1
entry_protocol = 'm3u8'
+ format_note=[]
if domain in self._DOESNT_WORK_WITH_FFMPEG and do_blacklist_streams:
self.write_debug(f"skipping {domain} (known not working)")
@@ -306,8 +308,12 @@ class _RadikoBaseIE(InfoExtractor):
if domain in self._DELIVERED_ONDEMAND:
# override the defaults for delivered as on-demand
delivered_live = False
- preference = 1
+ preference += 2
entry_protocol = None
+ if domain in self._AD_INSERTION:
+ preference -= 3
+ format_note.append("Ad insertion")
+
auth_headers = auth_data["token"]
@@ -328,6 +334,7 @@ class _RadikoBaseIE(InfoExtractor):
"url": playlist_url,
"http_headers": auth_headers,
}]
+ format_note.append("Chunked")
else:
m3u8_formats = self._extract_m3u8_formats(
--
cgit v1.2.3-70-g09d2
From cbb46ec14339cd938f96446440ea2bd32a2ff1ba Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 29 Aug 2025 20:35:13 +0100
Subject: PersonsIE: construct a timefree url_result directly
so that --download-archive etc will quickly skip without a full extraction
also removes reliance on `radiko_url` always being a share url
---
yt_dlp_plugins/extractor/radiko.py | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 1f5bf30..94df945 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -733,19 +733,16 @@ class RadikoPersonIE(InfoExtractor):
def entries():
key_station_only = len(self._configuration_arg("key_station_only", ie_key="rajiko")) > 0
for episode in person_api.get("data"):
- if key_station_only and episode.get("key_station_id") != episode.get("station_id"):
- continue
- share_url = traverse_obj(episode, ("radiko_url", ("pc", "sp", "android", "ios", "app"),
- {url_or_none}), get_all=False)
- # they're all identical share links at the moment (5th aug 2024) but they might not be in the future
+ station = episode.get("station_id")
+ if key_station_only and episode.get("key_station_id") != station:
+ continue
- # predictions:
- # pc will probably stay the same
- # don't know what sp is, possibly "SmartPhone"?, anyway seems reasonably generic
- # android is easier for me to reverse-engineer than ios (no ithing)
- # i assume "app" would be some internal tell-it-to-do-something link, not a regular web link
+ start = episode.get("start_at")
+ timestring = rtime.RadikoTime.fromisoformat(start).timestring()
- yield self.url_result(share_url, ie=RadikoShareIE, video_title=episode.get("title"))
+ timefree_id = join_nonempty(station, timestring)
+ timefree_url = f"https://radiko.jp/#!/ts/{station}/{timestring}"
+ yield self.url_result(timefree_url, ie=RadikoTimeFreeIE, video_id=timefree_id)
return self.playlist_result(entries(), playlist_id=join_nonempty("person", person_id))
--
cgit v1.2.3-70-g09d2
From bd04585fa66131530227456b9f8c2717c0d23a23 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 29 Aug 2025 20:57:50 +0100
Subject: add support for "r_seasons" programme pages
---
misc/test_extractors.py | 4 +++-
yt_dlp_plugins/extractor/radiko.py | 42 ++++++++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
index f6ee0dd..21800c5 100755
--- a/misc/test_extractors.py
+++ b/misc/test_extractors.py
@@ -44,7 +44,8 @@ def get_test_timefields(airtime, release_time):
from yt_dlp_plugins.extractor.radiko import (
RadikoTimeFreeIE, RadikoShareIE,
- RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE
+ RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
+ RadikoRSeasonsIE
)
from yt_dlp_plugins.extractor.radiko_podcast import (
@@ -147,6 +148,7 @@ IEs = [
RadikoTimeFreeIE, RadikoShareIE,
RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
+ RadikoRSeasonsIE,
]
import test.helper as th
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 94df945..e81a1ca 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -746,3 +746,45 @@ class RadikoPersonIE(InfoExtractor):
yield self.url_result(timefree_url, ie=RadikoTimeFreeIE, video_id=timefree_id)
return self.playlist_result(entries(), playlist_id=join_nonempty("person", person_id))
+
+
+class RadikoRSeasonsIE(InfoExtractor):
+ _VALID_URL = r"https?://(?:www\.)?radiko\.jp/(?:mobile/)?r_seasons/(?P\d+$)"
+ _TESTS = [{
+ "url": "https://radiko.jp/r_seasons/10012302",
+ "playlist_mincount": 4,
+ "info_dict": {
+ "id": '10012302',
+ "title": '山下達郎の楽天カード サンデー・ソングブック',
+ }
+ }, {
+ "url": "https://radiko.jp/r_seasons/10002831",
+ "playlist_mincount": 4,
+ "info_dict": {
+ "id": "10002831",
+ "title": "Tokyo Moon",
+ }
+ }]
+
+ def _real_extract(self, url):
+ season_id = self._match_id(url)
+ html = self._download_webpage(url, season_id)
+ pageProps = self._search_nextjs_data(html, season_id)["props"]["pageProps"]
+ season_id = traverse_obj(pageProps, ("rSeason", "id")) or season_id
+
+ def entries():
+ for episode in pageProps.get("pastPrograms"):
+ station = traverse_obj(episode, ("stationId"))
+ start = traverse_obj(episode, ("startAt", "seconds"))
+ timestring = rtime.RadikoTime.fromtimestamp(start, tz=rtime.JST).timestring()
+
+ timefree_id = join_nonempty(station, timestring)
+ timefree_url = f"https://radiko.jp/#!/ts/{station}/{timestring}"
+
+ yield self.url_result(timefree_url, ie=RadikoTimeFreeIE, video_id=timefree_id)
+
+ return self.playlist_result(
+ entries(),
+ playlist_id=season_id,
+ playlist_title=traverse_obj(pageProps, ("rSeason", "rSeasonName")),
+ )
--
cgit v1.2.3-70-g09d2
From 205bcb84e106126fedf419ab869c68bbee739441 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 11 Sep 2025 17:09:35 +0100
Subject: update some comments
---
yt_dlp_plugins/extractor/radiko.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index e81a1ca..d387a79 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -256,8 +256,7 @@ class _RadikoBaseIE(InfoExtractor):
config_device = traverse_obj(self._configuration_arg('device', casesense=True, ie_key="rajiko"), 0)
if not use_pc_html5:
- device = config_device or "aSmartPhone7a" # this device only gives us the on-demand one for timefree
- # that's good imo - we just get the one that works, and don't bother with probing the rest as well
+ device = config_device or "aSmartPhone7a" # still has the radiko.jp on-demand one for timefree
else:
device = config_device or "pc_html5" # the on-demand one doesnt work with timefree30 stuff sadly
# so just use pc_html5 which has everything
@@ -273,6 +272,7 @@ class _RadikoBaseIE(InfoExtractor):
do_as_live_chunks = not len(self._configuration_arg("no_as_live_chunks", ie_key="rajiko")) > 0
for element in url_data.findall(f".//url[@timefree='{timefree_int}'][@areafree='0']/playlist_create_url"):
# find s with matching timefree and no areafree, then get their
+ # we don't want areafree here because we should always be in-region
url = element.text
if url in seen_urls: # there are always dupes, even with ^ specific filtering
continue
--
cgit v1.2.3-70-g09d2
From 3d8d8b1cb6e3a7792077b413d705c8337ae9c014 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 11 Sep 2025 18:14:59 +0100
Subject: Don't return any formats when we don't actually have any
---
yt_dlp_plugins/extractor/radiko.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index d387a79..ae0ee49 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -542,9 +542,13 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
region = self._get_station_region(station)
station_meta = self._get_station_meta(region, station)
- chapters = self._extract_chapters(station, start, end, video_id=meta["id"])
- auth_data = self._auth(region, need_tf30=need_tf30)
- formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, use_pc_html5=need_tf30)
+ if live_status == "was_live":
+ chapters = self._extract_chapters(station, start, end, video_id=meta["id"])
+ auth_data = self._auth(region, need_tf30=need_tf30)
+ formats = self._get_station_formats(station, True, auth_data, start_at=start, end_at=end, use_pc_html5=need_tf30)
+ else:
+ chapters = None
+ formats = None
return {
**station_meta,
--
cgit v1.2.3-70-g09d2
From 770bfe4411b96b28e136fbedd6b00de5cac17823 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 12 Sep 2025 13:10:26 +0100
Subject: Update stream monitor script to be more readable when changes
actually happen
---
misc/streammon.py | 29 +++++++++++++++++++++--------
1 file changed, 21 insertions(+), 8 deletions(-)
diff --git a/misc/streammon.py b/misc/streammon.py
index 4051833..8f52bb4 100755
--- a/misc/streammon.py
+++ b/misc/streammon.py
@@ -5,6 +5,7 @@
import difflib
import os
import sys
+import xml.etree.ElementTree as ET
from datetime import datetime
import requests
@@ -22,10 +23,25 @@ else:
devices = ('pc_html5', 'aSmartPhone7a', 'aSmartPhone8')
stations = ('FMT', 'CCL', 'NORTHWAVE', 'TBS')
+def format_xml(txt):
+ root = ET.fromstring(txt)
+ res = ""
+ for el in root.findall("url"):
+ res += el.find("playlist_create_url").text
+ for k, v in el.attrib.items():
+ res += f" {k}:{v}"
+
+ res += "\n"
+ return res
+
for device in devices:
for station in stations:
url = STREAMS_API.format(device=device, station=station)
- now = s.get(url).text
+ now_response = s.get(url)
+ now = now_response.text
+ now_modified = now_response.headers["last-modified"]
+ now_datetime = datetime.strptime(now_modified, "%a, %d %b %Y %H:%M:%S %Z")
+
filename = f"{PATH}{station}-{device}.xml"
with open(filename, "a+") as f:
@@ -34,9 +50,9 @@ for device in devices:
modtime = datetime.fromtimestamp(os.path.getmtime(filename))
diff = difflib.unified_diff(
- past.splitlines(), now.splitlines(),
+ format_xml(past).splitlines(), format_xml(now).splitlines(),
fromfile=url, tofile=url,
- fromfiledate=str(modtime), tofiledate=str(datetime.now()),
+ fromfiledate=str(modtime), tofiledate=str(now_datetime.now()),
)
diff_str = "\n".join(diff)
@@ -45,9 +61,6 @@ for device in devices:
f.write(now)
s.post(DISCORD_WEBHOOK, json={
- "embeds": [{
- "type": "rich",
- "title": f"Streams changed: {station} {device}",
- "description": "\n".join(("```diff", diff_str, "```"))
- }]
+ "content": f"**Streams changed: {station} {device}**\n" + "\n".join(("```diff", diff_str, "```")),
})
+ os.utime(filename, (now_datetime.timestamp(), now_datetime.timestamp()))
--
cgit v1.2.3-70-g09d2
From a88ca60ee514c5347193fc679933a31117bac7ea Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sat, 13 Sep 2025 15:27:57 +0100
Subject: clarify "indexing (station) regions" text
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index ae0ee49..0566b6d 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -94,7 +94,7 @@ class _RadikoBaseIE(InfoExtractor):
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 station regions")
for stations in tree:
for station in stations:
area = station.find("area_id").text
--
cgit v1.2.3-70-g09d2
From f3a16083b9f09c132dce6f262a1fb30f672b9441 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 15:04:02 +0100
Subject: rename "misc" -> "contrib"
---
.github/workflows/download.yml | 2 +-
contrib/generate_html.py | 84 +++++++++++++++++
contrib/how to do a release | 41 +++++++++
contrib/old_generate_changelog.py | 116 +++++++++++++++++++++++
contrib/protostuff.py | 154 +++++++++++++++++++++++++++++++
contrib/randominfo.py | 12 +++
contrib/streammon.py | 66 +++++++++++++
contrib/test_areas.py | 26 ++++++
contrib/test_extractors.py | 183 +++++++++++++++++++++++++++++++++++++
misc/generate_html.py | 84 -----------------
misc/how to do a release | 41 ---------
misc/old_generate_changelog.py | 116 -----------------------
misc/protostuff.py | 154 -------------------------------
misc/randominfo.py | 12 ---
misc/streammon.py | 66 -------------
misc/test_areas.py | 26 ------
misc/test_extractors.py | 183 -------------------------------------
yt_dlp_plugins/extractor/radiko.py | 2 +-
18 files changed, 684 insertions(+), 684 deletions(-)
create mode 100755 contrib/generate_html.py
create mode 100644 contrib/how to do a release
create mode 100755 contrib/old_generate_changelog.py
create mode 100755 contrib/protostuff.py
create mode 100755 contrib/randominfo.py
create mode 100755 contrib/streammon.py
create mode 100755 contrib/test_areas.py
create mode 100755 contrib/test_extractors.py
delete mode 100755 misc/generate_html.py
delete mode 100644 misc/how to do a release
delete mode 100755 misc/old_generate_changelog.py
delete mode 100755 misc/protostuff.py
delete mode 100755 misc/randominfo.py
delete mode 100755 misc/streammon.py
delete mode 100755 misc/test_areas.py
delete mode 100755 misc/test_extractors.py
diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml
index 2a39a3b..115ae4e 100644
--- a/.github/workflows/download.yml
+++ b/.github/workflows/download.yml
@@ -45,4 +45,4 @@ jobs:
- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}/yt-dlp${{ runner.os == 'Windows' && ';' || ':' }}${{ env.PYTHONPATH }}
- run: python ./yt-dlp-plugins/yt-dlp-rajiko/misc/test_extractors.py
+ run: python ./yt-dlp-plugins/yt-dlp-rajiko/contrib/test_extractors.py
diff --git a/contrib/generate_html.py b/contrib/generate_html.py
new file mode 100755
index 0000000..0e15d6a
--- /dev/null
+++ b/contrib/generate_html.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+import os
+import hashlib
+import re
+
+pip_index = open("index.html", "w")
+
+pip_index.write("""
+
+
+ yt-dlp-rajiko pip index
+
+
+
+
+
+
+
+""")
+
+latest_tarball = tarballs[-1]
+latest_wheel = wheels[-1]
+print(latest_tarball, latest_wheel)
+
+os.remove("yt_dlp_rajiko-latest.tar.gz")
+os.symlink(latest_tarball, "yt_dlp_rajiko-latest.tar.gz")
+
+os.remove("yt_dlp_rajiko-latest.whl")
+os.symlink(latest_wheel, "yt_dlp_rajiko-latest.whl")
+
+site_sha256.reverse()
+
+latest_list = site_sha256[:2]
+previous_list = site_sha256[2:]
+
+latest = "\n".join(["", "", "\n".join(latest_list), "", ""])
+
+previous = "\n".join(["", "", "\n".join(previous_list), "", ""])
+
+for i in ["../../index.html", "../../index.ja.html"]:
+ with open(i, "r+") as f:
+ page = f.read()
+
+ page = re.sub(r".+", latest, page, flags=re.DOTALL)
+ page = re.sub(r".+", previous, page, flags=re.DOTALL)
+
+ f.seek(0)
+ f.truncate(0)
+ f.write(page)
diff --git a/contrib/how to do a release b/contrib/how to do a release
new file mode 100644
index 0000000..6e91e14
--- /dev/null
+++ b/contrib/how to do a release
@@ -0,0 +1,41 @@
+putting this here because i'll forget how to do it otherwise
+
+update the pyproject.toml
+tag it in git, eg v1.0
+
+## build the builds
+python3 -m build
+
+and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
+because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
+
+run script to update the pip index html and the dl/ "latest" symlinks
+this also updates the sha256s on the site
+
+## update the changelog file
+
+write in html, paste into the feed xml like
+make sure to set the link, date
+to get date use:
+git log --pretty --date=rfc2822
+
+include the pip instructions, sha256sum etc
+
+now push to the server
+
+!!NEW!!
+upload to pip proper as well
+go to dl/ dir and do
+twine upload yt_dlp_rajiko-1.x*
+
+
+## update github
+
+paste the changelog output into a github release, upload the new builds
+change link at the bottom to just "below"
+
+post in the radiko thread on 5ch if i can be bothered
+
+and thats probably all
diff --git a/contrib/old_generate_changelog.py b/contrib/old_generate_changelog.py
new file mode 100755
index 0000000..1bce073
--- /dev/null
+++ b/contrib/old_generate_changelog.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+import email.utils
+import feedgenerator
+
+def parse_changelog(lines):
+ got_version = False
+ got_date = False
+ got_url = False
+ done_remarks = False
+ releases = []
+ release = {}
+ release_remarks = []
+ release_changes = []
+ current_change = ""
+
+ for idx, line in enumerate(lines):
+ line = line.rstrip()
+
+ if not got_version:
+ got_version = True
+ release["version"] = line
+ continue
+
+ if not got_date:
+ release["date"] = email.utils.parsedate_to_datetime(line)
+ got_date = True
+ continue
+
+ key, sep, val = line.partition(": ")
+ if key in ["url", "sha256", "released"] and val != "":
+ release[key] = val
+ continue
+
+ if not done_remarks:
+ if line == "":
+ done_remarks = True
+ release["remarks"] = release_remarks
+ release_remarks = []
+ continue
+ else:
+ release_remarks.append(line)
+ continue
+
+ if line != "":
+ release_changes.append(line.rstrip())
+
+ if idx + 1 != len(lines):
+ continue
+
+ release["changes"] = release_changes
+ if release.get("released") != "no":
+ releases.append(release)
+
+ got_version = False
+ got_date = False
+ done_remarks = False
+ release = {}
+ release_changes = []
+
+ return releases
+
+def generate_rss_feed(releases):
+ feed = feedgenerator.Rss201rev2Feed(
+ title="yt-dlp-rajiko changelog",
+ description="Notifications for new yt-dlp-rajiko releases, with changelogs",
+ link="https://427738.xyz/yt-dlp-rajiko/",
+ language="en-GB",
+ ttl=180, # 3 hours
+ )
+
+ for release in releases:
+ title = "yt-dlp-rajiko " + release["version"] + " has been released"
+ description = ""
+ description += "
"
+ for remark in release["remarks"]:
+ description += remark
+ description += " "
+ description += "
"
+ description += "
This release:
\n"
+ description += "
"
+ for change in release["changes"]:
+ description += "
"
+ description += change
+ description += "
\n"
+ description += "
"
+
+ if release.get("url"):
+ if release["version"] != "1.0":
+ description += "\n
If you use pip, you should be able to upgrade with pip install yt-dlp-rajiko --upgrade --extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/. "
+ description += "If you installed manually, you can download the updated .whl from this post's link."
+ if release.get("sha256"):
+ description += " The SHA256 checksum should be "
+ description += release.get("sha256")
+ description += "."
+ description += "
"
+ else:
+ description += '\n
Please see the homepage for initial installation instructions.
'
+
+ feed.add_item(
+ title=title,
+ description=description,
+ link=release.get("url"),
+ pubdate=release["date"]
+ )
+ return feed
+
+if __name__ == "__main__":
+ with open("CHANGELOG") as f:
+ releases = parse_changelog(f.readlines())
+
+ feed = generate_rss_feed(releases)
+ feed_contents = feed.writeString("utf-8")
+ feed_contents = feed_contents.replace("\nBI', compression_flag, message_length)
+ return header + protobuf_data
+
+def strip_grpc_response(response):
+ return response[5:].rpartition(b"grpc-status:")[0]
+
+print("SIGNUP")
+# why do they have to make it so bloody complicated
+
+lsid = ''.join(random.choices('0123456789abcdef', k=32))
+big_funny = ("\n " + lsid).encode()
+
+signup = requests.post("https://api.annex.radiko.jp/radiko.UserService/SignUp", headers={
+ 'Origin': 'https://radiko.jp',
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+ }, data=( add_grpc_header(big_funny)),
+)
+
+print(signup.content)
+
+# youre meant to only do the sign up ^ once and then keep your id for later
+# so that you can V sign in and get the token for the API to work
+
+print("SIGNIN")
+
+si=add_grpc_header(protobug.dumps(SignInRequest(
+ lsid=lsid,
+ area="JP13",
+)))
+
+print(si)
+print(base64.b64encode(si))
+
+signin = requests.post("https://api.annex.radiko.jp/radiko.UserService/SignIn", headers={
+ 'Origin': 'https://radiko.jp',
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+}, data=si)
+
+print(signin.content)
+
+signin_result = protobug.loads(strip_grpc_response(signin.content), SignInResponse)
+
+
+headers = {
+ 'Origin': 'https://radiko.jp',
+ 'Authorization': f'Bearer {signin_result.jwt}',
+ 'x-annex-proto-version': '1.0.0',
+ 'Content-Type': 'application/grpc-web+proto',
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
+ 'X-Grpc-Web': '1',
+}
+
+response = requests.post('https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes', headers=headers,
+ data=add_grpc_header(protobug.dumps(ListPodcastEpisodesRequest(
+ channel_id="0ce1d2d7-5e07-4ec5-901a-d0eacdacc332",
+ dontknow=1,
+ page_length=200, # site uses 20
+# cursor="ef693874-0ad2-48cc-8c52-ac4de31cbf54" # here you put the id of the last episode you've seen in the list
+ )))
+)
+
+print(response)
+
+episodes = strip_grpc_response(response.content)
+
+
+with open("ListPodcastEpisodes.bin", "wb") as f:
+ f.write(episodes)
+
+
+@protobug.message
+class Audio:
+ revision: protobug.Int32 = protobug.field(1)
+ url: protobug.String = protobug.field(2)
+ fileSize: protobug.Int64 = protobug.field(3)
+ durationSec: protobug.Int64 = protobug.field(4)
+ transcoded: protobug.Bool = protobug.field(5)
+
+@protobug.message
+class EpisodeStartAt:
+ seconds: protobug.UInt64 = protobug.field(1)
+ nanos: protobug.UInt64 = protobug.field(2, default=0)
+
+
+@protobug.message
+class PodcastEpisode:
+ id: protobug.String = protobug.field(1)
+ workspaceId: protobug.String = protobug.field(2)
+ channelId: protobug.String = protobug.field(3)
+ title: protobug.String = protobug.field(4)
+ description: protobug.String = protobug.field(5)
+
+ audio: Audio = protobug.field(8)
+ channelImageUrl: protobug.String = protobug.field(16)
+ channelTitle: protobug.String = protobug.field(17)
+ channelStationName: protobug.String = protobug.field(18)
+ channelAuthor: protobug.String = protobug.field(19)
+
+ channelThumbnailImageUrl: protobug.String = protobug.field(21)
+ channelStationType: protobug.UInt32 = protobug.field(22)
+ startAt: EpisodeStartAt = protobug.field(27)
+ isEnabled: protobug.Bool = protobug.field(29)
+ hasTranscription: protobug.Bool = protobug.field(32)
+
+ imageUrl: protobug.String = protobug.field(7, default=None)
+ thumbnailImageUrl: protobug.String = protobug.field(20, default=None)
+
+@protobug.message
+class ListPodcastEpisodesResponse:
+ episodes: list[PodcastEpisode] = protobug.field(1)
+ hasNextPage: protobug.Bool = protobug.field(2, default=False)
+
+
+episodes_response = protobug.loads(episodes, ListPodcastEpisodesResponse)
+
+print(episodes_response)
+
+for e in episodes_response.episodes:
+ print(e.title, e.id)
+print(episodes_response.hasNextPage)
diff --git a/contrib/randominfo.py b/contrib/randominfo.py
new file mode 100755
index 0000000..bdb7660
--- /dev/null
+++ b/contrib/randominfo.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+from yt_dlp_plugins.extractor import radiko
+from yt_dlp import YoutubeDL
+
+
+ie = radiko._RadikoBaseIE()
+ydl = YoutubeDL(auto_init=False)
+ie.set_downloader(ydl)
+
+info = ie._generate_random_info()
+print("random device info")
+print(info)
diff --git a/contrib/streammon.py b/contrib/streammon.py
new file mode 100755
index 0000000..8f52bb4
--- /dev/null
+++ b/contrib/streammon.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+# monitor stream APIs for any changes, so I can check they don't break anything
+# run via cronjob every now and then
+
+import difflib
+import os
+import sys
+import xml.etree.ElementTree as ET
+from datetime import datetime
+
+import requests
+
+s = requests.Session()
+
+DISCORD_WEBHOOK = "PUT WEBHOOK HERE"
+STREAMS_API = "https://radiko.jp/v3/station/stream/{device}/{station}.xml"
+
+if len(sys.argv) > 1:
+ PATH = sys.argv[1]
+else:
+ PATH = ""
+
+devices = ('pc_html5', 'aSmartPhone7a', 'aSmartPhone8')
+stations = ('FMT', 'CCL', 'NORTHWAVE', 'TBS')
+
+def format_xml(txt):
+ root = ET.fromstring(txt)
+ res = ""
+ for el in root.findall("url"):
+ res += el.find("playlist_create_url").text
+ for k, v in el.attrib.items():
+ res += f" {k}:{v}"
+
+ res += "\n"
+ return res
+
+for device in devices:
+ for station in stations:
+ url = STREAMS_API.format(device=device, station=station)
+ now_response = s.get(url)
+ now = now_response.text
+ now_modified = now_response.headers["last-modified"]
+ now_datetime = datetime.strptime(now_modified, "%a, %d %b %Y %H:%M:%S %Z")
+
+
+ filename = f"{PATH}{station}-{device}.xml"
+ with open(filename, "a+") as f:
+ f.seek(0)
+ past = f.read()
+
+ modtime = datetime.fromtimestamp(os.path.getmtime(filename))
+ diff = difflib.unified_diff(
+ format_xml(past).splitlines(), format_xml(now).splitlines(),
+ fromfile=url, tofile=url,
+ fromfiledate=str(modtime), tofiledate=str(now_datetime.now()),
+ )
+
+ diff_str = "\n".join(diff)
+ if diff_str != "":
+ f.truncate(0)
+ f.write(now)
+
+ s.post(DISCORD_WEBHOOK, json={
+ "content": f"**Streams changed: {station} {device}**\n" + "\n".join(("```diff", diff_str, "```")),
+ })
+ os.utime(filename, (now_datetime.timestamp(), now_datetime.timestamp()))
diff --git a/contrib/test_areas.py b/contrib/test_areas.py
new file mode 100755
index 0000000..ba6475f
--- /dev/null
+++ b/contrib/test_areas.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import unittest
+
+from yt_dlp_plugins.extractor import radiko
+from yt_dlp import YoutubeDL
+
+
+class test_tokens(unittest.TestCase):
+
+ def setUp(self):
+ self.ie = radiko._RadikoBaseIE()
+ ydl = YoutubeDL(auto_init=False)
+ self.ie.set_downloader(ydl)
+
+ def test_area(self):
+ # check areas etc work
+ for i in range(1, 48):
+ area = "JP" + str(i)
+ with self.subTest(f"Negotiating token for {area}", area=area):
+ token = self.ie._negotiate_token(area)
+ self.assertEqual(token.get("X-Radiko-AreaId"), area)
+
+
+if __name__ == '__main__':
+ unittest.main()
+ # may wish to set failfast=True
diff --git a/contrib/test_extractors.py b/contrib/test_extractors.py
new file mode 100755
index 0000000..21800c5
--- /dev/null
+++ b/contrib/test_extractors.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+
+# programmes expire, so i have to update the times in the tests every time i run them
+# but thats a massive ballache, so i end up just not running them, which leads to cockups
+# so, this script has the tests automatically use the latest episode as you run it, by setting dynamically generated time values
+# everything else is always the same so it should be fine lol
+
+
+import datetime
+import os
+import sys
+import unittest
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.insert(0, "/home/g/Downloads/yt-dlp/") # TODO: un-hardcode. has to be the source/git repo because pip doesnt carry the tests
+
+from yt_dlp_plugins.extractor import radiko_time as rtime
+
+MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
+weekdays = {0: "MON", 1: "TUE", 2: "WED", 3: "THU", 4: "FRI", 5: "SAT", 6: "SUN"}
+
+now = rtime.RadikoTime.now(tz = rtime.JST)
+UTC = datetime.timezone.utc
+
+def get_latest_airtimes(now, weekday, hour, minute, duration):
+ days_after_weekday = (7 - (now.weekday() - weekday)) % 7
+ latest_airdate = (now + datetime.timedelta(days=days_after_weekday)).replace(hour=hour, minute=minute, second=0, microsecond=0)
+ if (latest_airdate + duration) > now:
+ latest_airdate -= datetime.timedelta(days=7)
+ return latest_airdate, latest_airdate + duration
+
+def get_test_timefields(airtime, release_time):
+ return {
+ "timestamp": airtime.timestamp(),
+ "release_timestamp": release_time.timestamp(),
+ "upload_date": airtime.astimezone(UTC).strftime("%Y%m%d"),
+ "release_date": release_time.astimezone(UTC).strftime("%Y%m%d"),
+
+ "duration": (release_time - airtime).total_seconds(),
+ }
+
+
+
+
+from yt_dlp_plugins.extractor.radiko import (
+ RadikoTimeFreeIE, RadikoShareIE,
+ RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
+ RadikoRSeasonsIE
+)
+
+from yt_dlp_plugins.extractor.radiko_podcast import (
+ RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
+)
+RadikoTimeFreeIE._TESTS = []
+
+
+
+# TOKYO MOON - interfm - EVERY FRI 2300
+airtime, release_time = get_latest_airtimes(now, FRI, 23, 0, datetime.timedelta(hours=1))
+RadikoTimeFreeIE._TESTS.append({
+ "url": f"https://radiko.jp/#!/ts/INT/{airtime.timestring()}",
+ "info_dict": {
+ "ext": "m4a",
+ "id": f"INT-{airtime.timestring()}",
+
+ **get_test_timefields(airtime, release_time),
+
+ 'title': 'TOKYO MOON',
+ 'description': r're:[\S\s]+Xハッシュタグは「#tokyomoon」$',
+ 'uploader': 'interfm',
+ 'uploader_id': 'INT',
+ 'uploader_url': 'https://www.interfm.co.jp/',
+ 'channel': 'interfm',
+ 'channel_id': 'INT',
+ 'channel_url': 'https://www.interfm.co.jp/',
+ 'thumbnail': 'https://program-static.cf.radiko.jp/ehwtw6mcvy.jpg',
+ 'chapters': list,
+ 'tags': ['松浦俊夫', 'ジャズの魅力を楽しめる'],
+ 'cast': ['松浦\u3000俊夫'],
+ 'series': 'Tokyo Moon',
+ 'live_status': 'was_live',
+ }
+})
+
+
+# late-night/v. early morning show, to test broadcast day handling
+# this should be monday 27:00 / tuesday 03:00
+airtime, release_time = get_latest_airtimes(now, TUE, 3, 0, datetime.timedelta(hours=2))
+RadikoTimeFreeIE._TESTS.append({
+ "url": f"https://radiko.jp/#!/ts/TBS/{airtime.timestring()}",
+ "info_dict": {
+ "ext": "m4a",
+ "id": f"TBS-{airtime.timestring()}",
+
+ **get_test_timefields(airtime, release_time),
+ 'title': 'CITY CHILL CLUB',
+ 'description': r"re:^目を閉じて…リラックスして[\S\s]+chill@tbs.co.jp$",
+ 'uploader': 'TBSラジオ',
+ 'uploader_id': 'TBS',
+ 'uploader_url': 'https://www.tbsradio.jp/',
+ 'channel': 'TBSラジオ',
+ 'channel_id': 'TBS',
+ 'channel_url': 'https://www.tbsradio.jp/',
+ 'thumbnail': 'https://program-static.cf.radiko.jp/nrf8fowbjo.jpg',
+ 'chapters': list,
+ 'tags': ['CCC905', '音楽との出会いが楽しめる', '人気アーティストトーク', '音楽プロデューサー出演', 'ドライブ中におすすめ', '寝る前におすすめ', '学生におすすめ'],
+ 'cast': list,
+ 'series': 'CITY CHILL CLUB',
+ 'live_status': 'was_live',
+ },
+})
+
+
+# testing 29-hour clock handling
+airtime, release_time = get_latest_airtimes(now, WED, 0, 0, datetime.timedelta(minutes=55))
+share_timestring = (airtime - datetime.timedelta(days=1)).strftime("%Y%m%d") + "240000"
+
+RadikoShareIE._TESTS = [{
+ "url": f"http://radiko.jp/share/?sid=FMT&t={share_timestring}",
+ "info_dict": {
+ "live_status": "was_live",
+ "ext": "m4a",
+ "id": f"FMT-{airtime.timestring()}",
+
+ **get_test_timefields(airtime, release_time),
+
+ "title": "JET STREAM",
+ "series": "JET STREAM",
+ "description": r"re:^JET STREAM・・・[\s\S]+https://www.tfm.co.jp/f/jetstream/message$",
+ "chapters": list,
+ "thumbnail": "https://program-static.cf.radiko.jp/greinlrspi.jpg",
+
+ "channel": "TOKYO FM",
+ "channel_id": "FMT",
+ "channel_url": "https://www.tfm.co.jp/",
+ "uploader": "TOKYO FM",
+ "uploader_id": "FMT",
+ "uploader_url": "https://www.tfm.co.jp/",
+
+ "cast": ["福山雅治"],
+ "tags": ["福山雅治", "夜間飛行", "音楽との出会いが楽しめる", "朗読を楽しめる", "寝る前に聴きたい"],
+ },
+ }]
+
+
+
+IEs = [
+ RadikoTimeFreeIE, RadikoShareIE,
+ RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
+ RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
+ RadikoRSeasonsIE,
+]
+
+import test.helper as th
+
+# override to only get testcases from our IEs
+
+def _new_gettestcases(include_onlymatching=False):
+ import yt_dlp.plugins as plugins
+ plugins.load_all_plugins()
+
+ for ie in IEs:
+ yield from ie.get_testcases(include_onlymatching)
+
+def _new_getwebpagetestcases():
+ import yt_dlp.plugins as plugins
+ plugins.load_all_plugins()
+
+ for ie in IEs:
+ for tc in ie.get_webpage_testcases():
+ tc.setdefault('add_ie', []).append('Generic')
+ yield tc
+
+th.gettestcases = _new_gettestcases
+th.getwebpagetestcases = _new_getwebpagetestcases
+
+import test.test_download as td
+
+class TestDownload(td.TestDownload):
+ pass
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/misc/generate_html.py b/misc/generate_html.py
deleted file mode 100755
index 0e15d6a..0000000
--- a/misc/generate_html.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env python3
-import os
-import hashlib
-import re
-
-pip_index = open("index.html", "w")
-
-pip_index.write("""
-
-
- yt-dlp-rajiko pip index
-
-
-
-
-
-
-
-""")
-
-latest_tarball = tarballs[-1]
-latest_wheel = wheels[-1]
-print(latest_tarball, latest_wheel)
-
-os.remove("yt_dlp_rajiko-latest.tar.gz")
-os.symlink(latest_tarball, "yt_dlp_rajiko-latest.tar.gz")
-
-os.remove("yt_dlp_rajiko-latest.whl")
-os.symlink(latest_wheel, "yt_dlp_rajiko-latest.whl")
-
-site_sha256.reverse()
-
-latest_list = site_sha256[:2]
-previous_list = site_sha256[2:]
-
-latest = "\n".join(["", "", "\n".join(latest_list), "", ""])
-
-previous = "\n".join(["", "", "\n".join(previous_list), "", ""])
-
-for i in ["../../index.html", "../../index.ja.html"]:
- with open(i, "r+") as f:
- page = f.read()
-
- page = re.sub(r".+", latest, page, flags=re.DOTALL)
- page = re.sub(r".+", previous, page, flags=re.DOTALL)
-
- f.seek(0)
- f.truncate(0)
- f.write(page)
diff --git a/misc/how to do a release b/misc/how to do a release
deleted file mode 100644
index 6e91e14..0000000
--- a/misc/how to do a release
+++ /dev/null
@@ -1,41 +0,0 @@
-putting this here because i'll forget how to do it otherwise
-
-update the pyproject.toml
-tag it in git, eg v1.0
-
-## build the builds
-python3 -m build
-
-and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
-because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
-
-run script to update the pip index html and the dl/ "latest" symlinks
-this also updates the sha256s on the site
-
-## update the changelog file
-
-write in html, paste into the feed xml like
-make sure to set the link, date
-to get date use:
-git log --pretty --date=rfc2822
-
-include the pip instructions, sha256sum etc
-
-now push to the server
-
-!!NEW!!
-upload to pip proper as well
-go to dl/ dir and do
-twine upload yt_dlp_rajiko-1.x*
-
-
-## update github
-
-paste the changelog output into a github release, upload the new builds
-change link at the bottom to just "below"
-
-post in the radiko thread on 5ch if i can be bothered
-
-and thats probably all
diff --git a/misc/old_generate_changelog.py b/misc/old_generate_changelog.py
deleted file mode 100755
index 1bce073..0000000
--- a/misc/old_generate_changelog.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python3
-import email.utils
-import feedgenerator
-
-def parse_changelog(lines):
- got_version = False
- got_date = False
- got_url = False
- done_remarks = False
- releases = []
- release = {}
- release_remarks = []
- release_changes = []
- current_change = ""
-
- for idx, line in enumerate(lines):
- line = line.rstrip()
-
- if not got_version:
- got_version = True
- release["version"] = line
- continue
-
- if not got_date:
- release["date"] = email.utils.parsedate_to_datetime(line)
- got_date = True
- continue
-
- key, sep, val = line.partition(": ")
- if key in ["url", "sha256", "released"] and val != "":
- release[key] = val
- continue
-
- if not done_remarks:
- if line == "":
- done_remarks = True
- release["remarks"] = release_remarks
- release_remarks = []
- continue
- else:
- release_remarks.append(line)
- continue
-
- if line != "":
- release_changes.append(line.rstrip())
-
- if idx + 1 != len(lines):
- continue
-
- release["changes"] = release_changes
- if release.get("released") != "no":
- releases.append(release)
-
- got_version = False
- got_date = False
- done_remarks = False
- release = {}
- release_changes = []
-
- return releases
-
-def generate_rss_feed(releases):
- feed = feedgenerator.Rss201rev2Feed(
- title="yt-dlp-rajiko changelog",
- description="Notifications for new yt-dlp-rajiko releases, with changelogs",
- link="https://427738.xyz/yt-dlp-rajiko/",
- language="en-GB",
- ttl=180, # 3 hours
- )
-
- for release in releases:
- title = "yt-dlp-rajiko " + release["version"] + " has been released"
- description = ""
- description += "
"
- for remark in release["remarks"]:
- description += remark
- description += " "
- description += "
"
- description += "
This release:
\n"
- description += "
"
- for change in release["changes"]:
- description += "
"
- description += change
- description += "
\n"
- description += "
"
-
- if release.get("url"):
- if release["version"] != "1.0":
- description += "\n
If you use pip, you should be able to upgrade with pip install yt-dlp-rajiko --upgrade --extra-index-url https://427738.xyz/yt-dlp-rajiko/pip/. "
- description += "If you installed manually, you can download the updated .whl from this post's link."
- if release.get("sha256"):
- description += " The SHA256 checksum should be "
- description += release.get("sha256")
- description += "."
- description += "
"
- else:
- description += '\n
Please see the homepage for initial installation instructions.
'
-
- feed.add_item(
- title=title,
- description=description,
- link=release.get("url"),
- pubdate=release["date"]
- )
- return feed
-
-if __name__ == "__main__":
- with open("CHANGELOG") as f:
- releases = parse_changelog(f.readlines())
-
- feed = generate_rss_feed(releases)
- feed_contents = feed.writeString("utf-8")
- feed_contents = feed_contents.replace("\nBI', compression_flag, message_length)
- return header + protobuf_data
-
-def strip_grpc_response(response):
- return response[5:].rpartition(b"grpc-status:")[0]
-
-print("SIGNUP")
-# why do they have to make it so bloody complicated
-
-lsid = ''.join(random.choices('0123456789abcdef', k=32))
-big_funny = ("\n " + lsid).encode()
-
-signup = requests.post("https://api.annex.radiko.jp/radiko.UserService/SignUp", headers={
- 'Origin': 'https://radiko.jp',
- 'Content-Type': 'application/grpc-web+proto',
- 'X-User-Agent': 'grpc-web-javascript/0.1',
- 'X-Grpc-Web': '1',
- }, data=( add_grpc_header(big_funny)),
-)
-
-print(signup.content)
-
-# youre meant to only do the sign up ^ once and then keep your id for later
-# so that you can V sign in and get the token for the API to work
-
-print("SIGNIN")
-
-si=add_grpc_header(protobug.dumps(SignInRequest(
- lsid=lsid,
- area="JP13",
-)))
-
-print(si)
-print(base64.b64encode(si))
-
-signin = requests.post("https://api.annex.radiko.jp/radiko.UserService/SignIn", headers={
- 'Origin': 'https://radiko.jp',
- 'Content-Type': 'application/grpc-web+proto',
- 'X-User-Agent': 'grpc-web-javascript/0.1',
- 'X-Grpc-Web': '1',
-}, data=si)
-
-print(signin.content)
-
-signin_result = protobug.loads(strip_grpc_response(signin.content), SignInResponse)
-
-
-headers = {
- 'Origin': 'https://radiko.jp',
- 'Authorization': f'Bearer {signin_result.jwt}',
- 'x-annex-proto-version': '1.0.0',
- 'Content-Type': 'application/grpc-web+proto',
- 'X-User-Agent': 'grpc-web-javascript/0.1',
- 'X-Grpc-Web': '1',
-}
-
-response = requests.post('https://api.annex.radiko.jp/radiko.PodcastService/ListPodcastEpisodes', headers=headers,
- data=add_grpc_header(protobug.dumps(ListPodcastEpisodesRequest(
- channel_id="0ce1d2d7-5e07-4ec5-901a-d0eacdacc332",
- dontknow=1,
- page_length=200, # site uses 20
-# cursor="ef693874-0ad2-48cc-8c52-ac4de31cbf54" # here you put the id of the last episode you've seen in the list
- )))
-)
-
-print(response)
-
-episodes = strip_grpc_response(response.content)
-
-
-with open("ListPodcastEpisodes.bin", "wb") as f:
- f.write(episodes)
-
-
-@protobug.message
-class Audio:
- revision: protobug.Int32 = protobug.field(1)
- url: protobug.String = protobug.field(2)
- fileSize: protobug.Int64 = protobug.field(3)
- durationSec: protobug.Int64 = protobug.field(4)
- transcoded: protobug.Bool = protobug.field(5)
-
-@protobug.message
-class EpisodeStartAt:
- seconds: protobug.UInt64 = protobug.field(1)
- nanos: protobug.UInt64 = protobug.field(2, default=0)
-
-
-@protobug.message
-class PodcastEpisode:
- id: protobug.String = protobug.field(1)
- workspaceId: protobug.String = protobug.field(2)
- channelId: protobug.String = protobug.field(3)
- title: protobug.String = protobug.field(4)
- description: protobug.String = protobug.field(5)
-
- audio: Audio = protobug.field(8)
- channelImageUrl: protobug.String = protobug.field(16)
- channelTitle: protobug.String = protobug.field(17)
- channelStationName: protobug.String = protobug.field(18)
- channelAuthor: protobug.String = protobug.field(19)
-
- channelThumbnailImageUrl: protobug.String = protobug.field(21)
- channelStationType: protobug.UInt32 = protobug.field(22)
- startAt: EpisodeStartAt = protobug.field(27)
- isEnabled: protobug.Bool = protobug.field(29)
- hasTranscription: protobug.Bool = protobug.field(32)
-
- imageUrl: protobug.String = protobug.field(7, default=None)
- thumbnailImageUrl: protobug.String = protobug.field(20, default=None)
-
-@protobug.message
-class ListPodcastEpisodesResponse:
- episodes: list[PodcastEpisode] = protobug.field(1)
- hasNextPage: protobug.Bool = protobug.field(2, default=False)
-
-
-episodes_response = protobug.loads(episodes, ListPodcastEpisodesResponse)
-
-print(episodes_response)
-
-for e in episodes_response.episodes:
- print(e.title, e.id)
-print(episodes_response.hasNextPage)
diff --git a/misc/randominfo.py b/misc/randominfo.py
deleted file mode 100755
index bdb7660..0000000
--- a/misc/randominfo.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env python3
-from yt_dlp_plugins.extractor import radiko
-from yt_dlp import YoutubeDL
-
-
-ie = radiko._RadikoBaseIE()
-ydl = YoutubeDL(auto_init=False)
-ie.set_downloader(ydl)
-
-info = ie._generate_random_info()
-print("random device info")
-print(info)
diff --git a/misc/streammon.py b/misc/streammon.py
deleted file mode 100755
index 8f52bb4..0000000
--- a/misc/streammon.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-# monitor stream APIs for any changes, so I can check they don't break anything
-# run via cronjob every now and then
-
-import difflib
-import os
-import sys
-import xml.etree.ElementTree as ET
-from datetime import datetime
-
-import requests
-
-s = requests.Session()
-
-DISCORD_WEBHOOK = "PUT WEBHOOK HERE"
-STREAMS_API = "https://radiko.jp/v3/station/stream/{device}/{station}.xml"
-
-if len(sys.argv) > 1:
- PATH = sys.argv[1]
-else:
- PATH = ""
-
-devices = ('pc_html5', 'aSmartPhone7a', 'aSmartPhone8')
-stations = ('FMT', 'CCL', 'NORTHWAVE', 'TBS')
-
-def format_xml(txt):
- root = ET.fromstring(txt)
- res = ""
- for el in root.findall("url"):
- res += el.find("playlist_create_url").text
- for k, v in el.attrib.items():
- res += f" {k}:{v}"
-
- res += "\n"
- return res
-
-for device in devices:
- for station in stations:
- url = STREAMS_API.format(device=device, station=station)
- now_response = s.get(url)
- now = now_response.text
- now_modified = now_response.headers["last-modified"]
- now_datetime = datetime.strptime(now_modified, "%a, %d %b %Y %H:%M:%S %Z")
-
-
- filename = f"{PATH}{station}-{device}.xml"
- with open(filename, "a+") as f:
- f.seek(0)
- past = f.read()
-
- modtime = datetime.fromtimestamp(os.path.getmtime(filename))
- diff = difflib.unified_diff(
- format_xml(past).splitlines(), format_xml(now).splitlines(),
- fromfile=url, tofile=url,
- fromfiledate=str(modtime), tofiledate=str(now_datetime.now()),
- )
-
- diff_str = "\n".join(diff)
- if diff_str != "":
- f.truncate(0)
- f.write(now)
-
- s.post(DISCORD_WEBHOOK, json={
- "content": f"**Streams changed: {station} {device}**\n" + "\n".join(("```diff", diff_str, "```")),
- })
- os.utime(filename, (now_datetime.timestamp(), now_datetime.timestamp()))
diff --git a/misc/test_areas.py b/misc/test_areas.py
deleted file mode 100755
index ba6475f..0000000
--- a/misc/test_areas.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-
-from yt_dlp_plugins.extractor import radiko
-from yt_dlp import YoutubeDL
-
-
-class test_tokens(unittest.TestCase):
-
- def setUp(self):
- self.ie = radiko._RadikoBaseIE()
- ydl = YoutubeDL(auto_init=False)
- self.ie.set_downloader(ydl)
-
- def test_area(self):
- # check areas etc work
- for i in range(1, 48):
- area = "JP" + str(i)
- with self.subTest(f"Negotiating token for {area}", area=area):
- token = self.ie._negotiate_token(area)
- self.assertEqual(token.get("X-Radiko-AreaId"), area)
-
-
-if __name__ == '__main__':
- unittest.main()
- # may wish to set failfast=True
diff --git a/misc/test_extractors.py b/misc/test_extractors.py
deleted file mode 100755
index 21800c5..0000000
--- a/misc/test_extractors.py
+++ /dev/null
@@ -1,183 +0,0 @@
-#!/usr/bin/env python3
-
-# programmes expire, so i have to update the times in the tests every time i run them
-# but thats a massive ballache, so i end up just not running them, which leads to cockups
-# so, this script has the tests automatically use the latest episode as you run it, by setting dynamically generated time values
-# everything else is always the same so it should be fine lol
-
-
-import datetime
-import os
-import sys
-import unittest
-
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-sys.path.insert(0, "/home/g/Downloads/yt-dlp/") # TODO: un-hardcode. has to be the source/git repo because pip doesnt carry the tests
-
-from yt_dlp_plugins.extractor import radiko_time as rtime
-
-MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
-weekdays = {0: "MON", 1: "TUE", 2: "WED", 3: "THU", 4: "FRI", 5: "SAT", 6: "SUN"}
-
-now = rtime.RadikoTime.now(tz = rtime.JST)
-UTC = datetime.timezone.utc
-
-def get_latest_airtimes(now, weekday, hour, minute, duration):
- days_after_weekday = (7 - (now.weekday() - weekday)) % 7
- latest_airdate = (now + datetime.timedelta(days=days_after_weekday)).replace(hour=hour, minute=minute, second=0, microsecond=0)
- if (latest_airdate + duration) > now:
- latest_airdate -= datetime.timedelta(days=7)
- return latest_airdate, latest_airdate + duration
-
-def get_test_timefields(airtime, release_time):
- return {
- "timestamp": airtime.timestamp(),
- "release_timestamp": release_time.timestamp(),
- "upload_date": airtime.astimezone(UTC).strftime("%Y%m%d"),
- "release_date": release_time.astimezone(UTC).strftime("%Y%m%d"),
-
- "duration": (release_time - airtime).total_seconds(),
- }
-
-
-
-
-from yt_dlp_plugins.extractor.radiko import (
- RadikoTimeFreeIE, RadikoShareIE,
- RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
- RadikoRSeasonsIE
-)
-
-from yt_dlp_plugins.extractor.radiko_podcast import (
- RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
-)
-RadikoTimeFreeIE._TESTS = []
-
-
-
-# TOKYO MOON - interfm - EVERY FRI 2300
-airtime, release_time = get_latest_airtimes(now, FRI, 23, 0, datetime.timedelta(hours=1))
-RadikoTimeFreeIE._TESTS.append({
- "url": f"https://radiko.jp/#!/ts/INT/{airtime.timestring()}",
- "info_dict": {
- "ext": "m4a",
- "id": f"INT-{airtime.timestring()}",
-
- **get_test_timefields(airtime, release_time),
-
- 'title': 'TOKYO MOON',
- 'description': r're:[\S\s]+Xハッシュタグは「#tokyomoon」$',
- 'uploader': 'interfm',
- 'uploader_id': 'INT',
- 'uploader_url': 'https://www.interfm.co.jp/',
- 'channel': 'interfm',
- 'channel_id': 'INT',
- 'channel_url': 'https://www.interfm.co.jp/',
- 'thumbnail': 'https://program-static.cf.radiko.jp/ehwtw6mcvy.jpg',
- 'chapters': list,
- 'tags': ['松浦俊夫', 'ジャズの魅力を楽しめる'],
- 'cast': ['松浦\u3000俊夫'],
- 'series': 'Tokyo Moon',
- 'live_status': 'was_live',
- }
-})
-
-
-# late-night/v. early morning show, to test broadcast day handling
-# this should be monday 27:00 / tuesday 03:00
-airtime, release_time = get_latest_airtimes(now, TUE, 3, 0, datetime.timedelta(hours=2))
-RadikoTimeFreeIE._TESTS.append({
- "url": f"https://radiko.jp/#!/ts/TBS/{airtime.timestring()}",
- "info_dict": {
- "ext": "m4a",
- "id": f"TBS-{airtime.timestring()}",
-
- **get_test_timefields(airtime, release_time),
- 'title': 'CITY CHILL CLUB',
- 'description': r"re:^目を閉じて…リラックスして[\S\s]+chill@tbs.co.jp$",
- 'uploader': 'TBSラジオ',
- 'uploader_id': 'TBS',
- 'uploader_url': 'https://www.tbsradio.jp/',
- 'channel': 'TBSラジオ',
- 'channel_id': 'TBS',
- 'channel_url': 'https://www.tbsradio.jp/',
- 'thumbnail': 'https://program-static.cf.radiko.jp/nrf8fowbjo.jpg',
- 'chapters': list,
- 'tags': ['CCC905', '音楽との出会いが楽しめる', '人気アーティストトーク', '音楽プロデューサー出演', 'ドライブ中におすすめ', '寝る前におすすめ', '学生におすすめ'],
- 'cast': list,
- 'series': 'CITY CHILL CLUB',
- 'live_status': 'was_live',
- },
-})
-
-
-# testing 29-hour clock handling
-airtime, release_time = get_latest_airtimes(now, WED, 0, 0, datetime.timedelta(minutes=55))
-share_timestring = (airtime - datetime.timedelta(days=1)).strftime("%Y%m%d") + "240000"
-
-RadikoShareIE._TESTS = [{
- "url": f"http://radiko.jp/share/?sid=FMT&t={share_timestring}",
- "info_dict": {
- "live_status": "was_live",
- "ext": "m4a",
- "id": f"FMT-{airtime.timestring()}",
-
- **get_test_timefields(airtime, release_time),
-
- "title": "JET STREAM",
- "series": "JET STREAM",
- "description": r"re:^JET STREAM・・・[\s\S]+https://www.tfm.co.jp/f/jetstream/message$",
- "chapters": list,
- "thumbnail": "https://program-static.cf.radiko.jp/greinlrspi.jpg",
-
- "channel": "TOKYO FM",
- "channel_id": "FMT",
- "channel_url": "https://www.tfm.co.jp/",
- "uploader": "TOKYO FM",
- "uploader_id": "FMT",
- "uploader_url": "https://www.tfm.co.jp/",
-
- "cast": ["福山雅治"],
- "tags": ["福山雅治", "夜間飛行", "音楽との出会いが楽しめる", "朗読を楽しめる", "寝る前に聴きたい"],
- },
- }]
-
-
-
-IEs = [
- RadikoTimeFreeIE, RadikoShareIE,
- RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
- RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
- RadikoRSeasonsIE,
-]
-
-import test.helper as th
-
-# override to only get testcases from our IEs
-
-def _new_gettestcases(include_onlymatching=False):
- import yt_dlp.plugins as plugins
- plugins.load_all_plugins()
-
- for ie in IEs:
- yield from ie.get_testcases(include_onlymatching)
-
-def _new_getwebpagetestcases():
- import yt_dlp.plugins as plugins
- plugins.load_all_plugins()
-
- for ie in IEs:
- for tc in ie.get_webpage_testcases():
- tc.setdefault('add_ie', []).append('Generic')
- yield tc
-
-th.gettestcases = _new_gettestcases
-th.getwebpagetestcases = _new_getwebpagetestcases
-
-import test.test_download as td
-
-class TestDownload(td.TestDownload):
- pass
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 0566b6d..5bda1de 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -437,7 +437,7 @@ class RadikoLiveIE(_RadikoBaseIE):
class RadikoTimeFreeIE(_RadikoBaseIE):
_NETRC_MACHINE = "rajiko"
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/ts/(?P[A-Z0-9-_]+)/(?P\d+)"
- # TESTS use a custom-ish script that updates the airdates automatically, see misc/test_extractors.py
+ # TESTS use a custom-ish script that updates the airdates automatically, see contrib/test_extractors.py
def _perform_login(self, username, password):
try:
--
cgit v1.2.3-70-g09d2
From bcb5df38d71f2b6d2092797201ad62638b6d3ef0 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 15:17:26 +0100
Subject: disable stream blacklist for live
ref https://github.com/garret1317/yt-dlp-rajiko/issues/29#issuecomment-3289577318
github issue #29
---
yt_dlp_plugins/extractor/radiko.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 5bda1de..ad5c77f 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -302,7 +302,9 @@ class _RadikoBaseIE(InfoExtractor):
entry_protocol = 'm3u8'
format_note=[]
- if domain in self._DOESNT_WORK_WITH_FFMPEG and do_blacklist_streams:
+ if timefree and domain in self._DOESNT_WORK_WITH_FFMPEG and do_blacklist_streams:
+ # TODO: remove this completely
+ # https://github.com/garret1317/yt-dlp-rajiko/issues/29
self.write_debug(f"skipping {domain} (known not working)")
continue
if domain in self._DELIVERED_ONDEMAND:
--
cgit v1.2.3-70-g09d2
From 98bfe822517eacd942f8a460e00ae8a30bb8d5e3 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 15:40:39 +0100
Subject: put protobug as a full dependency so everyone who installs with pip
gets it
it's pure python with no deps + trusted dev so it's fine
---
pyproject.toml | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index d92abe7..d4f6c04 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,14 +16,15 @@ classifiers = [
"Environment :: Plugins",
]
+dependencies = [
+ "protobug"
+]
+
[project.urls]
Homepage = "https://427738.xyz/yt-dlp-rajiko/"
"Source Code" = "https://github.com/garret1317/yt-dlp-rajiko/"
"Release Notes" = "https://427738.xyz/yt-dlp-rajiko/CHANGELOG.xml"
-[project.optional-dependencies]
-protobuf = ["protobug"]
-
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
--
cgit v1.2.3-70-g09d2
From a9b3c5f595774f016137323ddc10e9a303768817 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 15:41:58 +0100
Subject: the build thing wants me to put a SPDX licence id instead of the
classifier
even though the classifier is nicer because it's human readable as well
but it won't let me have both in there at once
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index d4f6c04..dc182bf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,6 +4,7 @@ version = "1.7"
description = "improved radiko.jp extractor for yt-dlp (fast and areafree)"
readme = "README.md"
+license = "0BSD"
license-files = ["LICENCE"]
authors = [
@@ -12,7 +13,6 @@ authors = [
requires-python = ">=3.8"
classifiers = [
- "License :: OSI Approved :: Zero-Clause BSD (0BSD)",
"Environment :: Plugins",
]
--
cgit v1.2.3-70-g09d2
From e1c74fc8eb027d40acc9d5114f352670b435c23b Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 17:12:00 +0100
Subject: clean up stray imports in protobufs file
---
yt_dlp_plugins/extractor/radiko_protobufs.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko_protobufs.py b/yt_dlp_plugins/extractor/radiko_protobufs.py
index ff4531e..4eb4f8b 100755
--- a/yt_dlp_plugins/extractor/radiko_protobufs.py
+++ b/yt_dlp_plugins/extractor/radiko_protobufs.py
@@ -4,11 +4,8 @@ try:
except ImportError:
protobug = None
-import base64
import struct
-
import random
-import requests
if protobug: # i suppose it works lmao
--
cgit v1.2.3-70-g09d2
From 8337d2b164759181777f64f12b985e4fad769ab7 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 17:28:08 +0100
Subject: Add support for bundled protobug library
github:
closes #29
---
yt_dlp_plugins/extractor/radiko_dependencies.py | 29 +++++++++++++++++++++++++
yt_dlp_plugins/extractor/radiko_podcast.py | 7 +++---
yt_dlp_plugins/extractor/radiko_protobufs.py | 7 ++----
3 files changed, 34 insertions(+), 9 deletions(-)
create mode 100644 yt_dlp_plugins/extractor/radiko_dependencies.py
diff --git a/yt_dlp_plugins/extractor/radiko_dependencies.py b/yt_dlp_plugins/extractor/radiko_dependencies.py
new file mode 100644
index 0000000..769a5e3
--- /dev/null
+++ b/yt_dlp_plugins/extractor/radiko_dependencies.py
@@ -0,0 +1,29 @@
+# Bundle importing code Copyright (c) 2021-2022 Grub4K, from yt-dont-lock-p.
+# https://github.com/Grub4K/yt-dont-lock-p/blob/ff3b6e1d42ce8584153ae27544d2c05b50ab5954/yt_dlp_plugins/postprocessor/yt_dont_lock_p/__init__.py#L23-L46
+# Used under 0BSD with permission
+
+# https://discord.com/channels/807245652072857610/1112613156934668338/1416816007732920430 (yt-dlp discord server, https://discord.gg/H5MNcFW63r )
+# [17:00] garret1317: @Grub4K can i pinch your MIT-licensed dependency bundling code to use in my 0BSD-licensed plugin?
+# I will credit of course but i can't require that anyone else does the same
+# (Any response to this message will be considered a written consent or refusal of the request)
+# [17:04] Grub4K: Feel free to use that part under 0BSD
+# [17:05] garret1317: 👍 cheers
+
+try:
+ import protobug
+except ImportError:
+ import sys
+ from pathlib import Path
+
+ # Try importing from zip file bundle
+ search_path = str(Path(__file__).parent.parent)
+ sys.path.append(search_path)
+ try:
+ import protobug
+ except ImportError:
+ protobug = None
+ except Exception:
+ protobug = None
+
+ finally:
+ sys.path.remove(search_path)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index 84bc288..a984be3 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -7,11 +7,10 @@ from yt_dlp.utils import (
)
import dataclasses
-try:
- import protobug
+
+from yt_dlp_plugins.extractor.radiko_dependencies import protobug
+if protobug:
import yt_dlp_plugins.extractor.radiko_protobufs as pb
-except ImportError:
- protobug = None
class _RadikoPodcastBaseIE(InfoExtractor):
diff --git a/yt_dlp_plugins/extractor/radiko_protobufs.py b/yt_dlp_plugins/extractor/radiko_protobufs.py
index 4eb4f8b..a8bbec1 100755
--- a/yt_dlp_plugins/extractor/radiko_protobufs.py
+++ b/yt_dlp_plugins/extractor/radiko_protobufs.py
@@ -1,12 +1,9 @@
#!/usr/bin/env python3
-try:
- import protobug
-except ImportError:
- protobug = None
-
import struct
import random
+from yt_dlp_plugins.extractor.radiko_dependencies import protobug
+
if protobug: # i suppose it works lmao
--
cgit v1.2.3-70-g09d2
From 3ca06221b455747c36dbba9e226ec09df09d2e96 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 17:28:21 +0100
Subject: add bundle stuff to gitignore
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index 23d37d7..cd9177d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,7 @@ __pycache__
*.pyc
wiki/
dist/
+build/
+bundle/
*.m4a*
*.m3u8
--
cgit v1.2.3-70-g09d2
From 68660d1dfe550be845033cd0301cd2e46144afdd Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 17:33:57 +0100
Subject: Add instructions for obtaining protobug in the warning message
---
yt_dlp_plugins/extractor/radiko_podcast.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index a984be3..10579b5 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -109,7 +109,7 @@ class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
cursor = episode.id
yield self._extract_episode(dataclasses.asdict(episode))
else:
- self.report_warning(f'Only extracting the latest {len(episode_list_response["episodesList"])} episodes. Install protobug for more.')
+ self.report_warning(f'protobug is required to extract more than the latest {len(episode_list_response["episodesList"])} episodes.\nIf you installed yt-dlp-rajiko manually, use the .zip bundle instead. If you installed with pip, install protobug as well.')
return {
"_type": "playlist",
--
cgit v1.2.3-70-g09d2
From ab4558f54b1740c949d119cd2ae748dd797a566a Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 19:25:34 +0100
Subject: release v1.8
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index dc182bf..1fe2143 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.7"
+version = "1.8"
description = "improved radiko.jp extractor for yt-dlp (fast and areafree)"
readme = "README.md"
--
cgit v1.2.3-70-g09d2
From f0067f1e256446727fdcf0eabeffea471ea87cfc Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 14 Sep 2025 21:07:09 +0100
Subject: update readme to link to plugin bundle
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 1546867..af01297 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ yt-dlp-rajiko is an improved [radiko.jp](https://radiko.jp) extractor plugin for
## Installation
-[Download the Python wheel](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.whl) or `pip install yt-dlp-rajiko`
+[Download the plugin bundle](https://427738.xyz/yt-dlp-rajiko/dl/yt_dlp_rajiko-latest.bundle.zip) or `pip install yt-dlp-rajiko`
Requires yt-dlp 2025.02.19 or above.
@@ -12,7 +12,7 @@ Use the pip command if you installed yt-dlp with pip. If you installed
yt-dlp with `pipx`, use `pipx inject yt-dlp yt-dlp-rajiko` to install
the plugin in yt-dlp's environment.
-Otherwise, download the wheel, and place it in one of these locations:
+Otherwise, download the plugin bundle, and place it in one of these locations:
- `~/.config/yt-dlp/plugins/` (on Linux and Mac)
- `%appdata%/yt-dlp/plugins/` (on Windows)
@@ -21,6 +21,7 @@ Otherwise, download the wheel, and place it in one of these locations:
documentation](https://github.com/yt-dlp/yt-dlp#installing-plugins).
You'll have to create those folders if they don't already exist.
+There is no need to unzip the plugin bundle.
More information about yt-dlp plugins is available from [yt-dlp's documentation](https://github.com/yt-dlp/yt-dlp#plugins).
--
cgit v1.2.3-70-g09d2
From 25d4c20be556825a5b3e2b6a66c719ad2c5d8c1f Mon Sep 17 00:00:00 2001
From: garret1317
Date: Thu, 18 Sep 2025 23:11:24 +0100
Subject: clarify protobug error message
---
yt_dlp_plugins/extractor/radiko_podcast.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index 10579b5..21e3dfd 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -109,7 +109,7 @@ class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
cursor = episode.id
yield self._extract_episode(dataclasses.asdict(episode))
else:
- self.report_warning(f'protobug is required to extract more than the latest {len(episode_list_response["episodesList"])} episodes.\nIf you installed yt-dlp-rajiko manually, use the .zip bundle instead. If you installed with pip, install protobug as well.')
+ self.report_warning(f'protobug is required to extract more than the latest {len(episode_list_response["episodesList"])} episodes.\nIf you installed yt-dlp-rajiko manually (with the .whl), use the .zip bundle instead. If you installed with pip, pip install protobug .')
return {
"_type": "playlist",
--
cgit v1.2.3-70-g09d2
From ca5cf0703d7a3171c7c9e5b1a9c89a96e0b8d6e8 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 00:48:09 +0100
Subject: add podcast search IE
---
yt_dlp_plugins/extractor/radiko_podcast.py | 41 ++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index 21e3dfd..67d6475 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -1,12 +1,16 @@
from yt_dlp.extractor.common import InfoExtractor
from yt_dlp.utils import (
clean_html,
+ OnDemandPagedList,
+ parse_qs,
traverse_obj,
+ update_url_query,
url_or_none,
str_or_none,
)
import dataclasses
+import random
from yt_dlp_plugins.extractor.radiko_dependencies import protobug
if protobug:
@@ -123,3 +127,40 @@ class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
}),
"entries": entries(),
}
+
+
+class RadikoPodcastSearchIE(InfoExtractor):
+ _VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/search/podcast/(?:timeshift|live)\?"
+
+ def _pagefunc(self, url, idx):
+ url = update_url_query(url, {"pageIdx": idx})
+ data = self._download_json(url, None, note=f"Downloading page {idx+1}")
+
+ results = []
+ for channel in data.get("channels"):
+ results.append(
+ self.url_result(
+ channel.get("channelUrl"),
+ id=channel.get("id"),
+ ie=RadikoPodcastChannelIE,
+ )
+ )
+ return results
+
+
+ def _real_extract(self, url):
+ # hack away the # so urllib.parse will work (same as normal RadikoSearchIE)
+ url = url.replace("/#!/", "/!/", 1)
+ queries = parse_qs(url)
+
+ keywords = traverse_obj(queries, ("key", 0))
+ search_url = update_url_query("https://api.annex-cf.radiko.jp/v1/podcasts/channels/search_with_keywords_by_offset", {
+ "keywords": keywords,
+ "uid": "".join(random.choices("0123456789abcdef", k=32)),
+ "limit": 50, # result limit. the actual limit before the api errors is 5000, but that seems a bit rude so i'll leave as 50 like the radio one
+ })
+
+ return self.playlist_result(
+ OnDemandPagedList(lambda idx: self._pagefunc(search_url, idx), 50),
+ title=keywords,
+ )
--
cgit v1.2.3-70-g09d2
From d9c78c18dbc3f6afc3994cd0060ab7b2f55d3af0 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 01:22:49 +0100
Subject: Fix search filtering + add support for podcast filtering
---
yt_dlp_plugins/extractor/radiko.py | 44 +++++++++++++++++++++++++++++++-------
1 file changed, 36 insertions(+), 8 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index ad5c77f..6654b57 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -19,6 +19,7 @@ from yt_dlp.utils import (
url_or_none,
update_url_query,
)
+from yt_dlp_plugins.extractor.radiko_podcast import RadikoPodcastSearchIE
import yt_dlp_plugins.extractor.radiko_time as rtime
import yt_dlp_plugins.extractor.radiko_hacks as hacks
@@ -566,7 +567,7 @@ class RadikoTimeFreeIE(_RadikoBaseIE):
class RadikoSearchIE(InfoExtractor):
- _VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/search/(?:timeshift|live|history)\?"
+ _VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/search/(?:radio/)?(?:timeshift|live|history)\?"
_TESTS = [{
# timefree, specific area
"url": "https://radiko.jp/#!/search/live?key=city%20chill%20club&filter=past&start_day=&end_day=®ion_id=&area_id=JP13&cul_area_id=JP13&page_idx=0",
@@ -609,23 +610,50 @@ class RadikoSearchIE(InfoExtractor):
results.append(
self.url_result(
f"https://radiko.jp/#!/ts/{station}/{timestring}",
- id=join_nonempty(station, timestring)
+ id=join_nonempty(station, timestring),
+ ie=RadikoTimeFreeIE,
)
)
return results
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
+ url = url.replace("/#!/", "/!/", 1)
queries = parse_qs(url)
+ key = traverse_obj(queries, ("key", 0))
- if queries.get("cul_area_id"):
- queries["cur_area_id"] = queries.pop("cul_area_id")
# site used to use "cul_area_id" in the search url, now it uses "cur_area_id" (with an r)
# and outright rejects the old one with HTTP Error 415: Unsupported Media Type
+ if queries.get("cul_area_id"):
+ queries["cur_area_id"] = queries.pop("cul_area_id")
+
+ filter_str = ""
+ if queries.get("filter"):
+ filter_set = set(queries["filter"][0].split("|"))
+ del queries["filter"]
+
+ if filter_set == {"channel"}:
+ podcast_search_url = update_url_query(
+ "https://radiko.jp/!/search/podcast/live", {"key": key}
+ ).replace("!", "#!", 1) # same shit with urllib.parse
+ return self.url_result(podcast_search_url, ie=RadikoPodcastSearchIE)
+
+ if "channel" in filter_set:
+ self.report_warning("Skipping podcasts. If you really want EVERY EPISODE of EVERY RESULT, set your search filter to Podcasts only.")
+
+ filter_set.discard("channel")
+ if filter_set == {"future", "past"}:
+ filter_str = ""
+ else:
+ filter_str = "|".join(filter_set) # there should be only one filter now, so this should be the same as filter_set[0]
+ # but if there's more than one, then we should at least try to pass it through as-is, in the hope that it works
+ if len(filter_set) != 1:
+ # but also kick up a stink about it so it's clear it probably won't
+ self.report_warning("Your search has an unknkown combination of filters, so this request will probably fail!")
search_url = update_url_query("https://api.annex-cf.radiko.jp/v1/programs/legacy/perl/program/search", {
**queries,
+ "filter": filter_str,
"uid": "".join(random.choices("0123456789abcdef", k=32)),
"app_id": "pc",
"row_limit": 50, # higher row_limit = more results = less requests = more good
@@ -633,20 +661,20 @@ class RadikoSearchIE(InfoExtractor):
results = OnDemandPagedList(lambda idx: self._pagefunc(search_url, idx), 50)
- key = traverse_obj(queries, ("key", 0))
day = traverse_obj(queries, ("start_day", 0)) or "all"
region = traverse_obj(queries, ("region_id", 0)) or traverse_obj(queries, ("area_id", 0))
- status_filter = traverse_obj(queries, ("filter", 0)) or "all"
+ status_filter = filter_str or "all"
playlist_id = join_nonempty(key, status_filter, day, region)
return {
"_type": "playlist",
- "title": traverse_obj(queries, ("key", 0)),
+ "title": key,
"id": playlist_id,
"entries": results,
}
+
class RadikoShareIE(InfoExtractor):
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/share/"
--
cgit v1.2.3-70-g09d2
From 291fd64ad2ca9d0d2b95a6812be3eebdb2012e6c Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 01:25:54 +0100
Subject: >forgot to add SearchIE tests
---
contrib/test_extractors.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/contrib/test_extractors.py b/contrib/test_extractors.py
index 21800c5..1ef63d0 100755
--- a/contrib/test_extractors.py
+++ b/contrib/test_extractors.py
@@ -45,7 +45,7 @@ def get_test_timefields(airtime, release_time):
from yt_dlp_plugins.extractor.radiko import (
RadikoTimeFreeIE, RadikoShareIE,
RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
- RadikoRSeasonsIE
+ RadikoSearchIE, RadikoRSeasonsIE
)
from yt_dlp_plugins.extractor.radiko_podcast import (
@@ -148,7 +148,7 @@ IEs = [
RadikoTimeFreeIE, RadikoShareIE,
RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
- RadikoRSeasonsIE,
+ RadikoSearchIE, RadikoRSeasonsIE,
]
import test.helper as th
--
cgit v1.2.3-70-g09d2
From b8807109ba7e4515ffc5d6d3d0fcf8888acd13a3 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 01:28:17 +0100
Subject: Add PodcastSearch test
---
contrib/test_extractors.py | 4 ++--
yt_dlp_plugins/extractor/radiko_podcast.py | 9 +++++++++
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/contrib/test_extractors.py b/contrib/test_extractors.py
index 1ef63d0..0b505b8 100755
--- a/contrib/test_extractors.py
+++ b/contrib/test_extractors.py
@@ -49,7 +49,7 @@ from yt_dlp_plugins.extractor.radiko import (
)
from yt_dlp_plugins.extractor.radiko_podcast import (
- RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
+ RadikoPodcastEpisodeIE, RadikoPodcastChannelIE, RadikoPodcastSearchIE,
)
RadikoTimeFreeIE._TESTS = []
@@ -148,7 +148,7 @@ IEs = [
RadikoTimeFreeIE, RadikoShareIE,
RadikoLiveIE, RadikoPersonIE, RadikoStationButtonIE,
RadikoPodcastEpisodeIE, RadikoPodcastChannelIE,
- RadikoSearchIE, RadikoRSeasonsIE,
+ RadikoSearchIE, RadikoPodcastSearchIE, RadikoRSeasonsIE,
]
import test.helper as th
diff --git a/yt_dlp_plugins/extractor/radiko_podcast.py b/yt_dlp_plugins/extractor/radiko_podcast.py
index 67d6475..27b91ad 100644
--- a/yt_dlp_plugins/extractor/radiko_podcast.py
+++ b/yt_dlp_plugins/extractor/radiko_podcast.py
@@ -131,6 +131,14 @@ class RadikoPodcastChannelIE(_RadikoPodcastBaseIE):
class RadikoPodcastSearchIE(InfoExtractor):
_VALID_URL = r"https?://(?:www\.)?radiko\.jp/#!/search/podcast/(?:timeshift|live)\?"
+ _TESTS = [{
+ "url": "https://radiko.jp/#!/search/podcast/live?key=ドラマ",
+ "playlist_mincount": 51,
+ "info_dict": {
+ "id": "ドラマ",
+ "title": "ドラマ",
+ },
+ }]
def _pagefunc(self, url, idx):
url = update_url_query(url, {"pageIdx": idx})
@@ -163,4 +171,5 @@ class RadikoPodcastSearchIE(InfoExtractor):
return self.playlist_result(
OnDemandPagedList(lambda idx: self._pagefunc(search_url, idx), 50),
title=keywords,
+ id=keywords, # i have to put some kind of id or the tests fail
)
--
cgit v1.2.3-70-g09d2
From e98426f316bf9d60d6e131a9163c8b872d663a92 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 11:18:02 +0100
Subject: oh, absence of a filter means every option doesnt it
---
yt_dlp_plugins/extractor/radiko.py | 35 ++++++++++++++++++-----------------
1 file changed, 18 insertions(+), 17 deletions(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 6654b57..ec260ab 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -627,29 +627,30 @@ class RadikoSearchIE(InfoExtractor):
if queries.get("cul_area_id"):
queries["cur_area_id"] = queries.pop("cul_area_id")
- filter_str = ""
if queries.get("filter"):
filter_set = set(queries["filter"][0].split("|"))
del queries["filter"]
+ else:
+ filter_set = {"future", "past", "channel"}
- if filter_set == {"channel"}:
- podcast_search_url = update_url_query(
- "https://radiko.jp/!/search/podcast/live", {"key": key}
- ).replace("!", "#!", 1) # same shit with urllib.parse
- return self.url_result(podcast_search_url, ie=RadikoPodcastSearchIE)
+ if filter_set == {"channel"}:
+ podcast_search_url = update_url_query(
+ "https://radiko.jp/!/search/podcast/live", {"key": key}
+ ).replace("!", "#!", 1) # same shit with urllib.parse
+ return self.url_result(podcast_search_url, ie=RadikoPodcastSearchIE)
- if "channel" in filter_set:
- self.report_warning("Skipping podcasts. If you really want EVERY EPISODE of EVERY RESULT, set your search filter to Podcasts only.")
+ if "channel" in filter_set:
+ self.report_warning("Skipping podcasts. If you really want EVERY EPISODE of EVERY RESULT, set your search filter to Podcasts only.")
+ filter_set.discard("channel")
- filter_set.discard("channel")
- if filter_set == {"future", "past"}:
- filter_str = ""
- else:
- filter_str = "|".join(filter_set) # there should be only one filter now, so this should be the same as filter_set[0]
- # but if there's more than one, then we should at least try to pass it through as-is, in the hope that it works
- if len(filter_set) != 1:
- # but also kick up a stink about it so it's clear it probably won't
- self.report_warning("Your search has an unknkown combination of filters, so this request will probably fail!")
+ if filter_set == {"future", "past"}:
+ filter_str = ""
+ else:
+ filter_str = "|".join(filter_set) # there should be only one filter now, so this should be the same as filter_set[0]
+ # but if there's more than one, then we should at least try to pass it through as-is, in the hope that it works
+ if len(filter_set) != 1:
+ # but also kick up a stink about it so it's clear it probably won't
+ self.report_warning("Your search has an unknkown combination of filters, so this request will probably fail!")
search_url = update_url_query("https://api.annex-cf.radiko.jp/v1/programs/legacy/perl/program/search", {
**queries,
--
cgit v1.2.3-70-g09d2
From adbc8de0b8707ef8af14b545d11b99317d5e171e Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 11:39:07 +0100
Subject: Fix search test
---
yt_dlp_plugins/extractor/radiko.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index ec260ab..8b6b623 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -592,6 +592,7 @@ class RadikoSearchIE(InfoExtractor):
"id": "ニュース-all-all",
"title": "ニュース"
},
+ 'expected_warnings': ['Skipping podcasts. If you really want EVERY EPISODE of EVERY RESULT, set your search filter to Podcasts only.'],
}]
def _strip_date(self, date):
--
cgit v1.2.3-70-g09d2
From 4678f793475241d395157eaee6f375a8b1f24976 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 11:50:30 +0100
Subject: fix typo
---
yt_dlp_plugins/extractor/radiko.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 8b6b623..922c0eb 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -651,7 +651,7 @@ class RadikoSearchIE(InfoExtractor):
# but if there's more than one, then we should at least try to pass it through as-is, in the hope that it works
if len(filter_set) != 1:
# but also kick up a stink about it so it's clear it probably won't
- self.report_warning("Your search has an unknkown combination of filters, so this request will probably fail!")
+ self.report_warning("Your search has an unknown combination of filters, so this request will probably fail!")
search_url = update_url_query("https://api.annex-cf.radiko.jp/v1/programs/legacy/perl/program/search", {
**queries,
--
cgit v1.2.3-70-g09d2
From 0e5861f648810ad1670fbe06f71346f8d7be73a3 Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 19 Sep 2025 12:08:33 +0100
Subject: release v1.9
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 1fe2143..780b641 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "yt-dlp-rajiko"
-version = "1.8"
+version = "1.9"
description = "improved radiko.jp extractor for yt-dlp (fast and areafree)"
readme = "README.md"
--
cgit v1.2.3-70-g09d2
From 9b5db8364faf25cf3019e92ed4110b7958a4da9e Mon Sep 17 00:00:00 2001
From: garret1317
Date: Sun, 21 Sep 2025 18:06:15 +0100
Subject: RSeasons: extract description and thumbnail
---
yt_dlp_plugins/extractor/radiko.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/yt_dlp_plugins/extractor/radiko.py b/yt_dlp_plugins/extractor/radiko.py
index 922c0eb..3fd19d9 100644
--- a/yt_dlp_plugins/extractor/radiko.py
+++ b/yt_dlp_plugins/extractor/radiko.py
@@ -792,6 +792,7 @@ class RadikoRSeasonsIE(InfoExtractor):
"info_dict": {
"id": '10012302',
"title": '山下達郎の楽天カード サンデー・ソングブック',
+ 'thumbnail': 'https://program-static.cf.radiko.jp/935a87fc-4a52-48e5-9468-7b2ef9448d9f.jpeg',
}
}, {
"url": "https://radiko.jp/r_seasons/10002831",
@@ -799,6 +800,8 @@ class RadikoRSeasonsIE(InfoExtractor):
"info_dict": {
"id": "10002831",
"title": "Tokyo Moon",
+ 'description': 'md5:3eef525003bbe96ccf33ec647c43d904',
+ 'thumbnail': 'https://program-static.cf.radiko.jp/0368ee85-5d5f-41c9-8ee1-6c1035d87b3f.jpeg',
}
}]
@@ -822,5 +825,9 @@ class RadikoRSeasonsIE(InfoExtractor):
return self.playlist_result(
entries(),
playlist_id=season_id,
- playlist_title=traverse_obj(pageProps, ("rSeason", "rSeasonName")),
+ **traverse_obj(pageProps, ("rSeason", {
+ "playlist_title": "rSeasonName",
+ "thumbnail": "backgroundImageUrl",
+ "description": ("summary", filter),
+ })),
)
--
cgit v1.2.3-70-g09d2
From 5e661893d582383cdfc811c7fae605c053ddc8ad Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 26 Sep 2025 14:11:52 +0100
Subject: add plugin bundle script
---
contrib/bundle.sh | 8 ++++++++
1 file changed, 8 insertions(+)
create mode 100755 contrib/bundle.sh
diff --git a/contrib/bundle.sh b/contrib/bundle.sh
new file mode 100755
index 0000000..e51d6bc
--- /dev/null
+++ b/contrib/bundle.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+version="$(uv tool run hatch version)"
+mkdir bundle/
+uv pip install --python-version 3.9 --python-platform linux --requirements pyproject.toml --target bundle/yt_dlp_plugins/
+rm -rf bundle/yt_dlp_plugins/*.dist-info bundle/yt_dlp_plugins/bin
+uv pip install --python-version 3.9 --python-platform linux --no-deps --target bundle/ .
+mkdir -p dist/
+(cd bundle/ && zip -9 --recurse-paths ../dist/yt_dlp_rajiko-${version}.bundle.zip yt_dlp_plugins)
--
cgit v1.2.3-70-g09d2
From 72292f904c85b9f31338ec2ac2b6ae737070acea Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 26 Sep 2025 14:14:16 +0100
Subject: update release instructions
---
contrib/how to do a release | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/contrib/how to do a release b/contrib/how to do a release
index 6e91e14..ba27910 100644
--- a/contrib/how to do a release
+++ b/contrib/how to do a release
@@ -4,13 +4,18 @@ update the pyproject.toml
tag it in git, eg v1.0
## build the builds
+
+WHEEL + SOURCE TARBALL
python3 -m build
-and then put BOTH items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
+ZIP BUNDLE
+contrib/bundle.sh
+
+and then put ALL items from `dist` into the pip index dir - ~/site2/yt-dlp-rajiko/pip/yt-dlp-rajiko/
because without the .whl pip has to "build" it itself, with all the stuff that needs to be installed for that to work
run script to update the pip index html and the dl/ "latest" symlinks
-this also updates the sha256s on the site
+this also updates the sha256 blocks on the site
## update the changelog file
@@ -25,10 +30,10 @@ include the pip instructions, sha256sum etc
now push to the server
-!!NEW!!
-upload to pip proper as well
-go to dl/ dir and do
-twine upload yt_dlp_rajiko-1.x*
+NOW UPLOAD TO PYPI AS WELL
+
+go to dl/ dir and do like
+twine upload yt_dlp_rajiko-1.x-py3-none-any.whl yt_dlp_rajiko-1.x.tar.gz
## update github
--
cgit v1.2.3-70-g09d2
From bd3436ba0b71b2f87f83afd329a4ad202a59cedb Mon Sep 17 00:00:00 2001
From: garret1317
Date: Fri, 26 Sep 2025 14:15:08 +0100
Subject: update site update script to handle plugin bundle
---
contrib/generate_html.py | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/contrib/generate_html.py b/contrib/generate_html.py
index 0e15d6a..a52c89d 100755
--- a/contrib/generate_html.py
+++ b/contrib/generate_html.py
@@ -20,6 +20,7 @@ site_sha256 = []
tarballs = []
wheels = []
+bundles = []
for item in sorted(os.listdir()):#, key=lambda x: x.name):
if os.path.islink(item):
@@ -29,6 +30,8 @@ for item in sorted(os.listdir()):#, key=lambda x: x.name):
tarballs.append(item)
elif item.endswith(".whl"):
wheels.append(item)
+ elif item.endswith(".bundle.zip"):
+ bundles.append(item)
else:
continue
@@ -45,7 +48,7 @@ for item in sorted(os.listdir()):#, key=lambda x: x.name):
pip_index.write(item)
pip_index.write("\n")
- site_string = checksum + " " + '' + item + " "
+ site_string = checksum + " " + '' + item + ""
site_sha256.append(site_string)
pip_index.write("""
@@ -55,7 +58,8 @@ pip_index.write("""
latest_tarball = tarballs[-1]
latest_wheel = wheels[-1]
-print(latest_tarball, latest_wheel)
+latest_bundle = bundles[-1]
+print(latest_tarball, latest_wheel, latest_bundle)
os.remove("yt_dlp_rajiko-latest.tar.gz")
os.symlink(latest_tarball, "yt_dlp_rajiko-latest.tar.gz")
@@ -63,12 +67,15 @@ os.symlink(latest_tarball, "yt_dlp_rajiko-latest.tar.gz")
os.remove("yt_dlp_rajiko-latest.whl")
os.symlink(latest_wheel, "yt_dlp_rajiko-latest.whl")
+os.remove("yt_dlp_rajiko-latest.bundle.zip")
+os.symlink(latest_bundle, "yt_dlp_rajiko-latest.bundle.zip")
+
site_sha256.reverse()
-latest_list = site_sha256[:2]
-previous_list = site_sha256[2:]
+latest_list = site_sha256[:3]
+previous_list = site_sha256[3:]
-latest = "\n".join(["", "", "\n".join(latest_list), "", ""])
+latest = "\n".join(["", "