
    ~jZ                        d Z ddlmZ ddlZddlZddlmZ ddlZddlm	Z	 ddl
mZ ddlmZmZmZmZ  ej        e          ZdZ G d	 d
          ZdS )u!  YouTube provider — list Max Amini's recent uploads, fetch transcripts.

Two pieces, deliberately different in auth:
  1. Listing recent videos uses the YouTube Data API v3 `search.list` endpoint
     with the user-supplied `YOUTUBE_API_KEY` (just an API key, no OAuth).
     Plain httpx — we don't pull in the heavy `google-api-python-client` since
     we only need this one GET.
  2. Transcript extraction uses the third-party `youtube-transcript-api`
     library. It hits the public `timedtext` endpoint — no OAuth, no API key
     needed. (The official Data API `captions.download` endpoint requires
     OAuth + ownership of the video, which we do not have for Max Amini's
     channel. This is the standard approach used across the ecosystem.)

The library is synchronous, so we run its calls via `asyncio.to_thread`.
Per-video transcript failures are non-fatal: we log + skip that video so the
stage can still surface the videos that DID have transcripts. The stage only
fails if the initial Data API listing itself fails (network/retryable).
    )annotationsN)Any)YouTubeTranscriptApi)settings)JsonExtractionErrorNonRetryableProviderErrorRetryableProviderError_build_semaphorez,https://www.googleapis.com/youtube/v3/searchc                  8    e Zd ZddZddZddd	ZddZddZdS )YouTubeClientreturnNonec                    t          j        t          j                  | _        t          t          j                  | _        t                      | _	        d S )N)timeout)
httpxAsyncClientr   YOUTUBE_TIMEOUT_httpr
   YOUTUBE_MAX_CONCURRENCY_semr   _ttaselfs    </workspace/show-city-briefing-agent/app/providers/youtube.py__init__zYouTubeClient.__init__+   s?    &x/GHHH
$X%EFF	 )**			    c                H   K   | j                                          d {V  d S N)r   acloser   s    r   closezYouTubeClient.close2   s2      j!!!!!!!!!!!r   Nmax_results
int | Nonelist[dict[str, Any]]c                  K   dt           j        ddt          |pt           j        d          t           j        d}| j        4 d{V  	 | j                            t          |           d{V }n7# t          j
        t          j        f$ r}t          dd	d
|           d}~ww xY w	 ddd          d{V  n# 1 d{V swxY w Y   |j        }|dk    s|dk    rt          d||j                  |dk    rt          d||j                  	 |                                }n&# t"          $ r}t%          d| d          |d}~ww xY wg }|                    dg           D ]}|                    d          pi                     d          }	|	s/|                    d          pi }
|                    |	|
                    d          pd|
                    d          pddd         |
                    d          |
                    d          pi                     di                               d          d|	 d           t(                              dt-          |                     |S )zEList the channel's most recent uploads via Data API v3 `search.list`.snippetdatevideo2   )part	channelIdordertype
maxResultskeyN)paramsyoutuber   z	network: i  i  i  z!youtube: non-JSON response (HTTP )itemsidvideoIdtitle descriptionpublishedAt
thumbnailsmediumurlz https://www.youtube.com/watch?v=)video_idr5   r7   published_at	thumbnailr;   z youtube: listed %d recent videos)r   YOUTUBE_CHANNEL_IDminYOUTUBE_MAX_VIDEOSYOUTUBE_API_KEYr   r   get_DATA_API_BASEr   TimeoutExceptionNetworkErrorr	   status_codetextr   json
ValueErrorr   appendloggerinfolen)r   r!   r/   responseexcstatusdatavideositemr<   snips              r   list_recent_videosz YouTubeClient.list_recent_videos5   si      !4kHX-H"MM+
 
 9 	N 	N 	N 	N 	N 	N 	N 	NN!%v!N!NNNNNNN*E,>? N N N,Y;Ls;L;LMMMN 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N %S==FcMM(FHMJJJS==+Ivx}MMM	^==??DD 	^ 	^ 	^%&S&&S&S&STTZ]]	^ (*HHWb)) 	 	D,"11)<<H 88I&&,"DMM$'**0b $ 7 7 =2ttD $ 7 7"hh|44:??"MMQQRWXXD(DD      	6FDDDsN   B;	'A10B;1B%B  B%%B;;
CCD' '
E
1EE
r<   strc                  K   	 t          | j        d          r8t          j        | j        j        |dg           d{V }t          |          }n(t          j        | j        j        |dg           d{V }nF# t          $ r9}t          	                    d|t          |          j                   Y d}~dS d}~ww xY w|sdS g }|D ]c}t          |t                    r|                    dd          }nt          |dd          }|r"|                    t#          |                     dd                    |          S )	a  Fetch the transcript text. Returns '' if unavailable (transcripts
        are user-supplied or auto-generated and not all videos have them).

        The `youtube-transcript-api` library is synchronous; wrap in
        `asyncio.to_thread`. All errors are caught and treated as
        "no transcript for this video" so one un-transcripted clip never
        fails the whole stage.
        fetchen)	languagesNz"youtube: no transcript for %s (%s)r6   rH    )hasattrr   asyncio	to_threadrY   listget_transcript	ExceptionrL   debugr,   __name__
isinstancedictrC   getattrrK   rW   join)r   r<   fetchedchunksrP   textschunkrH   s           r   fetch_transcriptzYouTubeClient.fetch_transcript`   s     	ty'** 
 !( 1IOX$! ! !       g&0I,h4&             	 	 	 LL=xcI[\\\22222		
  	2 	( 	(E%&& 2yy,,ufb11 (SYY'''xxs   A5A: :
B=.B88B=c                4  K   |                                   d{V }g }|D ]@}|                     |d                    d{V }|s&||d<   |                    |           At                              dt          |          t          |                     |S )zConvenience: list + transcribe each. Skips videos with no transcript.

        Returns list of dicts: {video_id, title, published_at, url, transcript, thumbnail}.
        Nr<   
transcriptz%youtube: %d/%d videos had transcripts)rV   rm   rK   rL   rM   rN   )r   rS   outvro   s        r   list_recent_with_transcriptsz*YouTubeClient.list_recent_with_transcripts   s      
 ..00000000$& 	 	A#44Qz]CCCCCCCCJ (AlOJJqMMMM;SXXs6{{SSS
r   )r   r   r   )r!   r"   r   r#   )r<   rW   r   rW   )r   r#   )rd   
__module____qualname__r   r    rV   rm   rr    r   r   r   r   *   s        + + + +" " " ") ) ) ) )V$ $ $ $L     r   r   )__doc__
__future__r   r^   loggingtypingr   r   youtube_transcript_apir   
app.configr   app.providers._commonr   r   r	   r
   	getLoggerrd   rL   rD   r   ru   r   r   <module>r~      s   & # " " " " "          7 7 7 7 7 7                  
	8	$	$?j j j j j j j j j jr   