1- import json
21import copy
3- from typing import Union
2+ from typing import Union , Optional
3+ import json
44from urllib .parse import urlencode
55
6- from youtubesearchpython .core .constants import *
76from youtubesearchpython .core .requests import RequestCore
8- from youtubesearchpython .handlers .componenthandler import ComponentHandler
7+ from youtubesearchpython .core .componenthandler import ComponentHandler
8+ from youtubesearchpython .core .constants import *
9+ from youtubesearchpython .core .exceptions import YouTubeRequestError , YouTubeParseError
10+ import httpx
911
1012
1113class ChannelSearchCore (RequestCore , ComponentHandler ):
1214 response = None
1315 responseSource = None
16+ resultComponents = []
1417
1518 def __init__ (self , query : str , language : str , region : str , searchPreferences : str , browseId : str , timeout : int ):
1619 super ().__init__ ()
@@ -21,7 +24,6 @@ def __init__(self, query: str, language: str, region: str, searchPreferences: st
2124 self .searchPreferences = searchPreferences
2225 self .continuationKey = None
2326 self .timeout = timeout
24- self .resultComponents = []
2527
2628 def sync_create (self ):
2729 self ._syncRequest ()
@@ -32,79 +34,116 @@ async def next(self):
3234 await self ._asyncRequest ()
3335 self ._parseChannelSearchSource ()
3436 self .response = self ._getChannelSearchComponent (self .response )
35- return self .response
37+ return { 'result' : self .response }
3638
3739 def _parseChannelSearchSource (self ) -> None :
3840 try :
39- contents = self .response .get ("contents" , {})
40-
41- tabs = (
42- contents .get ("twoColumnBrowseResultsRenderer" , {}).get ("tabs" )
43- or contents .get ("singleColumnBrowseResultsRenderer" , {}).get ("tabs" )
44- or []
45- )
46-
47- last_tab = tabs [- 1 ] if tabs else {}
48-
49- if "expandableTabRenderer" in last_tab :
50- self .response = (
51- last_tab ["expandableTabRenderer" ]
52- .get ("content" , {})
53- .get ("sectionListRenderer" , {})
54- .get ("contents" , [])
55- )
56- else :
57- tab_renderer = last_tab .get ("tabRenderer" , {})
58- content = tab_renderer .get ("content" )
59- if content and "sectionListRenderer" in content :
60- self .response = content ["sectionListRenderer" ].get ("contents" , [])
41+ # Try to get tabs from response
42+ tabs = self .response .get ("contents" , {}).get ("twoColumnBrowseResultsRenderer" , {}).get ("tabs" , [])
43+ if not tabs :
44+ # Try alternative structure
45+ tabs = self .response .get ("contents" , {}).get ("singleColumnBrowseResultsRenderer" , {}).get ("tabs" , [])
46+
47+ if not tabs :
48+ self .response = []
49+ return
50+
51+ # Get the last tab (usually the search results tab)
52+ last_tab = tabs [- 1 ]
53+
54+ # Try expandableTabRenderer first
55+ if 'expandableTabRenderer' in last_tab :
56+ expandable = last_tab ["expandableTabRenderer" ]
57+ # Check if content exists
58+ if 'content' in expandable :
59+ content = expandable ["content" ]
60+ if 'sectionListRenderer' in content :
61+ self .response = content ["sectionListRenderer" ].get ("contents" , [])
62+ else :
63+ self .response = []
64+ else :
65+ # Try to get from expandableTabRenderer directly
66+ if 'sectionListRenderer' in expandable :
67+ self .response = expandable ["sectionListRenderer" ].get ("contents" , [])
68+ else :
69+ self .response = []
70+ # Try tabRenderer
71+ elif 'tabRenderer' in last_tab :
72+ tab_renderer = last_tab ["tabRenderer" ]
73+ if 'content' in tab_renderer :
74+ content = tab_renderer ["content" ]
75+ if 'sectionListRenderer' in content :
76+ self .response = content ["sectionListRenderer" ].get ("contents" , [])
77+ else :
78+ self .response = []
6179 else :
6280 self .response = []
63- except Exception :
64- raise Exception ("ERROR: Could not parse YouTube response." )
81+ else :
82+ self .response = []
83+ except (KeyError , AttributeError , IndexError ) as e :
84+ raise YouTubeParseError (f'Failed to parse YouTube response: { str (e )} ' )
85+ except Exception as e :
86+ raise YouTubeParseError (f'Unexpected error parsing response: { str (e )} ' )
6587
6688 def _getRequestBody (self ):
89+ ''' Fixes #47 '''
6790 requestBody = copy .deepcopy (requestPayload )
68- requestBody ["query" ] = self .query or ""
69-
70- context = requestBody .setdefault ("context" , {})
71- client = context .setdefault ("client" , {})
72- client .update ({
73- "hl" : self .language or client .get ("hl" ),
74- "gl" : self .region or client .get ("gl" ),
91+ requestBody ['query' ] = self .query
92+ requestBody ['client' ] = {
93+ 'hl' : self .language ,
94+ 'gl' : self .region ,
95+ }
96+ requestBody ['params' ] = self .searchPreferences
97+ requestBody ['browseId' ] = self .browseId
98+ self .url = 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode ({
99+ 'key' : searchKey ,
75100 })
76-
77- if self .searchPreferences :
78- requestBody ["params" ] = self .searchPreferences
79- if self .browseId :
80- requestBody ["browseId" ] = self .browseId
81- if self .continuationKey :
82- requestBody ["continuation" ] = self .continuationKey
83-
84- if not searchKey :
85- raise Exception ("INNERTUBE API key (searchKey) is not set." )
86-
87- self .url = "https://www.youtube.com/youtubei/v1/browse?" + urlencode ({"key" : searchKey })
88101 self .data = requestBody
89102
90103 def _syncRequest (self ) -> None :
104+ ''' Fixes #47 '''
91105 self ._getRequestBody ()
92- request = self . syncPostRequest ()
106+
93107 try :
94- self .response = request .json () if hasattr (request , "json" ) else json .loads (request .text )
95- except Exception :
96- raise Exception ("ERROR: Could not make request." )
108+ request = self .syncPostRequest ()
109+ if request .status_code != 200 :
110+ raise YouTubeRequestError (f'Request failed with status code { request .status_code } . URL: { self .url } ' )
111+ self .response = request .json ()
112+ except httpx .RequestError as e :
113+ raise YouTubeRequestError (f'Failed to make request to { self .url } : { str (e )} ' )
114+ except httpx .HTTPStatusError as e :
115+ raise YouTubeRequestError (f'HTTP error { e .response .status_code } for { self .url } : { str (e )} ' )
116+ except json .JSONDecodeError as e :
117+ raise YouTubeRequestError (f'Failed to decode JSON response: { str (e )} ' )
118+ except Exception as e :
119+ raise YouTubeRequestError (f'Unexpected error making request: { str (e )} ' )
97120
98121 async def _asyncRequest (self ) -> None :
122+ ''' Fixes #47 '''
99123 self ._getRequestBody ()
100- request = await self . asyncPostRequest ()
124+
101125 try :
102- self .response = request .json () if hasattr (request , "json" ) else json .loads (request .text )
103- except Exception :
104- raise Exception ("ERROR: Could not make request." )
126+ request = await self .asyncPostRequest ()
127+ if request .status_code != 200 :
128+ raise YouTubeRequestError (f'Request failed with status code { request .status_code } . URL: { self .url } ' )
129+ self .response = request .json ()
130+ except httpx .RequestError as e :
131+ raise YouTubeRequestError (f'Failed to make request to { self .url } : { str (e )} ' )
132+ except httpx .HTTPStatusError as e :
133+ raise YouTubeRequestError (f'HTTP error { e .response .status_code } for { self .url } : { str (e )} ' )
134+ except json .JSONDecodeError as e :
135+ raise YouTubeRequestError (f'Failed to decode JSON response: { str (e )} ' )
136+ except Exception as e :
137+ raise YouTubeRequestError (f'Unexpected error making request: { str (e )} ' )
105138
106139 def result (self , mode : int = ResultMode .dict ) -> Union [str , dict ]:
140+ '''Returns the search result.
141+ Args:
142+ mode (int, optional): Sets the type of result. Defaults to ResultMode.dict.
143+ Returns:
144+ Union[str, dict]: Returns JSON or dictionary.
145+ '''
107146 if mode == ResultMode .json :
108- return json .dumps ({" result" : self .response }, indent = 4 )
147+ return json .dumps ({' result' : self .response }, indent = 4 )
109148 elif mode == ResultMode .dict :
110- return {" result" : self .response }
149+ return {' result' : self .response }
0 commit comments