Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/laracasts.py (Python)

Code before:
    IE_NAME = 'laracasts:series'
    _VALID_URL = r'https?://(?:www\.)?laracasts\.com/series/(?P<series>[^/?#]+)/?(?:$|[?#])'
    _TESTS = [{
        'url': 'https://laracasts.com/series/30-days-to-learn-laravel-11',
        'info_dict': {
            'title': '30 Days to Learn Laravel',
            'id': 210,
        },
        'only_matching': True,
    }]

    def _entries(self, series, episode_count):
        for current_episode in range(1, episode_count + 1):
            webpage_url = f'https://laracasts.com/series/{series}/episodes/{current_episode}'
            yield self.url_result(webpage_url, LaracastsIE)

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        display_id = mobj.group('series')
        webpage = self._download_webpage(url, display_id)
        episode_count = int(self._search_regex(r'episodeCount&quot;:(?P<episode_count>[0-9]+)', webpage, 'episode_count'))
        playlist_title = self._search_regex(r'title&quot;:&quot;(?P<playlist_title>[^&]+)&quot;,', webpage, 'playlist_title')
        playlist_id = self._search_regex(r'id&quot;:(?P<playlist_id>[0-9]+)', webpage, 'playlist_id')
        return self.playlist_result(self._entries(display_id, episode_count), playlist_id, playlist_title)

class LaracastsIE(InfoExtractor):
    IE_NAME = 'laracasts'
    _VALID_URL = r'https?://(?:www\.)?laracasts\.com/series/(?P<series>[^/?#]+)/episodes/(?P<episode_number>[0-9]+)'
    _TESTS = [{
        'url': 'https://laracasts.com/series/30-days-to-learn-laravel-11/episodes/1',
        'md5': 'c8f5e7b02ad0e438ef9280a08c8493dc',
        'info_dict': {
            'id': '922040563',
            'title': '1-Hello-Laravel',
            'uploader': 'Laracasts',
            'uploader_id': 'user20182673',
            'uploader_url': 'https://vimeo.com/user20182673',
            'ext': 'mp4',
            'duration': 519,
            'thumbnail': 'https://i.vimeocdn.com/video/1812897371-64aac3913bc92e99c5a56ff58fa0d4894993ba04bd2e6703d3f2295e998d5548-d_1280',
        }
    }]

    def _real_extract(self, url):
        mobj = self._match_valid_url(url)
        series, episode_number = mobj.group('series', 'episode_number')
        display_id = '%s/%s' % (series, episode_number)
        webpage = self._download_webpage(url, display_id)
        video_id = self._search_regex(r'vimeoId&quot;:&quot;(?P<vimeo_id>[0-9]+)', webpage, 'vimeo_id')
        embed_url = VimeoIE._smuggle_referrer(f'https://player.vimeo.com/video/{video_id}', 'https://laracasts.com/')
        return self.url_result(embed_url)

Review comment (refactor):
```suggestion
        return self.playlist_result(self._entries(display_id, episode_count), playlist_id, playlist_title)


class LaracastsIE(InfoExtractor):
```

Code after:
    clean_html,
    extract_attributes,
    get_element_html_by_id,
    int_or_none,
    parse_duration,
    str_or_none,
    unified_strdate,
    url_or_none,
    urljoin,
)
from ..utils.traversal import traverse_obj


class LaracastsBaseIE(InfoExtractor):
    def _get_prop_data(self, url, display_id):
        webpage = self._download_webpage(url, display_id)
        return traverse_obj(
            get_element_html_by_id('app', webpage),
            ({extract_attributes}, 'data-page', {json.loads}, 'props'))

    def _parse_episode(self, episode):
        if not traverse_obj(episode, 'vimeoId'):
            self.raise_login_required('This video is only available for subscribers.')
        return self.url_result(
            VimeoIE._smuggle_referrer(
                f'https://player.vimeo.com/video/{episode["vimeoId"]}', 'https://laracasts.com/'),
            VimeoIE, url_transparent=True,
            **traverse_obj(episode, {
                'id': ('id', {int}, {str_or_none}),
                'webpage_url': ('path', {lambda x: urljoin('https://laracasts.com', x)}),
                'title': ('title', {clean_html}),
                'season_number': ('chapter', {int_or_none}),
                'episode_number': ('position', {int_or_none}),
                'description': ('body', {clean_html}),
                'thumbnail': ('largeThumbnail', {url_or_none}),
                'duration': ('length', {int_or_none}),
                'date': ('dateSegments', 'published', {unified_strdate}),
            }))


class LaracastsIE(LaracastsBaseIE):
    IE_NAME = 'laracasts'
    _VALID_URL = r'https?://(?:www\.)?laracasts\.com/series/(?P<id>[\w-]+/episodes/\d+)/?(?:[?#]|$)'
    _TESTS = [{
        'url': 'https://laracasts.com/series/30-days-to-learn-laravel-11/episodes/1',
        'md5': 'c8f5e7b02ad0e438ef9280a08c8493dc',
        'info_dict': {
            'id': '922040563',
            'title': 'Hello, Laravel',
            'ext': 'mp4',
            'duration': 519,