Repository: Hexer10/youtube_explode_dart

Issue:
[Bug]: getting videos from playlist cut off at 100 videos
### What happened?

When trying to get the videos of a playlist with more than 100 videos it gets cut off.

Code: 
```dart
final YoutubeExplode _yt = YoutubeExplode();

try {
      var playlist = await _yt.playlists.get(
        'PLtbG4NJHV0Z9suvpfWnr4Km2EoVRR0wH9',
      );
      if (playlist != null) {
        var videos = await _yt.playlists.getVideos(playlist.id).toList();
        setState(() {
          _videos = videos;
          _isLoading = false;
        });
        print('Loaded ${_videos.length} videos.'); // it should be 216 but 100.
      }
    }
```

### Stacktrace / error logs.

```shell
nothing
```

### Package (youtube_explode_dart) version

2.4.0

### Checklist

- [x] I have provided a descriptive title
- [x] I have searched the issues of this repository and believe that this is not a duplicate
- [x] I have tested with the latest version of the library
- [x] (If relevant) I can open the video/playlist in a incognito/private window without being logged in.
- [ ] I have sponsored the project

### Additional notes

This python code works well. I'm sorry I couldn't provide the code in Dart. I'm not very good at Dart.

```python
import requests
import re
import json
import time

def fetch_playlist_page(http_session, playlist_id):
    # Step 1. Get playlist HTML
    url = f"https://www.youtube.com/playlist?list={playlist_id}&hl=en&persist_hl=1"
    html = http_session.get(url).text

    # Step 2. Parse initialData
    initial_data_match = re.search(r"var ytInitialData = ({.*?});</script>", html)
    if not initial_data_match:
        raise Exception("Could not find ytInitialData")

    initial_data = json.loads(initial_data_match.group(1))

    # Step 3. Check if playlist exists in initial data
    if "contents" in initial_data:
        print("[✓] Playlist exists in initial HTML data.")
        return initial_data, html  # Return HTML as well for API info extraction

    # Step 4. Extract browseId if playlist doesn't exist
    browse_id_match = re.search(r'"browseId":"(VL.*?)"', html)
    if not browse_id_match:
        raise Exception("Could not find browseId")
    browse_id = browse_id_match.group(1)

    # Extract visitorData (may or may not exist)
    visitor_match = re.search(r'"VISITOR_DATA":"(.*?)"', html)
    visitor_data = visitor_match.group(1) if visitor_match else ''

    # Step 5. Extract YouTube API Key and Client Version
    innertube_key_match = re.search(r'"INNERTUBE_API_KEY":"(.*?)"', html)
    client_version_match = re.search(r'"INNERTUBE_CLIENT_VERSION":"(.*?)"', html)
    if not innertube_key_match or not client_version_match:
        raise Exception("Could not find API Key or Client version")

    api_key = innertube_key_match.group(1)
    client_version = client_version_match.group(1)

    # Step 6. Call /browse API
    api_url = f"https://www.youtube.com/youtubei/v1/browse?key={api_key}"
    headers = {
        "Content-Type": "application/json",
        "x-youtube-client-name": "1",
        "x-goog-visitor-id": visitor_data
    }
    body = {
        "context": {
            "client": {
                "clientName": "WEB",
                "clientVersion": client_version
            }
        },
        "browseId": browse_id
    }

    resp = http_session.post(api_url, headers=headers, json=body)
    if resp.status_code != 200:
        raise Exception(f"Browse API request failed: {resp.status_code} - {resp.text}")
    
    data = resp.json()
    print("[✓] Fallback browse API successful.")
    return data, html


def extract_api_info(html):
    """Extract API information needed for API calls from HTML."""
    # Extract API Key
    innertube_key_match = re.search(r'"INNERTUBE_API_KEY":"(.*?)"', html)
    # Extract Client Version
    client_version_match = re.search(r'"INNERTUBE_CLIENT_VERSION":"(.*?)"', html)
    # Extract Visitor Data
    visitor_match = re.search(r'"VISITOR_DATA":"(.*?)"', html)
    
    if not innertube_key_match or not client_version_match:
        raise Exception("Could not find API Key or Client version")
    
    return {
        'api_key': innertube_key_match.group(1),
        'client_version': client_version_match.group(1),
        'visitor_data': visitor_match.group(1) if visitor_match else ''
    }


def fetch_continuation_data(http_session, continuation_token, api_info):
    """Fetch additional data using continuation token."""
    api_url = f"https://www.youtube.com/youtubei/v1/browse?key={api_info['api_key']}"
    headers = {
        "Content-Type": "application/json",
        "x-youtube-client-name": "1",
        "x-goog-visitor-id": api_info['visitor_data']
    }
    body = {
        "context": {
            "client": {
                "clientName": "WEB",
                "clientVersion": api_info['client_version']
            }
        },
        "continuation": continuation_token
    }

    resp = http_session.post(api_url, headers=headers, json=body)
    if resp.status_code != 200:
        raise Exception(f"Continuation API request failed: {resp.status_code} - {resp.text}")
    
    return resp.json()


def find_continuation_token(data):
    """Find continuation token from data."""
    try:
        # Find continuation token using exact path
        contents = data['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['playlistVideoListRenderer']['contents']
        
        # Find continuationItemRenderer in contents array
        for item in contents:
            if 'continuationItemRenderer' in item:
                continuation_item = item['continuationItemRenderer']
                
                # Try various paths
                try:
                    # Path 1: continuationEndpoint.commandExecutorCommand.commands[1].continuationCommand.token
                    if 'continuationEndpoint' in continuation_item:
                        endpoint = continuation_item['continuationEndpoint']
                        if 'commandExecutorCommand' in endpoint:
                            commands = endpoint['commandExecutorCommand']['commands']
                            if len(commands) > 1 and 'continuationCommand' in commands[1]:
                                token = commands[1]['continuationCommand']['token']
                                print(f"[✓] Continuation token found (path 1): {token[:50]}...")
                                return token
                        
                        # Path 2: continuationEndpoint.continuationCommand.token
                        if 'continuationCommand' in endpoint:
                            token = endpoint['continuationCommand']['token']
                            print(f"[✓] Continuation token found (path 2): {token[:50]}...")
                            return token
                
                except (KeyError, IndexError, TypeError):
                    continue
        
        return None
    except (KeyError, TypeError) as e:
        print(f"Failed to find continuation token: {e}")
        return None


def find_continuation_token_from_response(data):
    """Find next continuation token from continuation response."""
    try:
        # Find next token from continuation response
        contents = data['onResponseReceivedActions'][0]['appendContinuationItemsAction']['continuationItems']
        
        for item in contents:
            if 'continuationItemRenderer' in item:
                continuation_item = item['continuationItemRenderer']
                
                # Try various paths
                try:
                    # Path 1: continuationEndpoint.commandExecutorCommand.commands[1].continuationCommand.token
                    if 'continuationEndpoint' in continuation_item:
                        endpoint = continuation_item['continuationEndpoint']
                        if 'commandExecutorCommand' in endpoint:
                            commands = endpoint['commandExecutorCommand']['commands']
                            if len(commands) > 1 and 'continuationCommand' in commands[1]:
                                token = commands[1]['continuationCommand']['token']
                                print(f"[✓] Next continuation token found (path 1): {token[:50]}...")
                                return token
                        
                        # Path 2: continuationEndpoint.continuationCommand.token
                        if 'continuationCommand' in endpoint:
                            token = endpoint['continuationCommand']['token']
                            print(f"[✓] Next continuation token found (path 2): {token[:50]}...")
                            return token
                
                except (KeyError, IndexError, TypeError):
                    continue
        
        return None
    except (KeyError, TypeError, IndexError) as e:
        print(f"Failed to find next continuation token: {e}")
        return None


def extract_videos_from_initial_data(data):
    """Extract video list from initial data."""
    video_list = []
    try:
        contents = data['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['playlistVideoListRenderer']['contents']

        for item in contents:
            if 'playlistVideoRenderer' in item:
                video = item['playlistVideoRenderer']
                title = video.get('title', {}).get('runs', [{}])[0].get('text', 'No Title')
                video_id = video.get('videoId', 'No ID')
                video_list.append({'title': title, 'videoId': video_id})

    except (KeyError, TypeError) as e:
        print(f"Initial data structure error: {e}")
        return []

    return video_list


def extract_videos_from_continuation(data):
    """Extract video list from continuation response."""
    video_list = []
    try:
        contents = data['onResponseReceivedActions'][0]['appendContinuationItemsAction']['continuationItems']

        for item in contents:
            if 'playlistVideoRenderer' in item:
                video = item['playlistVideoRenderer']
                title = video.get('title', {}).get('runs', [{}])[0].get('text', 'No Title')
                video_id = video.get('videoId', 'No ID')
                video_list.append({'title': title, 'videoId': video_id})

    except (KeyError, TypeError, IndexError) as e:
        print(f"Continuation data structure error: {e}")
        return []

    return video_list


def fetch_complete_playlist(playlist_id, max_requests=20):
    """Fetch all videos from the playlist."""
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    })
    
    print(f"Starting to fetch playlist {playlist_id}...")
    
    # 1. Get initial data
    initial_data, html = fetch_playlist_page(session, playlist_id)
    api_info = extract_api_info(html)
    
    # 2. Extract initial video list
    all_videos = extract_videos_from_initial_data(initial_data)
    print(f"Found {len(all_videos)} videos in initial data")
    
    # 3. Fetch additional data using continuation token
    continuation_token = find_continuation_token(initial_data)
    
    if not continuation_token:
        print("❌ Could not find continuation token. Returning initial data only.")
        return all_videos
    
    request_count = 0
    
    while continuation_token and request_count < max_requests:
        request_count += 1
        print(f"Fetching additional data, request #{request_count}...")
        
        try:
            # Rate limiting (to avoid being blocked)
            time.sleep(0.5)
            
            continuation_data = fetch_continuation_data(session, continuation_token, api_info)
            new_videos = extract_videos_from_continuation(continuation_data)
            
            if new_videos:
                all_videos.extend(new_videos)
                print(f"+ {len(new_videos)} videos added (total: {len(all_videos)})")
                
                # Find next continuation token
                continuation_token = find_continuation_token_from_response(continuation_data)
                
                if not continuation_token:
                    print("[✓] All videos fetched (no more continuation tokens)")
                    break
            else:
                print("No more videos found.")
                break
                
        except Exception as e:
            print(f"Continuation request failed: {e}")
            break
    
    if request_count >= max_requests:
        print(f"Reached maximum request limit ({max_requests}). There might be more videos.")
    
    return all_videos


# ✅ Usage example
if __name__ == "__main__":
    playlist_id = "PLtbG4NJHV0Z9suvpfWnr4Km2EoVRR0wH9"
    
    # Fetch complete playlist
    video_list = fetch_complete_playlist(playlist_id, max_requests=30)

    # Save to JSON file
    with open(f"{playlist_id}_complete_playlist.json", "w", encoding="utf-8") as f:
        json.dump(video_list, f, ensure_ascii=False, indent=2)

    print(f"\n--- Complete Playlist Results ---")
    print(f"Total songs: {len(video_list)}")
    print(f"File saved: {playlist_id}_complete_playlist.json")
    
    # Display first 10 and last 10 songs
    if video_list:
        print("\nFirst 10 songs:")
        for i, video in enumerate(video_list[:10]):
            print(f"{i+1}. {video['title']} (ID: {video['videoId']})")
        
        if len(video_list) > 20:
            print(f"\n... ({len(video_list)-20} songs omitted) ...\n")
            print("Last 10 songs:")
            for i, video in enumerate(video_list[-10:], len(video_list)-9):
                print(f"{i}. {video['title']} (ID: {video['videoId']})")
    else:
        print("Could not extract playlist information.")
```



Fix patch:
diff --git a/lib/src/reverse_engineering/pages/playlist_page.dart b/lib/src/reverse_engineering/pages/playlist_page.dart
index 1045311..fb37204 100644
--- a/lib/src/reverse_engineering/pages/playlist_page.dart
+++ b/lib/src/reverse_engineering/pages/playlist_page.dart
@@ -157,13 +157,22 @@ class _InitialData extends InitialData {
       ?.getT<String>('text')
       .parseInt();
 
-  late final String? continuationToken =
-      (videosContent ?? playlistVideosContent)
-          ?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null)
-          ?.get('continuationItemRenderer')
-          ?.get('continuationEndpoint')
-          ?.get('continuationCommand')
-          ?.getT<String>('token');
+  String? get continuationToken {
+    final continuationEndpoint = (videosContent ?? playlistVideosContent)
+        ?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null)
+        ?.get('continuationItemRenderer')
+        ?.get('continuationEndpoint');
+
+    return continuationEndpoint
+            ?.get('continuationCommand')
+            ?.getT<String>('token') ??
+        continuationEndpoint
+            ?.get('commandExecutorCommand')
+            ?.getList('commands')
+            ?.firstWhereOrNull((e) => e['continuationCommand'] != null)
+            ?.get('continuationCommand')
+            ?.getT<String>('token');
+  }
 
   List<JsonMap>? get playlistVideosContent =>
       root


Test patch:
diff --git a/test/playlist_test.dart b/test/playlist_test.dart
index ee4d4cf..21fefae 100644
--- a/test/playlist_test.dart
+++ b/test/playlist_test.dart
@@ -56,6 +56,17 @@ void main() {
     expect(videos.length, greaterThan(100));
   });
 
+  test('Get a shit ton of items from a playlist', () async {
+    final videos = await yt!.playlists
+        .getVideos(
+          PlaylistId(
+            'https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX',
+          ),
+        )
+        .toList();
+    expect(videos.length, greaterThan(3500));
+  }, timeout: Timeout(Duration(minutes: 3)));
+
   group('Get videos in any playlist', () {
     for (final val in {
       PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'),