[^?/]+)'
    _TESTS = [{
        'url': 'https://abema.tv/video/episode/194-25_s2_p1',
        'info_dict': {
            'id': '194-25_s2_p1',
            'title': '第1話 「チーズケーキ」 「モーニング再び」',
            'series': '異世界食堂2',
            'season': 'シーズン2',
            'season_number': 2,
            'episode': '第1話 「チーズケーキ」 「モーニング再び」',
            'episode_number': 1,
        },
        'skip': 'expired',
    }, {
        'url': 'https://abema.tv/channels/anime-live2/slots/E8tvAnMJ7a9a5d',
        'info_dict': {
            'id': 'E8tvAnMJ7a9a5d',
            'title': 'ゆるキャン△ SEASON2 全話一挙【無料ビデオ72時間】',
            'series': 'ゆるキャン△ SEASON2',
            'episode': 'ゆるキャン△ SEASON2 全話一挙【無料ビデオ72時間】',
            'season_number': 2,
            'episode_number': 1,
            'description': 'md5:9c5a3172ae763278f9303922f0ea5b17',
        },
        'skip': 'expired',
    }, {
        'url': 'https://abema.tv/video/episode/87-877_s1282_p31047',
        'info_dict': {
            'id': 'E8tvAnMJ7a9a5d',
            'title': '第5話『光射す』',
            'description': 'md5:56d4fc1b4f7769ded5f923c55bb4695d',
            'thumbnail': r're:https://hayabusa\.io/.+',
            'series': '相棒',
            'episode': '第5話『光射す』',
        },
        'skip': 'expired',
    }, {
        'url': 'https://abema.tv/now-on-air/abema-anime',
        'info_dict': {
            'id': 'abema-anime',
            # this varies
            # 'title': '女子高生の無駄づかい 全話一挙【無料ビデオ72時間】',
            'description': 'md5:55f2e61f46a17e9230802d7bcc913d5f',
            'is_live': True,
        },
        'skip': 'Not supported until yt-dlp implements native live downloader OR AbemaTV can start a local HTTP server',
    }]
    _TIMETABLE = None
    def _real_extract(self, url):
        # starting download using infojson from this extractor is undefined behavior,
        # and never be fixed in the future; you must trigger downloads by directly specifying URL.
        # (unless there's a way to hook before downloading by extractor)
        video_id, video_type = self._match_valid_url(url).group('id', 'type')
        headers = {
            'Authorization': 'Bearer ' + self._get_device_token(),
        }
        video_type = video_type.split('/')[-1]
        webpage = self._download_webpage(url, video_id)
        canonical_url = self._search_regex(
            r'(.+?)', webpage, 'title', default=None)
        if not title:
            jsonld = None
            for jld in re.finditer(
                    r'(?is)(?:)?',
                    webpage):
                jsonld = self._parse_json(jld.group('json_ld'), video_id, fatal=False)
                if jsonld:
                    break
            if jsonld:
                title = jsonld.get('caption')
        if not title and video_type == 'now-on-air':
            if not self._TIMETABLE:
                # cache the timetable because it goes to 5MiB in size (!!)
                self._TIMETABLE = self._download_json(
                    'https://api.abema.io/v1/timetable/dataSet?debug=false', video_id,
                    headers=headers)
            now = time_seconds(hours=9)
            for slot in self._TIMETABLE.get('slots', []):
                if slot.get('channelId') != video_id:
                    continue
                if slot['startAt'] <= now and now < slot['endAt']:
                    title = slot['title']
                    break
        # read breadcrumb on top of page
        breadcrumb = self._extract_breadcrumb_list(webpage, video_id)
        if breadcrumb:
            # breadcrumb list translates to: (e.g. 1st test for this IE)
            # Home > Anime (genre) > Isekai Shokudo 2 (series name) > Episode 1 "Cheese cakes" "Morning again" (episode title)
            # hence this works
            info['series'] = breadcrumb[-2]
            info['episode'] = breadcrumb[-1]
            if not title:
                title = info['episode']
        description = self._html_search_regex(
            (r'(.+?)
(.+?)
[^?/#]+)/?(?:\?(?:[^#]+&)?s=(?P[^]+))?'
    _PAGE_SIZE = 25
    _TESTS = [{
        'url': 'https://abema.tv/video/title/90-1887',
        'info_dict': {
            'id': '90-1887',
            'title': 'シャッフルアイランド',
            'description': 'md5:61b2425308f41a5282a926edda66f178',
        },
        'playlist_mincount': 2,
    }, {
        'url': 'https://abema.tv/video/title/193-132',
        'info_dict': {
            'id': '193-132',
            'title': '真心が届く~僕とスターのオフィス・ラブ!?~',
            'description': 'md5:9b59493d1f3a792bafbc7319258e7af8',
        },
        'playlist_mincount': 16,
    }, {
        'url': 'https://abema.tv/video/title/25-1nzan-whrxe',
        'info_dict': {
            'id': '25-1nzan-whrxe',
            'title': 'ソードアート・オンライン',
            'description': 'md5:c094904052322e6978495532bdbf06e6',
        },
        'playlist_mincount': 25,
    }, {
        'url': 'https://abema.tv/video/title/26-2mzbynr-cph?s=26-2mzbynr-cph_s40',
        'info_dict': {
            'title': '〈物語〉シリーズ',
            'id': '26-2mzbynr-cph',
            'description': 'md5:e67873de1c88f360af1f0a4b84847a52',
        },
        'playlist_count': 59,
    }]
    def _fetch_page(self, playlist_id, series_version, season_id, page):
        query = {
            'seriesVersion': series_version,
            'offset': str(page * self._PAGE_SIZE),
            'order': 'seq',
            'limit': str(self._PAGE_SIZE),
        }
        if season_id:
            query['seasonId'] = season_id
        programs = self._call_api(
            f'v1/video/series/{playlist_id}/programs', playlist_id,
            note=f'Downloading page {page + 1}',
            query=query)
        yield from (
            self.url_result(f'https://abema.tv/video/episode/{x}')
            for x in traverse_obj(programs, ('programs', ..., 'id')))
    def _entries(self, playlist_id, series_version, season_id):
        return OnDemandPagedList(
            functools.partial(self._fetch_page, playlist_id, series_version, season_id),
            self._PAGE_SIZE)
    def _real_extract(self, url):
        playlist_id, season_id = self._match_valid_url(url).group('id', 'season')
        series_info = self._call_api(f'v1/video/series/{playlist_id}', playlist_id)
        return self.playlist_result(
            self._entries(playlist_id, series_info['version'], season_id), playlist_id=playlist_id,
            playlist_title=series_info.get('title'),
            playlist_description=series_info.get('content'))