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

Code before:
            sts = int_or_none(ytcfg.get('STS'))

        if not sts:
            # Attempt to extract from player
            if player_url is None:
                error_msg = 'Cannot extract signature timestamp without player_url.'
                if fatal:
                    raise ExtractorError(error_msg)
                self.report_warning(error_msg)
                return
            code = self._load_player(video_id, player_url, fatal=fatal)
            if code:
                sts = int_or_none(self._search_regex(
                    r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
                    'JS player signature timestamp', group='sts', fatal=fatal))
        return sts

    def _mark_watched(self, video_id, player_responses):
        mark_watched_urls = {
            "playback": get_first(
                player_responses, ('playbackTracking', 'videostatsPlaybackUrl', 'baseUrl'),
                expected_type=url_or_none),
            "watchtime": get_first(
                player_responses, ('playbackTracking', 'videostatsWatchtimeUrl', 'baseUrl'),
                expected_type=url_or_none)
        }

        if not all(mark_watched_urls.values()):
            self.report_warning('Unable to mark watched')
            return
        for url_type, url in mark_watched_urls.items():
            parsed_url = compat_urlparse.urlparse(url)
            qs = compat_urlparse.parse_qs(parsed_url.query)

            # cpn generation algorithm is reverse engineered from base.js.
            # In fact it works even with dummy cpn.
            CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
            cpn = ''.join(CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16))

            video_length = qs.get('len') or ['1.5']
            # more consistent results setting it to right before the end
            video_length_float = float(video_length[0]) - 1
            video_length = [str(video_length_float)]

            qs.update({
                'ver': ['2'],
                'cpn': [cpn],
                'cmt': video_length,
                'el': 'detailpage',  # otherwise defaults to "shorts"
            })


Review comment (suggestion):
makes more sense as 2 seperate vars imo. Say, `playback_url` and `watchtime_url`. Most of the code doesn't need to be inside the loop. Unwrapping it only causes a couple lines of repeated code
```py
playback_url = get_first(...)
if not playback_url:
    ...
...
self._download_webpage(...)

watchtime_url = get_first(...)
if not watchtime_url:
    self.report_warning('Unable to mark fully watched')  # Is this needed?
    return
qs.update({...})
url = ...
self._download_webpage(..., 'Marking fully watched', ...)
```

Code after:
            sts = int_or_none(ytcfg.get('STS'))

        if not sts:
            # Attempt to extract from player
            if player_url is None:
                error_msg = 'Cannot extract signature timestamp without player_url.'
                if fatal:
                    raise ExtractorError(error_msg)
                self.report_warning(error_msg)
                return
            code = self._load_player(video_id, player_url, fatal=fatal)
            if code:
                sts = int_or_none(self._search_regex(
                    r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
                    'JS player signature timestamp', group='sts', fatal=fatal))
        return sts

    def _mark_watched(self, video_id, player_responses):
        for is_full, key in enumerate(('videostatsPlaybackUrl', 'videostatsWatchtimeUrl')):
            label = 'fully ' if is_full else ''
            url = get_first(player_responses, ('playbackTracking', key, 'baseUrl'),
                            expected_type=url_or_none)
            if not url:
                self.report_warning(f'Unable to mark {label}watched')
                return
            parsed_url = compat_urlparse.urlparse(url)
            qs = compat_urlparse.parse_qs(parsed_url.query)

            # cpn generation algorithm is reverse engineered from base.js.
            # In fact it works even with dummy cpn.
            CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
            cpn = ''.join(CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16))

            # # more consistent results setting it to right before the end
            video_length = [str(float((qs.get('len') or ['1.5'])[0]) - 1)]

            qs.update({
                'ver': ['2'],
                'cpn': [cpn],
                'cmt': video_length,
                'el': 'detailpage',  # otherwise defaults to "shorts"
            })

            if is_full:
                # these seem to mark watchtime "history" in the real world
                # they're required, so send in a single value
                qs.update({
                    'st': video_length,
                    'et': video_length,
                })