2020 update_headerfile_cookie ,
2121)
2222from fuo_ytmusic .helpers import Singleton
23+ from fuo_ytmusic .lyrics import (
24+ build_watch_playlist_body ,
25+ extract_lyrics_browse_id ,
26+ timestamped_lyrics_to_lrc ,
27+ )
2328from fuo_ytmusic .models import (
2429 AlbumInfo ,
2530 ArtistInfo ,
@@ -216,10 +221,13 @@ class YtmusicService(metaclass=Singleton):
216221 def __init__ (self ):
217222 self ._session = requests .Session ()
218223 self ._api : Optional [YTMusic ] = None
224+ self ._anonymous_lyrics_api : Optional [YTMusic ] = None
225+ self ._timeout : Optional [int ] = None
219226 self ._session .hooks ["response" ].append (self ._do_logging )
220227
221228 self ._signature_timestamp = 0
222229 self ._api_lock = threading .Lock ()
230+ self ._anonymous_lyrics_api_lock = threading .Lock ()
223231 self ._profile_manager = YtmusicProfileManager (self )
224232 self ._language : Optional [str ] = None
225233
@@ -245,9 +253,12 @@ def get_signature_timestamp(self):
245253 return 0
246254
247255 def reinitialize_by_headerfile (self , headerfile = None ):
256+ self ._api = self ._create_api (headerfile , self ._session )
257+
258+ def _create_api (self , headerfile , session : requests .Session ) -> YTMusic :
248259 language = self ._language or "zh_CN"
249260 options = dict (
250- requests_session = self . _session ,
261+ requests_session = session ,
251262 language = language ,
252263 oauth_credentials = OAuthCredentials (
253264 # In the new version of ytmusicapi, client_id and client_secret
@@ -259,35 +270,62 @@ def reinitialize_by_headerfile(self, headerfile=None):
259270 ".apps.googleusercontent.com"
260271 ),
261272 client_secret = "SboVhoG9s0rNafixCSGGKXAT" ,
262- session = self . _session ,
273+ session = session ,
263274 ),
264275 )
265276 # Due to https://github.com/sigma67/ytmusicapi/issues/676,
266277 # YTMusic does not work in specific cases when auth file is provided.
267278 # So initialize without auth file when 400 is returned.
268279 if headerfile is not None and headerfile .exists ():
269280 logger .info ("Initializing ytmusic api with headerfile." )
270- self . _api = YTMusic (str (headerfile ), ** options )
271- self . _api .set_headerfile_path (headerfile )
281+ api = YTMusic (str (headerfile ), ** options )
282+ api .set_headerfile_path (headerfile )
272283 else :
273284 logger .info ("Initializing ytmusic api with no headerfile." )
274- self ._api = YTMusic (** options )
285+ api = YTMusic (** options )
286+ return api
287+
288+ def _get_anonymous_lyrics_api (self ) -> YTMusic :
289+ if self ._anonymous_lyrics_api is None :
290+ with self ._anonymous_lyrics_api_lock :
291+ if self ._anonymous_lyrics_api is None :
292+ # Headerfile-authenticated clients can get HTTP 400 when
293+ # requesting timestamped lyrics, while anonymous requests
294+ # for the same lyrics browse id succeed.
295+ session = requests .Session ()
296+ session .hooks ["response" ].append (self ._do_logging )
297+ session .proxies = dict (self ._session .proxies )
298+ if self ._timeout is not None :
299+ session .request = partial (
300+ session .request ,
301+ timeout = self ._timeout ,
302+ )
303+ self ._anonymous_lyrics_api = self ._create_api (None , session )
304+ return self ._anonymous_lyrics_api
305+
306+ def _clear_anonymous_lyrics_api (self ):
307+ with self ._anonymous_lyrics_api_lock :
308+ self ._anonymous_lyrics_api = None
275309
276310 def setup_language (self , language : str ):
277311 self ._language = language
312+ self ._clear_anonymous_lyrics_api ()
278313
279314 def setup_http_proxy (self , http_proxy ):
280315 self ._session .proxies = {
281316 "http" : http_proxy ,
282317 "https" : http_proxy ,
283318 }
319+ self ._clear_anonymous_lyrics_api ()
284320
285321 def setup_timeout (self , timeout ):
322+ self ._timeout = timeout
286323 if isinstance (self ._session .request , partial ):
287324 request = self ._session .request .func
288325 else :
289326 request = self ._session .request
290327 self ._session .request = partial (request , timeout = timeout )
328+ self ._clear_anonymous_lyrics_api ()
291329
292330 def search (
293331 self ,
@@ -365,6 +403,20 @@ def album_info(self, browse_id: str) -> AlbumInfo:
365403 def song_info (self , video_id : str ) -> SongInfo :
366404 return SongInfo (** self .api .get_song (video_id , self .get_signature_timestamp ()))
367405
406+ def _song_lyrics_browse_id (self , video_id : str , api = None ) -> Optional [str ]:
407+ if api is None :
408+ api = self .api
409+ response = api .send_api_request ("next" , build_watch_playlist_body (video_id ))
410+ return extract_lyrics_browse_id (response )
411+
412+ def song_lyrics (self , video_id : str ) -> Optional [str ]:
413+ lyrics_api = self ._get_anonymous_lyrics_api ()
414+ lyrics_browse_id = self ._song_lyrics_browse_id (video_id , api = lyrics_api )
415+ if lyrics_browse_id is None :
416+ return None
417+ lyrics_payload = lyrics_api .get_lyrics (lyrics_browse_id , timestamps = True )
418+ return timestamped_lyrics_to_lrc (lyrics_payload )
419+
368420 @ttl_cache (maxsize = CACHE_SIZE , ttl = CACHE_TTL )
369421 def categories (self ) -> List [Categories ]:
370422 return [
0 commit comments