From 05c15e38ef52bb1eab441652bc1ee3b70c53a7db Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Mon, 7 Jul 2025 03:50:44 +0900 Subject: [PATCH 1/9] [ie/locipo] Add extractor --- yt_dlp/extractor/_extractors.py | 1 + yt_dlp/extractor/locipo.py | 174 ++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 yt_dlp/extractor/locipo.py diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py index fbbd9571f7..2eb54a1a02 100644 --- a/yt_dlp/extractor/_extractors.py +++ b/yt_dlp/extractor/_extractors.py @@ -1059,6 +1059,7 @@ ) from .livestreamfails import LivestreamfailsIE from .lnk import LnkIE +from .locipo import LocipoIE from .loco import LocoIE from .loom import ( LoomFolderIE, diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py new file mode 100644 index 0000000000..5dc4cbd05e --- /dev/null +++ b/yt_dlp/extractor/locipo.py @@ -0,0 +1,174 @@ +from typing import Literal + +from yt_dlp.utils._utils import classproperty + +from .common import InfoExtractor +from ..utils import ( + filter_dict, + int_or_none, + parse_iso8601, + str_or_none, + traverse_obj, + url_or_none, +) + + +class LocipoIE(InfoExtractor): + + @classproperty + def IE_NAME(cls) -> Literal['locipo']: + return 'locipo' + + IE_DESC = 'Locipo (ロキポ) Video/Playlist' + + _VALID_URL = r'https?://locipo\.jp/creative/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(\?.*list=(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))?' + + _TESTS = [ + { + 'url': 'https://locipo.jp/creative/fb5ffeaa-398d-45ce-bb49-0e221b5f94f1', + 'info_dict': { + 'ext': 'mp4', + 'id': 'fb5ffeaa-398d-45ce-bb49-0e221b5f94f1', + 'series': 'リアルカレカノ', + 'series_id': 'b865b972-99fe-41d5-a72c-8ed5c42132bd', + 'duration': 3622, + 'title': 'リアルカレカノ#4 ~伊達さゆりと勉強しよっ?~', + 'description': 'TVアニメ「ラブライブ!スーパースター!!」澁谷かのん役などで\n活躍中の人気声優「伊達さゆり」さんと、恋人気分が味わえるコンテンツが登場!\n\n全てカレシ・カノジョの1人称目線で撮影しているため\nこの動画でしか味わえない、ドキドキ感が満載!\n一緒に勉強したり…ご飯を食べたり…相談に乗ってもらったり…\nいろんなシチュエーションを楽しんでください!\n', + 'uploader': 'thk', + 'uploader_id': '1', + 'thumbnail': 'https://dophkbxgy39ig.cloudfront.net/store/creatives/99190/large-51fec5367d73fc55dc364250885dfb2e.png', + 'timestamp': 1711789200, + 'modified_timestamp': 1725415481, + 'upload_date': '20240330', + 'modified_date': '20240904', + }, + }, + { + 'url': 'https://locipo.jp/creative/8be557b9-5a97-4092-825e-5cb8c72b36ab?list=3058b313-3a7c-4d64-b067-d3d870b4b17d&noautoplay=&redirect=true', + 'info_dict': { + 'id': '3058b313-3a7c-4d64-b067-d3d870b4b17d', + 'title': '達眼の戦士s ', + 'description': '今注目のeスポーツで活躍するプロに密着!\n勝利への強いこだわりに迫るドキュメントバラエティ', + }, + 'playlist_count': 2, + }, + { + 'url': 'https://locipo.jp/creative/867176a9-cfd8-4807-b5f0-e41a549ba588?list=07738b35-6ce6-48b6-92f7-00167a95bb12', + 'info_dict': { + 'id': '07738b35-6ce6-48b6-92f7-00167a95bb12', + 'title': 'チャント!特集', + }, + 'playlist_mincount': 30, + }, + ] + + def _real_extract(self, url: str): + creative_id = self._match_valid_url(url).group('creative_id') # type: ignore + try: + playlist_id = self._match_valid_url(url).group('playlist_id') # type: ignore + except AttributeError: + playlist_id = '' + + if playlist_id and self.get_param('noplaylist'): + self.to_screen(f'--no-playlist option specified. Processing only video {creative_id}') + playlist_id = '' + elif playlist_id: + self.to_screen(f'Processing playlist ID {playlist_id}. if you want to process only the video {creative_id}, use --no-playlist option') + + if not playlist_id: + creative_data = self._download_json( + url_or_request=f'https://api.locipo.jp/api/v1/creatives/{creative_id}', + video_id=creative_id, + headers=filter_dict( + { + 'accept': 'application/json, text/plain, */*', + 'origin': 'https://locipo.jp', + 'referer': 'https://locipo.jp/', + }, + ), + ) + + return { + # traverse_obj(creative_data, ('video', 'hls', {str})) is used to extract the HLS URL + 'formats': self._extract_m3u8_formats(m3u8_url=traverse_obj(creative_data, ('video', 'hls', {str})), video_id=creative_id), # type: ignore + 'id': creative_id, + **traverse_obj( + creative_data, + { + 'series': ('playlist', 'title', {str}), + 'series_id': ('playlist', 'id', {str}), + 'duration': ('video', 'duration', {int_or_none}), + 'title': ('title', {str}), + 'description': ('description', {str}), + 'uploader': ('station_cd', {str}), + 'uploader_id': ('station_id', {str}), + 'thumbnail': ('thumb', {url_or_none}), + 'timestamp': ('broadcast_started_at', {parse_iso8601}), + 'modified_timestamp': ('updated_at', {parse_iso8601}), + }, + ), # type: ignore + } + + playlist_data = self._download_json( + url_or_request=f'https://api.locipo.jp/api/v1/playlists/{playlist_id}', + video_id=playlist_id, + headers=filter_dict( + { + 'accept': 'application/json, text/plain, */*', + 'origin': 'https://locipo.jp', + 'referer': 'https://locipo.jp/', + }, + ), + ) + + # NOTE: This API can return up to 1000 videos. Since there doesn't seem to be any playlist with more than 1000 items at the moment, pagination is currently not implemented. + playlist_creatives_data = self._download_json( + url_or_request=f'https://api.locipo.jp/api/v1/playlists/{playlist_id}/creatives', + video_id=None, + headers=filter_dict( + { + 'accept': 'application/json, text/plain, */*', + 'origin': 'https://locipo.jp', + 'referer': 'https://locipo.jp/', + }, + ), + ) + + entries = [] + for creative in playlist_creatives_data.get('items', []): # type: ignore + entries.append( + { + **traverse_obj( + creative, + { + 'id': ('id', {str}), + 'duration': ('video', 'duration', {int_or_none}), + 'title': ('title', {str}), + 'description': ('description', {str_or_none}), + 'uploader': ('station_cd', {str_or_none}), + 'uploader_id': ('station_id', {str_or_none}), + 'thumbnail': ('thumb', {url_or_none}), + 'timestamp': ('broadcast_started_at', {parse_iso8601}), + 'modified_timestamp': ('updated_at', {parse_iso8601}), + }, + ), # type: ignore + **traverse_obj( + playlist_data, + { + 'series': ('title', {str}), + 'series_id': ('id', {str}), + }, + ), # type: ignore + 'formats': self._extract_m3u8_formats( + m3u8_url=traverse_obj(creative, ('video', 'hls', {str})), # type: ignore + video_id=traverse_obj(creative, ('id', {str})), # type: ignore + ), + }, + ) + + return self.playlist_result( + entries=entries, + playlist_id=playlist_id, + playlist_title=traverse_obj(playlist_data, ('title', {str})), # type: ignore + playlist_description=traverse_obj(playlist_data, ('description', {str_or_none})), # type: ignore + ) From c991dd739111b959802635222e0571a347145169 Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:58:33 +0900 Subject: [PATCH 2/9] [ie/locipo] Move classproperty import --- yt_dlp/extractor/locipo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 5dc4cbd05e..5883fe3e79 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -1,9 +1,8 @@ from typing import Literal -from yt_dlp.utils._utils import classproperty - from .common import InfoExtractor from ..utils import ( + classproperty, filter_dict, int_or_none, parse_iso8601, From a0f2dd57a077c17bb4da0912b5c72a7674ccd32f Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Mon, 7 Jul 2025 23:59:02 +0900 Subject: [PATCH 3/9] [ie/locipo] Remove unnecessary comment --- yt_dlp/extractor/locipo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 5883fe3e79..1e3ef48b99 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -88,7 +88,6 @@ def _real_extract(self, url: str): ) return { - # traverse_obj(creative_data, ('video', 'hls', {str})) is used to extract the HLS URL 'formats': self._extract_m3u8_formats(m3u8_url=traverse_obj(creative_data, ('video', 'hls', {str})), video_id=creative_id), # type: ignore 'id': creative_id, **traverse_obj( From 198730d17f7ee67730039709f3b204c91f7a5c0a Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:09:34 +0900 Subject: [PATCH 4/9] [ie/locipo] Remove unnecessary block Co-authored-by: garret1317 --- yt_dlp/extractor/locipo.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 1e3ef48b99..b4c1a85e55 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -13,11 +13,6 @@ class LocipoIE(InfoExtractor): - - @classproperty - def IE_NAME(cls) -> Literal['locipo']: - return 'locipo' - IE_DESC = 'Locipo (ロキポ) Video/Playlist' _VALID_URL = r'https?://locipo\.jp/creative/(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(\?.*list=(?P[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))?' From 02133aeb1005a764a09bfdb22bc2c65350d1f7d6 Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:14:36 +0900 Subject: [PATCH 5/9] [ie/locipo] Remove redundant block Co-authored-by: garret1317 --- yt_dlp/extractor/locipo.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index b4c1a85e55..9bdd8b0bb7 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -57,17 +57,7 @@ class LocipoIE(InfoExtractor): ] def _real_extract(self, url: str): - creative_id = self._match_valid_url(url).group('creative_id') # type: ignore - try: - playlist_id = self._match_valid_url(url).group('playlist_id') # type: ignore - except AttributeError: - playlist_id = '' - - if playlist_id and self.get_param('noplaylist'): - self.to_screen(f'--no-playlist option specified. Processing only video {creative_id}') - playlist_id = '' - elif playlist_id: - self.to_screen(f'Processing playlist ID {playlist_id}. if you want to process only the video {creative_id}, use --no-playlist option') + creative_id, playlist_id = self._match_valid_url(url).group('creative_id', 'playlist_id') if not playlist_id: creative_data = self._download_json( From 1925bb95b72f222682399f0c1575309aa0c7976b Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:20:05 +0900 Subject: [PATCH 6/9] [ie/locipo] Ignore type check --- yt_dlp/extractor/locipo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 9bdd8b0bb7..9c8704d3d6 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -57,7 +57,7 @@ class LocipoIE(InfoExtractor): ] def _real_extract(self, url: str): - creative_id, playlist_id = self._match_valid_url(url).group('creative_id', 'playlist_id') + creative_id, playlist_id = self._match_valid_url(url).group('creative_id', 'playlist_id') # type: ignore if not playlist_id: creative_data = self._download_json( From 15b1211c21561ab0ce92dac7d4aed0c0fd3bd747 Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:22:43 +0900 Subject: [PATCH 7/9] [ie/locipo] Clean up _download_json block --- yt_dlp/extractor/locipo.py | 48 +++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 9c8704d3d6..05a1558c9e 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -61,15 +61,13 @@ def _real_extract(self, url: str): if not playlist_id: creative_data = self._download_json( - url_or_request=f'https://api.locipo.jp/api/v1/creatives/{creative_id}', - video_id=creative_id, - headers=filter_dict( - { - 'accept': 'application/json, text/plain, */*', - 'origin': 'https://locipo.jp', - 'referer': 'https://locipo.jp/', - }, - ), + f'https://api.locipo.jp/api/v1/creatives/{creative_id}', + creative_id, + headers={ + 'accept': 'application/json, text/plain, */*', + 'origin': 'https://locipo.jp', + 'referer': 'https://locipo.jp/', + }, ) return { @@ -93,28 +91,24 @@ def _real_extract(self, url: str): } playlist_data = self._download_json( - url_or_request=f'https://api.locipo.jp/api/v1/playlists/{playlist_id}', - video_id=playlist_id, - headers=filter_dict( - { - 'accept': 'application/json, text/plain, */*', - 'origin': 'https://locipo.jp', - 'referer': 'https://locipo.jp/', - }, - ), + f'https://api.locipo.jp/api/v1/playlists/{playlist_id}', + playlist_id, + headers={ + 'accept': 'application/json, text/plain, */*', + 'origin': 'https://locipo.jp', + 'referer': 'https://locipo.jp/', + }, ) # NOTE: This API can return up to 1000 videos. Since there doesn't seem to be any playlist with more than 1000 items at the moment, pagination is currently not implemented. playlist_creatives_data = self._download_json( - url_or_request=f'https://api.locipo.jp/api/v1/playlists/{playlist_id}/creatives', - video_id=None, - headers=filter_dict( - { - 'accept': 'application/json, text/plain, */*', - 'origin': 'https://locipo.jp', - 'referer': 'https://locipo.jp/', - }, - ), + f'https://api.locipo.jp/api/v1/playlists/{playlist_id}/creatives', + None, + headers={ + 'accept': 'application/json, text/plain, */*', + 'origin': 'https://locipo.jp', + 'referer': 'https://locipo.jp/', + }, ) entries = [] From 3cb5f1a953aa2d4744e56991e15e08faf71dc555 Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:31:44 +0900 Subject: [PATCH 8/9] [ie/locipo] Split single metadata traversal function --- yt_dlp/extractor/locipo.py | 45 ++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 05a1558c9e..2a4d7ea8ae 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -56,6 +56,19 @@ class LocipoIE(InfoExtractor): }, ] + def _get_creative_metadata(self, creative_data): + return traverse_obj(creative_data, { + 'id': ('id', {str}), + 'duration': ('video', 'duration', {int_or_none}), + 'title': ('title', {str}), + 'description': ('description', {str}), + 'uploader': ('station_cd', {str}), + 'uploader_id': ('station_id', {str}), + 'thumbnail': ('thumb', {url_or_none}), + 'timestamp': ('broadcast_started_at', {parse_iso8601}), + 'modified_timestamp': ('updated_at', {parse_iso8601}), + }) + def _real_extract(self, url: str): creative_id, playlist_id = self._match_valid_url(url).group('creative_id', 'playlist_id') # type: ignore @@ -73,19 +86,12 @@ def _real_extract(self, url: str): return { 'formats': self._extract_m3u8_formats(m3u8_url=traverse_obj(creative_data, ('video', 'hls', {str})), video_id=creative_id), # type: ignore 'id': creative_id, + **self._get_creative_metadata(creative_data), # type: ignore **traverse_obj( creative_data, { 'series': ('playlist', 'title', {str}), 'series_id': ('playlist', 'id', {str}), - 'duration': ('video', 'duration', {int_or_none}), - 'title': ('title', {str}), - 'description': ('description', {str}), - 'uploader': ('station_cd', {str}), - 'uploader_id': ('station_id', {str}), - 'thumbnail': ('thumb', {url_or_none}), - 'timestamp': ('broadcast_started_at', {parse_iso8601}), - 'modified_timestamp': ('updated_at', {parse_iso8601}), }, ), # type: ignore } @@ -115,20 +121,11 @@ def _real_extract(self, url: str): for creative in playlist_creatives_data.get('items', []): # type: ignore entries.append( { - **traverse_obj( - creative, - { - 'id': ('id', {str}), - 'duration': ('video', 'duration', {int_or_none}), - 'title': ('title', {str}), - 'description': ('description', {str_or_none}), - 'uploader': ('station_cd', {str_or_none}), - 'uploader_id': ('station_id', {str_or_none}), - 'thumbnail': ('thumb', {url_or_none}), - 'timestamp': ('broadcast_started_at', {parse_iso8601}), - 'modified_timestamp': ('updated_at', {parse_iso8601}), - }, - ), # type: ignore + 'formats': self._extract_m3u8_formats( + m3u8_url=traverse_obj(creative, ('video', 'hls', {str})), # type: ignore + video_id=traverse_obj(creative, ('id', {str})), # type: ignore + ), + **self._get_creative_metadata(creative), # type: ignore **traverse_obj( playlist_data, { @@ -136,10 +133,6 @@ def _real_extract(self, url: str): 'series_id': ('id', {str}), }, ), # type: ignore - 'formats': self._extract_m3u8_formats( - m3u8_url=traverse_obj(creative, ('video', 'hls', {str})), # type: ignore - video_id=traverse_obj(creative, ('id', {str})), # type: ignore - ), }, ) From 79b4b1694b2bd893190ec6d099a72edd4ebf2e22 Mon Sep 17 00:00:00 2001 From: gravesducking <219445875+gravesducking@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:32:10 +0900 Subject: [PATCH 9/9] [ie/locipo] Remove unused imports --- yt_dlp/extractor/locipo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/yt_dlp/extractor/locipo.py b/yt_dlp/extractor/locipo.py index 2a4d7ea8ae..ebcb2ae906 100644 --- a/yt_dlp/extractor/locipo.py +++ b/yt_dlp/extractor/locipo.py @@ -1,9 +1,6 @@ -from typing import Literal from .common import InfoExtractor from ..utils import ( - classproperty, - filter_dict, int_or_none, parse_iso8601, str_or_none,