U
    D×vi}R  ã                   @   sb  d dl Z d dlZd dlZd dlZd dlmZmZmZmZ d dl	m
Z
 d dlmZmZ d dlmZmZ d dlmZmZmZ d dlmZmZmZ d dlmZmZmZ ed	eƒZed
œdd„Z e!dœdd„Z"ee#dœdd„Z$ee%e%dœdd„Z&edœdd„Z'ej(ddgdddd„ ƒZ)ej(ddgddd d„ ƒZ*ej(d!dgd"dd#d"„ ƒZ+ej(d$dgd%dd&d%„ ƒZ,dS )'é    N)Ú	BlueprintÚrequestÚjsonifyÚcurrent_app)Úbulk_db)Úrequire_bulk_api_keyÚget_access_token_for_alias)Úparse_publish_at_iso_to_tsÚcreator_can_post_now)Úsafe_filenameÚvideo_duration_seconds_ffprobeÚbuild_public_media_url)Úprocess_one_scheduled_jobÚresolve_access_token_for_jobÚrefresh_submitted_job_status)Úquery_creator_infoÚupload_video_direct_postÚfetch_post_statusZbulk_api)Ú
table_namec              	   C   sb   |   d|› d¡ |  ¡ }g }|D ]:}z| |d ¡ W q" tk
rZ   | |d ¡ Y q"X q"|S )z€
    Devuelve lista de columnas reales del schema para inserts robustos.
    Funciona con row_factory sqlite3.Row o tuplas.
    zPRAGMA table_info(ú)Únameé   )ÚexecuteÚfetchallÚappendÚ	Exception)Úcurr   ÚrowsÚcolsÚr© r    ú5/var/www/html/luxverbi-app/app/blueprints/bulk_api.pyÚ_table_columns   s    r"   )Újobc                    s|   |   ¡ }t|dƒ}t|ƒ‰ ‡ fdd„| ¡ D ƒ}d | ¡ ¡}d dgt|ƒ ¡}d|› d|› d}| |t| 	¡ ƒ¡ |S )	z^
    Inserta en bulk_jobs filtrando por columnas reales (evita 'N values for M columns').
    Ú	bulk_jobsc                    s   i | ]\}}|ˆ kr||“qS r    r    )Ú.0ÚkÚv©Úcolsetr    r!   Ú
<dictcomp>0   s       z$_insert_bulk_job.<locals>.<dictcomp>z, ú?zINSERT INTO bulk_jobs (z
) VALUES (r   )
Úcursorr"   ÚsetÚitemsÚjoinÚkeysÚlenr   ÚtupleÚvalues)Úconnr#   r   r   ÚfilteredÚcolnamesÚplaceholdersÚsqlr    r(   r!   Ú_insert_bulk_job(   s    
r9   )ÚerrÚreturnc                    s¾   ˆsdS t ˆƒ ¡ ‰ ˆ d¡r"dS ˆ d¡r0dS dddddd	d
dddddddddddddg}t‡ fdd„|D ƒƒrvdS ˆ d¡sŠˆ d¡rŽdS ddddd d!d"g}t‡fd#d„|D ƒƒrºd$S d$S )%u‹  
    Decide si el fallo merece reintento (requeue) en lugar de marcar el job como failed.

    FilosofÃ­a:
      - Reintentar todo lo que sea probablemente transitorio: 5xx, 429, internal_error,
        timeouts, errores de red, "cannot_post_now", etc.
      - Marcar failed solo para errores claramente permanentes (fichero inexistente, privacy
        invÃ¡lida, duraciÃ³n excedida, etc.)
    TÚcannot_post_nowZmissing_publish_idzhttp=429ztoo many requestsZ
rate_limitZthrottlezhttp=500zhttp=502zhttp=503zhttp=504Zinternal_errorzservice unavailablezbad gatewayzgateway timeoutÚtimeoutz	timed outZreadtimeoutZconnecttimeoutÚ
connectionzconnection resetZtemporarilyz	try againc                 3   s   | ]}|ˆ kV  qd S ©Nr    )r%   Úm)Úer    r!   Ú	<genexpr>X   s     z&_is_transient_error.<locals>.<genexpr>Údirect_post_failedÚcreator_info_failedZfile_not_found_on_serverÚduration_exceeds_maxÚinvalid_privacy_levelÚbranded_cannot_be_self_onlyÚmissing_privacy_levelÚmissing_videoÚmissing_filenamec                 3   s   | ]}ˆ   |¡V  qd S r?   )Ú
startswith)r%   Úp)r:   r    r!   rB   i   s     F)ÚstrÚlowerrK   Úany)r:   Ztransient_markersZpermanent_prefixesr    )rA   r:   r!   Ú_is_transient_error9   sV    


               ø
ù	rP   )r:   Úattempts_nextr;   c                 C   sV   d}| p
d  d¡rdnd}t||dtd|d ƒ  ƒ}t|t d	d
¡ ƒ}t|| ƒS )u¼   
    Backoff exponencial con jitter, con cap. Evita martillear la API.

    - cannot_post_now: arranca mÃ¡s alto.
    - cap: 6h (el sistema sigue intentando, pero sin bucle agresivo).
    i`T  Ú r<   é<   é   é   r   r   g        gš™™™™™É?)rK   ÚminÚmaxÚintÚrandomÚuniform)r:   rQ   ÚcapÚbaseÚexpZjitterr    r    r!   Ú_backoff_secondso   s
    r^   )r;   c                 C   s„   z| d pd  ¡ }W n tk
r,   d}Y nX |r<d|› S z| d pHd  ¡ }W n tk
rh   d}Y nX |r€d|dd… › S dS )	z—
    Clave para agrupar 'cooldown' por cuenta.
    Preferimos account_alias; si no existe (legacy), degradamos a un hash parcial del access_token.
    Úaccount_aliasrR   zalias:Úaccess_tokenztoken:Né   Úunknown)Ústripr   )ÚrowÚaliasÚtokr    r    r!   Ú_job_account_key}   s    


rg   z/api/bulk/publishÚPOSTÚapi_bulk_publish)ÚmethodsÚendpointc                  C   s  t ƒ  tj d¡pd ¡ } tj d¡p(d ¡ }| r~zt| ƒ}W n> tk
r| } z tddt|ƒdœƒdf W Y ¢S d}~X Y nX |s”tdd	d
œƒdfS dtj	kr°tddd
œƒdfS tj	d }|j
sÒtddd
œƒdfS tj d¡pàd ¡ }tj d¡pôd ¡ }|stddd
œƒdfS tj dd¡dk}tj dd¡dk}tj dd¡dk}tj dd¡dk}	tj dd¡dk}
tj dd¡dk}tj dd¡dk}|r²|dkr²tddd
œƒdfS zt|ƒ}W n@ tk
rþ } z tddt|ƒdœƒdf W Y ¢S d}~X Y nX t|ƒ\}}|s&tdd|dœƒdfS | d¡p4g }||krTtdd |d!œƒdfS | d"¡d#krhd}| d$¡d#kr|d}| d%¡d#krd}| }| }| }tjd& }t|j
ƒ}tt ¡ ƒ› d'|› }tj ||¡}| |¡ | d(¡}t|ƒ}t|tƒr\|d)kr\|d)kr\||kr\zt |¡ W n tk
rD   Y nX tdd*||d+œƒdfS t|tjd, tjd- tjd. d/}zVt||||||||
|d0|d1}| d2¡p¬i  d3¡}td#||||| pÈdd4œƒd5fW S  tk
r } z tdd6t|ƒdœƒdf W Y ¢S d}~X Y nX dS )7uà   
    Publish inmediato (server-to-server). AquÃ­ SÃ es correcto generar video_url firmado,
    porque se publica en el momento (la ventana de TTL es suficiente).
    Soporta: account_alias (recomendado) o access_token.
    r_   rR   r`   FÚalias_token_failed©ÚokÚerrorÚdetailsé  NZ%missing_access_token_or_account_alias©rn   ro   ÚvideorI   rJ   ÚtitleÚprivacy_levelrH   Úallow_commentÚ1Ú
allow_duetÚallow_stitchÚcommercial_toggleÚ0Úbrand_organic_toggleÚbrand_content_toggleÚis_aigcÚ	SELF_ONLYrG   rD   r<   )rn   ro   ÚreasonÚprivacy_level_optionsrF   )rn   ro   ÚoptionsÚcomment_disabledTÚduet_disabledÚstitch_disabledÚ
UPLOAD_DIRÚ_Úmax_video_post_duration_secr   rE   )rn   ro   ÚdurrW   ÚPUBLIC_BASE_URLÚMEDIA_SIGNING_SECRETÚMEDIA_TOKEN_TTL_SECONDS)Ústored_filenameÚpublic_base_urlÚsigning_secretÚttl_secondsÚPULL_FROM_URL)r`   Úcaptionru   Údisable_commentÚdisable_duetÚdisable_stitchr}   r|   r~   ÚmodeÚ	video_urlÚdataÚ
publish_id)rn   r™   r—   Ú	init_respr   r_   éÈ   rC   )r   r   ÚformÚgetrc   r   r   r   rM   ÚfilesÚfilenamer   r
   r   Úconfigr   rX   ÚtimeÚosÚpathr/   Úsaver   Ú
isinstanceÚremover   r   )r_   r`   rA   Úfilert   ru   rv   rx   ry   rz   r|   r}   r~   Úcreator_infoÚcan_post_nowr€   r‚   r“   r”   r•   Ú
upload_dirÚoriginal_fnr   Ú	save_pathÚmax_durr‰   r—   rš   r™   r    r    r!   ri      s¼    .

.




*üõúÿ	÷
z/api/bulk/statusÚapi_bulk_statusc               
   C   s  t ƒ  tj d¡pd ¡ } tj d¡p(d ¡ }| r~zt| ƒ}W n> tk
r| } z tddt|ƒdœƒdf W Y ¢S d}~X Y nX tj d	¡pŒd ¡ }|rš|s¬tdd
dœƒdfS zt	||ƒ}td|dœƒdfW S  tk
r } ztdt|ƒdœƒdf W Y ¢S d}~X Y nX dS )zJ
    Status (server-to-server). Soporta account_alias o access_token.
    r_   rR   r`   Frl   rm   rq   Nr™   Z"missing_access_token_or_publish_idrr   T)rn   r˜   r›   )
r   r   rœ   r   rc   r   r   r   rM   r   )r_   r`   rA   r™   r˜   r    r    r!   r®     s     .
z/api/bulk/scheduleÚapi_bulk_schedulec                  C   s&  t ƒ  tj d¡pd ¡ } | s0tdddœƒdfS zt| ƒ}W n> tk
rz } z tddt|ƒdœƒdf W Y ¢S d	}~X Y nX tj d
¡pŠd ¡ }|s¦tdddœƒdfS zt	|ƒ}W n> tk
rð } z tddt|ƒdœƒdf W Y ¢S d	}~X Y nX dtj
krtdddœƒdfS tj
d }|js4tdddœƒdfS tj d¡pDd ¡ }tj d¡pZd ¡ }|sxtdddœƒdfS tj dd¡dk}tj dd¡dk}	tj dd¡dk}
tj dd¡dk}tj dd¡dk}tj dd¡dk}tj dd¡dk}|r|dkrtdddœƒdfS tjd }t|jƒ}t ¡ j}|› d|› }tj ||¡}| |¡ tt ¡ ƒ}||||d d!d	|| |||rˆd"nd!|	r”d"nd!|
r d"nd!|r¬d"nd!|r¸d"nd!|rÄd"nd!|rÐd"nd!||dd	d	d	|d#œ}ttjd$ ƒ}t||ƒ | ¡  | ¡  td%|||| d&œƒd'fS )(uM  
    Schedule (server-to-server). Para jobs programados, account_alias es requerido
    para que el cron/worker pueda refrescar tokens con refresh_token.

    Importante:
      - NO generamos video_url firmado aquÃ­, porque caduca (TTL) antes de publish.
      - El worker debe generar video_url justo en el momento de publicar.
    r_   rR   FZ)account_alias_required_for_scheduled_jobsrr   rq   rl   rm   NÚ
publish_atÚmissing_publish_atZinvalid_publish_atrs   rI   rJ   rt   ru   rH   rv   rw   rx   ry   rz   r{   r|   r}   r~   r   rG   r†   r‡   Ú	scheduledr   r   )ÚidÚcreated_at_tsÚpublish_at_tsÚnext_attempt_tsÚstatusÚattemptsÚ
last_errorr`   r_   rt   ru   rv   rx   ry   rz   r|   r}   r~   r   Úoriginal_filenamer—   r™   Úlast_statusÚlast_fail_reasonÚupdated_at_tsÚBULK_DB_PATHT)rn   Újob_idrµ   r   r_   r›   )r   r   rœ   r   rc   r   r   r   rM   r	   rž   rŸ   r   r    r   ÚuuidÚuuid4Úhexr¢   r£   r/   r¤   rX   r¡   r   r9   ÚcommitÚclose)r_   r`   rA   r°   rµ   r§   rt   ru   rv   rx   ry   rz   r|   r}   r~   rª   r«   r¿   r   r¬   Únow_tsr#   r4   r    r    r!   r¯   )  s     
..




ç
ûÿøz/api/bulk/process_dueÚapi_bulk_process_duec                  C   sV  t ƒ  ttj dd¡pdƒ} tt ¡ ƒ}ttj dd¡p:dƒ}tt	j
d ƒ}| ¡ }ddddddœ}| d|| f¡ | ¡ }g }g }tƒ }	|D ]4}
t|
ƒ}||	kr®| |
¡ qŽ|	 |¡ | |
¡ qŽ|r|dkr|| }|D ]}| d	|||d
 |f¡ qà|d  t|ƒ7  < | ¡  |D ]x}
|
d
 }| d||f¡ |jdkrLq| ¡  z4t|
t	j
d t	j
d t	j
d t	j
d d\}}}W nB tk
rÊ } z"d}dt|ƒj› dt|ƒ› }W 5 d}~X Y nX |r|}t|
ƒ}| d|||||f¡ |d  d7  < | ¡  q|pd}t|
d ƒd }t|ƒrj|t||ƒ }| d|||||f¡ |d  d7  < n$| d||||f¡ |d  d7  < | ¡  q| d| f¡ | ¡ }|D ]„}
|
d
 }t|
ƒ\}}|sÖq´|d kr
|d!krîd"nd#}| d$|||||f¡ n| d%||||f¡ |d&  d7  < | ¡  q´| ¡  td'||d(œƒd)fS )*u  
    Cron endpoint:
      - Auth: X-Api-Key
      - Procesa jobs due (scheduled) y refresca jobs submitted.

    Mejoras:
      - Reintento con backoff para errores transitorios (incl. 5xx/internal_error).
      - Cooldown por cuenta para no disparar 2 publicaciones seguidas en segundos.
      - ProtecciÃ³n anti-atasco: si el worker lanza excepciÃ³n, requeue en lugar de dejar "submitting".
    Úmax_jobsÚ10ZBULK_MIN_SECONDS_BETWEEN_POSTSZ180r¾   r   )Úscheduled_submittedÚscheduled_failedÚscheduled_requeuedÚsubmitted_updatedÚscheduled_deferred_by_cooldownz–
        SELECT * FROM bulk_jobs
        WHERE status='scheduled' AND next_attempt_ts <= ?
        ORDER BY publish_at_ts ASC
        LIMIT ?
        z´
                UPDATE bulk_jobs
                SET next_attempt_ts=?, updated_at_ts=?
                WHERE id=? AND status='scheduled' AND next_attempt_ts <= ?
                r³   rÍ   z
            UPDATE bulk_jobs
            SET status='submitting', updated_at_ts=?
            WHERE id=? AND status='scheduled'
            r   r†   rŠ   r‹   rŒ   )rª   rŽ   r   r   Fzworker_exception: z: Na¥  
                UPDATE bulk_jobs
                SET status='submitted',
                    publish_id=?,
                    attempts=attempts+1,
                    last_error=NULL,
                    last_status=NULL,
                    last_fail_reason=NULL,
                    access_token=?,
                    next_attempt_ts=?,
                    updated_at_ts=?
                WHERE id=?
                rÉ   Zunknown_errorr¸   a  
                UPDATE bulk_jobs
                SET status='scheduled',
                    attempts=?,
                    last_error=?,
                    next_attempt_ts=?,
                    updated_at_ts=?
                WHERE id=?
                rË   zØ
                UPDATE bulk_jobs
                SET status='failed',
                    attempts=?,
                    last_error=?,
                    updated_at_ts=?
                WHERE id=?
                rÊ   z}
        SELECT * FROM bulk_jobs
        WHERE status='submitted'
        ORDER BY updated_at_ts ASC
        LIMIT ?
        )ÚPUBLISH_COMPLETEZFAILEDZSEND_TO_USER_INBOXrÎ   ÚcompleteZfailedzÚ
                UPDATE bulk_jobs
                SET status=?,
                    last_status=?,
                    last_fail_reason=?,
                    updated_at_ts=?
                WHERE id=?
                z¼
                UPDATE bulk_jobs
                SET last_status=?,
                    last_fail_reason=?,
                    updated_at_ts=?
                WHERE id=?
                rÌ   T)rn   Ú	processedrÅ   r›   ) r   rX   r   rœ   r   r¡   r¢   Úenvironr   r   r    r,   r   r   r-   rg   r   Úaddr1   rÃ   Úrowcountr   r   ÚtypeÚ__name__rM   r   rP   r^   r   rÄ   r   )rÇ   rÅ   Z
cooldown_sr4   r   rÐ   ZdueZpickedÚdeferredÚseenrd   ÚkeyZbump_tsr   r¿   rn   ÚmsgZ
_init_resprA   r™   Zused_access_tokenr:   rQ   Znext_tsÚsubsr·   Zfail_reasonZ
new_statusr    r    r!   rÆ   ™  sÒ    û	ù	

ú
úû,ò

	ö
÷ù	
÷
ø)-r¢   r¡   rÀ   rY   Úflaskr   r   r   r   Údb.bulkr   Úservices.authr   r   Úservices.creatorr	   r
   Úservices.mediar   r   r   Zservices.bulk_workerr   r   r   Útiktok_clientr   r   r   rÕ   ÚbprM   r"   Údictr9   ÚboolrP   rX   r^   rg   Úrouteri   r®   r¯   rÆ   r    r    r    r!   Ú<module>   s0   
6
}

o