U
    ԝiD                     @   s|  d dl mZmZmZmZmZmZmZmZm	Z	m
Z
mZ d dlZd dlZd dlZd dlZd dlZd dlZd dlmZ d dlmZmZmZmZ d dlmZmZmZmZmZ d dlmZm Z  d dl!m"Z" d dl#m$Z$m%Z%m&Z&m'Z'm(Z( d d	l)m*Z*m+Z+ d d
l,m-Z-m.Z.m/Z/ d dl0m1Z1m2Z2 d dl3m4Z4m5Z5m6Z6 d dl7m8Z8m9Z9 d dl:m;Z;m<Z<m=Z= ee>Z?ee?_@ee?jd< ee?jd< ee?jd< ee?jd< ejABejACeDZEejAFeEdZGejHeGdd ejAFeEdZIejHeIdd ejAFeEdZJe*eJ e  dZKdddhZLdddhZMejNeOdd d!ZPejNeQdd"d#d$ZReOeOd%d&d'ZSd(d) ZTeOeOd*d+d,ZUeVd-d.d/ZWd0d1 ZXeOd2d3d4ZYeOeVd5d6d7ZZeOd-d8d9Z[eOd-d:d;Z\eOeOd<d=d>Z]ej^d?d@dAZ_dBdC Z`dDdE ZaeOd2dFdGZbe?cdHeOdIdJdKZde?edLdMdN Zfe?edOdPdQ Zge?edRdSdT Zhe?edUdVdW Zie?edXdYdZ Zje?jed[d\d]gd^d_d` Zke?jedad]gd^dbdc Zle?cdddedf Zme?ndgeOdhdidjZoe?ndkeOdhdldmZpe?ndndodp Zqe?edqdrds Zre?edtdudv Zse?jedwd]gd^dxdy Zte?jedzd]gd^d{d| Zue?jed}d]gd^d~d Zve?jedd]gd^dd Zwe?jedd]gd^dd Zxe?jedd]gd^dd Zye?jedd]gd^dd Zze?jedd\gd^eOdddZ{e?jedd\gd^eOdddZ|e?jedd]gd^eOdddZ}e?jedd\gd^eOdddZ~e?jedd\gd^eOdddZe?cdeOdddZe?cddd Ze>dkrxe?jdddd dS )    )Flaskrender_templaterequestredirecturl_forsessionflashjsonify	send_filesend_from_directoryabortN)datetime)OptionalDictAnyTuple)FLASK_SECRET_KEYPUBLIC_BASE_URLMEDIA_SIGNING_SECRETMEDIA_TOKEN_TTL_SECONDSADMIN_PASSWORD)init_token_db
save_token)verify_media_token)build_authorize_urlexchange_code_for_tokenquery_creator_infoupload_video_direct_postfetch_post_status)init_bulk_dbbulk_db)require_bulk_api_keyget_access_token_for_aliasrequire_login_access_token)
save_draft
load_draft)safe_filenamevideo_duration_seconds_ffprobebuild_public_media_url)creator_can_post_nowparse_publish_at_iso_to_ts)resolve_access_token_for_jobprocess_one_scheduled_jobrefresh_submitted_job_statusr   r   r   r   ZuploadsT)exist_okZurl_propertieszbulk_jobs.sqlite3Zadmin_authenticatedcompletefailed	cancelled	scheduled
submitting	submitted)conn
table_namec              	   C   sf   |   }|d| d g }| D ]:}z||d  W q& tk
r^   ||d  Y q&X q&|S )NzPRAGMA table_info()name   )cursorexecutefetchallappend	Exception)r5   r6   curcolsrow rB   app.py_table_columns\   s    rD   )r5   jobreturnc                    sh   t | d  fdd| D }d| }ddgt| }| d| d| dt|  d S )	NZ	bulk_jobsc                    s"   i | ]\}}|t  kr||qS rB   )set).0kvr@   rB   rC   
<dictcomp>j   s       z$_insert_bulk_job.<locals>.<dictcomp>z, ?zINSERT INTO bulk_jobs (z
) VALUES (r7   )rD   itemsjoinkeyslenr;   tuplevalues)r5   rE   ZfilteredZcolnamesZplaceholdersrB   rK   rC   _insert_bulk_jobh   s
    
rT   )valuerF   c                 C   s   | pd   S N striplower)rU   rB   rB   rC   _normalize_job_statusp   s    r[   c                 C   s8   | sd S | d | d | d | d | d | d | d dS )	Nidstatuspublish_at_tsupdated_at_ts
publish_idlast_statuslast_fail_reason)job_idr]   r^   r_   r`   ra   rb   rB   rA   rB   rB   rC   _serialize_job_matcht   s    re   account_aliasoriginal_filenamec           	      C   s   t t}| }|d| |f | }|  d}d}|D ]:}t|d }|tkrb|dkrb|}q<|tkr<|dkr<|}q<| |t	||dkt
|t
|dS )u   
    Devuelve coincidencias por alias + nombre de archivo original.

    Solo los jobs activos bloquean una nueva programaciÃ³n.
    Los jobs terminales deben permitir re-subir/reprogramar el video.
    a  
        SELECT id, status, publish_at_ts, updated_at_ts, publish_id, last_status, last_fail_reason
        FROM bulk_jobs
        WHERE account_alias=? AND original_filename=?
        ORDER BY
          CASE
            WHEN LOWER(COALESCE(status, '')) IN ('scheduled', 'submitting', 'submitted') THEN 0
            ELSE 1
          END,
          publish_at_ts DESC,
          updated_at_ts DESC
        Nr]   )rg   rh   Zmatches_totalZcan_scheduleactive_matchlatest_terminal_match)r    BULK_DB_PATHr:   r;   r<   closer[   ACTIVE_JOB_STATUSESTERMINAL_JOB_STATUSESrQ   re   )	rg   rh   r5   r?   rowsri   rj   rA   r]   rB   rB   rC   *_lookup_existing_jobs_by_original_filename   s.    rp   )rF   c                   C   s   t tdkS )NT)r   getADMIN_SESSION_KEYrB   rB   rB   rC   _is_admin_authenticated   s    rs   c                  C   s,   t  r
d S tjrtjntj} ttd| dS )Nadmin_login)next)rs   r   Zquery_string	full_pathpathr   r   next_urlrB   rB   rC   _admin_guard   s    rz   )stored_filenamec                 C   s:   | pd  }|r(d|ks(d|ks(d|kr,d S tjt|S )NrW   /\..)rY   osrw   rO   
UPLOAD_DIR)r{   filenamerB   rB   rC   _job_file_path   s    r   )r{   rF   c                 C   s   t | }t|otj|S )N)r   boolr   rw   isfile)r{   rw   rB   rB   rC   _job_has_file   s    r   c                 C   sD   | dkrdS zt t| dW S  tk
r>   t|  Y S X d S )NrV   -z%Y-%m-%d %H:%M:%S)r   Zfromtimestampintstrftimer>   str)ZtsrB   rB   rC   
_format_ts   s    r   c                 C   s|   | dkrdS ddddg}t | }|d }|D ]$}|dk sD||d	 krH qR|d
 }q,|dkrj|dd| S t| d| S )NrV   r   BZKBZMBZGBr   i   g      @z.1f )floatr   )sizeZunitsrU   ZunitrB   rB   rC   _format_bytes   s    
r   )r]   rF   c                 C   sD   | pd   }|dkrdS |dkr(dS |dkr4dS |dkr@dS d	S )
NrW   r/   okr0   errr1   warn   r3   r4   inforX   )r]   strB   rB   rC   _status_tone   s    r   rd   c                 C   s   t | }|dpd  }|tk|d< t||d< t|dpDd|d< t|d|d< t|d	|d
< t|d|d< t|d|d< |d rt|dpd}zt	j
||d< W q tk
r   d |d< Y qX nd |d< t|d |d< |S )Nr]   rW   is_terminalZstatus_toner{   has_filecreated_at_tsZcreated_at_labelr^   Zpublish_at_labelnext_attempt_tsZnext_attempt_labelr_   Zupdated_at_labelZfile_size_bytesZfile_size_label)dictrq   rY   rZ   rn   r   r   r   r   r   rw   getsizeOSErrorr   )rA   rE   r]   	file_pathrB   rB   rC   _job_for_admin   s$    r   c                  C   sh   t t} tj| _|  }|d | }|   g }|D ](}t	|}|d rX|d sXq:|
| q:|S )Nz
        SELECT * FROM bulk_jobs
        ORDER BY
          CASE
            WHEN status IN ('scheduled', 'submitting', 'submitted') THEN 0
            ELSE 1
          END,
          publish_at_ts ASC,
          updated_at_ts DESC
        r   r   )r    rk   sqlite3Rowrow_factoryr:   r;   r<   rl   r   r=   )r5   r?   ro   jobsrA   rE   rB   rB   rC   _load_admin_jobs  s    r   c                 C   s   i }| D ].}| dpd }|s$q||g | qg }ttD ]r}| sTqF| }| |j	g }||j	|j
t|j
tt|jt|tdd |D tdd |D d qF|jdd d	 |S )
Nr{   rW   c                 s   s   | ]}|d  rdndV  qdS )r   r   r9   NrB   rH   jrB   rB   rC   	<genexpr>4  s     z)_load_upload_inventory.<locals>.<genexpr>c                 s   s   | ]}|d  rdndV  qdS )r   r9   r   NrB   r   rB   rB   rC   r   5  s     )r{   Z
size_bytesZ
size_labelZmodified_at_labellinked_jobs_countactive_jobs_countZterminal_jobs_countc                 S   s    | d dk| d  | d   fS )Nr   r   r   r{   )rZ   )itemrB   rB   rC   <lambda>:  s    

z(_load_upload_inventory.<locals>.<lambda>)key)rq   rY   
setdefaultr=   r   scandirr   is_filestatr8   st_sizer   r   r   st_mtimerQ   sumsort)r   Z	job_linksrE   r{   filesentryr   Zlinked_jobsrB   rB   rC   _load_upload_inventory  s4    r   c                 C   s   t t}tj|_| }|d| f | }|  d}d}|D ]>}t	|d pRd}|d p`d
  tkrx||7 }qB||7 }qB||fS )Nz
        SELECT status, COUNT(*) AS total
        FROM bulk_jobs
        WHERE stored_filename=?
        GROUP BY status
        r   totalr]   rW   )r    rk   r   r   r   r:   r;   r<   rl   r   rY   rZ   rn   )r{   r5   r?   ro   ZactiveZterminalrA   r   rB   rB   rC   _count_jobs_for_fileC  s"    	

r   z/media/<path:token_or_file>)token_or_filec                 C   s   |   dr@tjt| }tj|s0td tt| dddS t	| t
jd }|s\td |d}|srtd	 d
|ksd|ksd|krtd	 tjt|}tj|std tt|dddS )Nz.txt  Fz
text/plain)as_attachmentmimetyper   i  f  r|   r}   r~   	video/mp4)rZ   endswithr   rw   rO   URLPROP_DIRexistsr   r   r   appconfigrq   r   )r   ZfullZpayloadr   rv   rB   rB   rC   mediaa  s"    
r   r|   c                  C   s   t dd k	} td| dS )Ntiktok_tokenzlanding.htmlZis_connectedr   rq   r   r   rB   rB   rC   landing  s    r   /appc                  C   s   t dd k	} td| dS )Nr   z
index.htmlr   r   r   rB   rB   rC   app_home  s    r   z/contactc                  C   s   t dd k	} td| dS )Nr   zcontact.htmlr   r   r   rB   rB   rC   contact  s    r   z/tosc                   C   s   t dS )Nztos.htmlr   rB   rB   rB   rC   tos  s    r   z/privacyc                   C   s   t dS )Nzprivacy.htmlr   rB   rB   rB   rC   privacy  s    r   z/admin/loginZGETPOST)methodsc                  C   s   t  rttdS tjdp,tjdp,d } tjdkrtjdpJd}t	t
jdp\d}|rt||rdtt< dt_tdd	 | d
rt| S ttdS tdd td| dS )N
admin_jobsru   rW   r   Zpasswordr   TzAcceso admin concedido.successr|   u   Contraseña admin incorrecta.errorzadmin_login.htmlrx   )rs   r   r   r   argsrq   formrY   methodr   r   r   secretsZcompare_digestr   rr   modifiedr   
startswithr   )ry   ZprovidedZexpectedrB   rB   rC   rt     s     



rt   z/admin/logoutc                   C   s(   t td  dt _tdd ttdS )NTu   Sesión admin cerrada.r   rt   )r   poprr   r   r   r   r   rB   rB   rB   rC   admin_logout  s    
r   z/adminc                  C   sN   t  } | r| S t }dd |D }dd |D }t|}td|||t|dS )Nc                 S   s   g | ]}|d  s|qS r   rB   rH   rE   rB   rB   rC   
<listcomp>  s      zadmin_jobs.<locals>.<listcomp>c                 S   s   g | ]}|d  r|qS r   rB   r   rB   rB   rC   r     s      zadmin_jobs.html)in_progress_jobsfinished_jobsupload_filesZ
total_jobs)rz   r   r   r   rQ   )guardr   r   r   r   rB   rB   rC   r     s    r   z/admin/jobs/<job_id>/cancel)rc   c                 C   s  t  }|r|S tt}tj|_| }|d| f | }|s^|	  t
dd ttdS |d phd  }|tkr|	  t
dd ttdS tt }|d	||| f |  |	  |jd
krt
dd ttdS |dkrt
dd n
t
dd ttdS )N"SELECT * FROM bulk_jobs WHERE id=?El trabajo indicado no existe.r   r   r]   rW   z!Ese trabajo ya estaba finalizado.warningz
        UPDATE bulk_jobs
        SET status='cancelled',
            next_attempt_ts=?,
            last_status='CANCELLED_BY_ADMIN',
            updated_at_ts=?
        WHERE id=? AND status NOT IN ('complete', 'failed', 'cancelled')
        r9   uG   No se pudo cancelar el trabajo porque su estado cambió mientras tanto.r   ul   Trabajo cancelado localmente. Si TikTok ya había aceptado el envío, esta acción no puede retirarlo allí.z Trabajo cancelado correctamente.r   )rz   r    rk   r   r   r   r:   r;   fetchonerl   r   r   r   rY   rZ   rn   r   timecommitrowcount)rc   r   r5   r?   rA   r]   now_tsrB   rB   rC   admin_cancel_job  sD    




r   z/admin/jobs/<job_id>/relaunchc                 C   s  t  }|r|S tt}tj|_| }|d| f | }|s^|	  t
dd ttdS |d phd  }|tkr|	  t
dd ttdS |d pd}t|}|rtj|s|	  t
d	d ttdS t|d
 pd p|}t j}	|	 d| }
t|
}zt|| W nL tk
rn } z,|	  t
d| d ttd W Y S d }~X Y nX tt }|	|||ddd |d |d |d |d |d |d |d |d |d |d |d |
|dd d d |d}zt|| |  W nr tk
rh } zR|	  zt | W n tk
r6   Y nX t
d| d ttd W Y S d }~X Y nX |	  t
d|	 d ttdS )Nr   r   r   r   r]   rW   z-Solo se pueden relanzar trabajos finalizados.r{   z=No se puede relanzar porque el archivo original ya no existe.rh   _z6No se pudo duplicar el archivo para el relanzamiento: r2   r   access_tokenrg   titleprivacy_levelallow_comment
allow_duetallow_stitchcommercial_togglebrand_organic_togglebrand_content_toggleis_aigc)r\   r   r^   r   r]   attemptsZ
last_errorr   rg   r   r   r   r   r   r   r   r   r   r{   rh   	video_urlr`   ra   rb   r_   z#No se pudo crear el nuevo trabajo: zTrabajo relanzado. Nuevo job: r   )!rz   r    rk   r   r   r   r:   r;   r   rl   r   r   r   rY   rZ   rn   r   r   rw   r   r&   uuiduuid4hexshutilZcopy2r   r   r   rT   r   r>   remove)rc   r   r5   r?   rA   r]   Zsource_filenamesource_pathrh   Z
new_job_idZnew_stored_filenameZnew_file_pathexcr   rE   rB   rB   rC   admin_relaunch_job	  s    



"
"r  z/admin/files/deletec               
   C   s
  t  } | r| S tjdpd }t|}|sDtdd ttdS t	|\}}t
j|srtdd ttdS zt
| W nB tk
r } z$td| d ttd W Y S d }~X Y nX |d	krtd
| dd n|d	krtdd n
tdd ttdS )Nr{   rW   u   Nombre de archivo inválido.r   r   z&El archivo ya no existe en el sistema.r   zNo se pudo borrar el archivo: r   u   Archivo borrado. Ojo: había uE    trabajo(s) en curso usando ese vídeo y ahora quedarán sin fichero.uV   Archivo borrado. Los trabajos finalizados asociados dejarán de mostrarse en el panel.r   zArchivo borrado del sistema.)rz   r   r   rq   rY   r   r   r   r   r   r   rw   r   r   r   )r   r{   r   Zactive_jobsZterminal_jobsr  rB   rB   rC   admin_delete_filea  s4    

"

r  z/loginc                  C   s:   t jdpd } td}|td< | td< tt|S )z
    Optional: /login?alias=en  or /login?alias=es
    This alias will be used to persist tokens server-side for cron/worker.
    aliasdefault   oauth_stateoauth_alias)	r   r   rq   rY   r   Z	token_hexr   r   r   )r  staterB   rB   rC   login  s
    
r  z/tiktok/callbackc               
   C   s   t jd} | r(td|  d tdS t jd}t jd}|rV|rV|tdkrhtdd tdS zHt|}|td< d	t_td
pd }t	|| td| dd W n2 t
k
r } ztd| d W 5 d }~X Y nX tdS )Nr   zTikTok error: r   coder
  r  z$Invalid OAuth state or missing code.r   Tr	  r  z$Successfully connected with TikTok (z).r   zError exchanging code: )r   r   rq   r   r   r   r   r   rY   r   r>   )r   r  r
  Z
token_datar  erB   rB   rC   tiktok_callback  s&    

"r  z/logoutc                   C   s*   t dd  t dd  tdd tdS )Nr   ZdraftszDisconnected.r   r   )r   r   r   r   rB   rB   rB   rC   logout  s    
r  z/api/bulk/publishc                  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 |stddd	dfS d
tj	krtddd	dfS tj	d
 }|j
stddd	dfS tjdpd }tjdpd }|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rt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
}t j}| d%| }tjt|}|| |d&}t|}t|trT|d'krT|d'krT||krTzt| W n tk
r<   Y nX tdd(||d)dfS t|tjd* tjd+ tjd, d-}zNt||||||||
|d.|d/}|d0pi d1}td"||||d2d3fW S  tk
r } z tdd4t|ddf W Y S d }~X Y nX d S )5Nrg   rW   r   Falias_token_failedr   r   Zdetailsr   %missing_access_token_or_account_aliasr   r   videomissing_videomissing_filenamer   r   missing_privacy_levelr   1r   r   r   0r   r   r   	SELF_ONLYbranded_cannot_be_self_onlyZcreator_info_failedcannot_post_now)r   r   reasonprivacy_level_optionsZinvalid_privacy_level)r   r   optionscomment_disabledTduet_disabledstitch_disabledr   max_video_post_duration_secr   Zduration_exceeds_max)r   r   durmaxr   r   r   r{   public_base_urlsigning_secretttl_secondsPULL_FROM_URLr   Zcaptionr   disable_commentdisable_duetdisable_stitchr   r   r   moder   datar`   )r   r`   r   	init_respr{      Zdirect_post_failed)r!   r   r   rq   rY   r"   r>   r	   r   r   r   r   r)   r&   r   r   r   r   rw   rO   r   saver'   
isinstancer   r   r(   r   r   r   )rg   r   r  filer   r   r   r   r   r   r   r   r   creator_infocan_post_nowr  r  r,  r-  r.  original_fndraft_idr{   	save_pathmax_durr$  r   r1  r`   rB   rB   rC   api_bulk_publish  s    .

.




*	r<  z/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pd }|r|st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 )Nrg   rW   r   Fr  r  r   r`   Z"missing_access_token_or_publish_idr  Tr   r0  r2  )
r!   r   r   rq   rY   r"   r>   r	   r   r   )rg   r   r  r`   r0  rB   rB   rC   api_bulk_status>  s     .
r>  z/api/bulk/find_existingc                  C   s   t   tjdpd } tjdp4tjdp4d }| sPtddddfS |sftdd	ddfS t|}t| |d
}tddi|dfS )Nrg   rW   rh   r   FZaccount_alias_requiredr  r   Zmissing_original_filenamerf   r   Tr2  )r!   r   r   rq   rY   r	   r&   rp   )rg   Zoriginal_filename_rawrh   lookuprB   rB   rC   api_bulk_find_existingV  s     r@  z/api/bulk/schedulec                  C   s0  t   tjdpd } tjdp(d }| r|szt| }W n> tk
r } z tddt|ddf W Y S d }~X Y nX |stddd	dfS tjd
pd }|st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sRtddd	dfS tjdpbd }tjdpxd }|stddd	dfS tjdpd  }|dkrt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}|rl|d krltdd!d	dfS t|j}t| |d"}d }|dkr|d# }n|d$kr|d# p|d% }|rt|d& }td'd'|tkrd(nd)|d* |d& |d+ || pd |d,	d-fS t j}| d.| }tjt|}|| t|tjd/ tjd0 tjd1 d2}tt }tt}|  }|!d3||||d4d5d || pd |||	rd6nd5|
rd6nd5|rd6nd5|rd6nd5|rd6nd5|rd6nd5|rd6nd5|||d d d |f |"  |#  td'||||| p$d d7d-fS )8Nrg   rW   r   Fr  r  r   r  r  
publish_atZmissing_publish_atZinvalid_publish_atr  r  r  r   r   r  dedupe_modeactive_only>   noneallrC  Zinvalid_dedupe_moder   r  r   r   r   r  r   r   r   r  r  rf   ri   rE  rj   r]   TZalready_scheduledZalready_existsrc   r^   )	r   Zskippedr  rc   r]   r^   rh   rg   rB  r2  r   r   r   r   r&  a  
        INSERT INTO bulk_jobs (
          id, created_at_ts, publish_at_ts, next_attempt_ts, status, attempts, last_error,
          access_token, account_alias,
          title, privacy_level,
          allow_comment, allow_duet, allow_stitch,
          commercial_toggle, brand_organic_toggle, brand_content_toggle, is_aigc,
          stored_filename, original_filename, video_url,
          publish_id, last_status, last_fail_reason,
          updated_at_ts
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        r2   r   r9   )r   rc   r^   r{   r   rg   )$r!   r   r   rq   rY   r"   r>   r	   r   r*   r   r   rZ   r&   rp   r[   rm   r   r   r   r   rw   rO   r   r3  r(   r   r   r   r   r    rk   r:   r;   r   rl   )rg   r   r  rA  r^   r5  r   r   rB  r   r   r   r   r   r   r   r8  Zexisting_lookupZexisting_matchZexisting_statusrc   r{   r:  r   r   r5   r?   rB   rB   rC   api_bulk_schedulem  s    ..







*	rF  z/api/bulk/process_duec               	   C   st  t   ttjddpd} tt }tt}| }ddddd}|	d|| f |
 }|D ]F}|d }|	d||f |jdkrqb|  t|ttjd	 tjd
 tjd d\}}	}
|r|	}t|}|	d|||||f |jdkr|d  d7  < |  qb|	}t|d d }|drr|dkrr|d }|	d|||||f |jdkr|d  d7  < n0|	d||||f |jdkr|d  d7  < |  qb|	d| f |
 }|D ]}|d }t|\}}|sq|dkr|dkr dnd}|	d|||||f n|	d||||f |jdkrL|d  d7  < |  q|  td||d d!fS )"Nmax_jobsZ10r   )scheduled_submittedscheduled_failedscheduled_requeuedsubmitted_updatedz
        SELECT * FROM bulk_jobs
        WHERE status='scheduled' AND next_attempt_ts <= ?
        ORDER BY publish_at_ts ASC
        LIMIT ?
        r\   z
            UPDATE bulk_jobs
            SET status='submitting', updated_at_ts=?
            WHERE id=? AND status='scheduled'
            r9   r   r   r   )Z
upload_dirr'  r(  r)  a  
                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=? AND status='submitting'
                rH  r   r     i,  a  
                UPDATE bulk_jobs
                SET status='scheduled',
                    attempts=?,
                    last_error=?,
                    next_attempt_ts=?,
                    updated_at_ts=?
                WHERE id=? AND status='submitting'
                rJ  z
                UPDATE bulk_jobs
                SET status='failed',
                    attempts=?,
                    last_error=?,
                    updated_at_ts=?
                WHERE id=? AND status='submitting'
                rI  z}
        SELECT * FROM bulk_jobs
        WHERE status='submitted'
        ORDER BY updated_at_ts ASC
        LIMIT ?
        )PUBLISH_COMPLETEZFAILEDZSEND_TO_USER_INBOXrM  r/   r0   z
                UPDATE bulk_jobs
                SET status=?,
                    last_status=?,
                    last_fail_reason=?,
                    updated_at_ts=?
                WHERE id=? AND status='submitted'
                z
                UPDATE bulk_jobs
                SET last_status=?,
                    last_fail_reason=?,
                    updated_at_ts=?
                WHERE id=? AND status='submitted'
                rK  T)r   	processedr   r2  )r!   r   r   r   rq   r   r    rk   r:   r;   r<   r   r   r,   r   r   r   r+   r   r-   rl   r	   )rG  r   r5   r?   rN  ZduerA   rc   r   msgZ
_init_respr`   Zused_access_tokenr   Zattempts_nextZnext_tsZsubsr]   Zfail_reasonZ
new_statusrB   rB   rC   api_bulk_process_due  s    	

	

	

rP  z/upload_localc               
   C   s   t  } | stdd tdS dtjkr8tdd tdS tjd }|jsZtdd tdS t|j}t j	}| d| }t
jt|}|| t||||d d d dd	 ttd
|dS )N!Please connect with TikTok first.r   r   r  zNo video file provided.zNo file selected.r   Zlocal_uploaded)r   r{   rh   r`   last_init_responseposting_summaryr]   ui.post_to_tiktokr9  )r#   r   r   r   r   r   r&   r   r   r   r   rw   rO   r   r3  r$   r   )r   r5  r8  r9  r{   r:  rB   rB   rC   upload_local  s8    







rV  z/static_preview/<draft_id>rU  c                 C   sJ   t  }|sdS t| }|sdS |d}|r8tj|s<dS t|dddS )N)zNot authenticated  )z	Not foundr   r   r   F)r   r   )r#   r%   rq   r   rw   r   r
   )r9  r   draftrw   rB   rB   rC   static_preview  s    
rY  z/post/<draft_id>c                 C   s2  t  }|stdd tdS t| }|s:tdd tdS zt|}W n> tk
r } z td| d td W Y S d }~X Y nX t|\}}|d}t|d }d}	d }
t	|t
r|d	kr|d	kr||krd
}	d||f }
d }|d}|rt|tjd tjd tjd d}td| |||	|
|||d	S )NrQ  r   r   Draft not found.8Cannot post right now. Please try again later. Details: r#  r   Tr   Fz7Video is %.1fs but max allowed is %ss for this creator.r{   r   r   r   r&  zpost_to_tiktok.html)r9  rX  r6  duration_okduration_msgpull_urlr7  cannot_post_reason)r#   r   r   r%   r   r>   r)   rq   r'   r4  r   r(   r   r   r   )r9  r   rX  r6  r  r7  r_  r;  r$  r\  r]  r^  r{   rB   rB   rC   post_to_tiktok  sR    


"
r`  z/post/<draft_id>/publishc                 C   sL  t  }|stdd tdS t| }|s:tdd tdS zt|}W nF tk
r } z(td| d ttd| d W Y S d }~X Y nX tj	dd	krtd
d ttj
ptdS t|\}}|st|pdd ttd| dS tj	dpd }|s tdd ttj
ptdS tj	dp0d }tj	ddk}	tj	ddk}
tj	ddk}|r|dkrtdd ttd| dS |	dpg }||krtdd ttd| dS tj	ddk}tj	ddk}tj	ddk}|	ddkrd}|	ddkrd}|	d dkr0d}| }| }| }d}d}|	r|
sv|svtd!d ttd| dS |
}|}|r|dkrtd"d ttd| dS |	d#}t|d$ }t|tr
|d%kr
|d%kr
||kr
td&||f d ttd| dS |	d'}|s4td(d ttd| dS tjt|}tj|sjtd)d ttd| dS t|tjd* tjd+ tjd, d-}zrt||||||||dd.|d/}|	d0pi 	d1}t| |||||||||	|
|d2d3|d4 ttd5| dW S  tk
rF } z(td6| d ttd| d W Y S d }~X Y nX d S )7NrQ  r   r   rZ  r[  rT  rU  Zmusic_confirmr  uE   You must agree to TikTok’s Music Usage Confirmation before posting.zui.app_homezJThis TikTok account cannot publish at this moment. Please try again later.r   rW   z1Please select a Privacy status before publishing.r   r   Zon
your_brandbranded_contentr  zIBranded content cannot be posted with privacy set to Only me (SELF_ONLY).r  z'You must select a valid privacy option.r   r   r   r   TFr!  r"  z]If commercial content disclosure is on, you must select Your brand, Branded content, or both.z<Branded content visibility cannot be set to private/only me.r#  r   r   z1Video duration %.1fs exceeds allowed maximum %ss.r{   z2Internal error: missing stored_filename for draft.zVideo file not found on server.r   r   r   r&  r*  r+  r0  r`   )r   r   r   r   r   r   ra  rb  Zposted)rR  r`   rS  r]   r   zui.status_pagezError posting to TikTok: )r#   r   r   r%   r   r>   r   r   r   rq   Zreferrerr)   rY   r'   r4  r   r   rw   rO   r   r   r(   r   r   r   r$   )r9  r   rX  r6  r  r7  r_  r   r   r   ra  rb  r  r   r   r   r,  r-  r.  r   r   r;  r$  r{   
local_pathr   r1  r`   rB   rB   rC   publish  s    

&






*



rd  z/status/<draft_id>c                 C   sH   t  }|stdd tdS t| }|s:tdd tdS td| |dS )NrQ  r   r   rZ  zstatus.html)r9  rX  )r#   r   r   r%   r   )r9  r   rX  rB   rB   rC   status_page  s    

re  z/api/status/<draft_id>c              
   C   s   t  }|stddddfS t| }|s:tddddfS |d}|sZt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 )NFnot_authenticatedr  rW  draft_not_foundr   r`   TZno_publish_id_yet)r   r
  r2  r=  )r#   r	   r%   rq   r   r>   r   )r9  r   rX  r`   r0  r  rB   rB   rC   
api_status  s    

rh  z/api/media_url/<draft_id>c                 C   s   t  }|stddddfS t| }|s:tddddfS |d}|sZtdddd	fS tjt|}tj|stdd
ddfS t	|t
jd t
jd t
jd d}td|dS )NFrf  r  rW  rg  r   r{   Zmissing_stored_filenamei  Zfile_not_foundr   r   r   r&  T)r   url)r#   r	   r%   rq   r   rw   rO   r   r   r(   r   r   )r9  r   rX  r{   rc  ri  rB   rB   rC   api_media_url  s&    
rj  z/healthzc                   C   s   t ddidfS )Nr   Tr2  )r	   rB   rB   rB   rC   healthz  s    rk  __main__z0.0.0.0iI"  F)ZhostZportdebug)Zflaskr   r   r   r   r   r   r   r	   r
   r   r   r   r   r   r   r   r   r   typingr   r   r   r   r   r   r   r   r   r   Ztoken_storer   r   Zmedia_tokensr   Ztiktok_clientr   r   r   r   r   Zdb.bulkr   r    Zservices.authr!   r"   r#   Zservices.draftsr$   r%   Zservices.mediar&   r'   r(   Zservices.creatorr)   r*   Zservices.bulk_workerr+   r,   r-   __name__r   Z
secret_keyrw   dirnameabspath__file__ZBASE_DIRrO   r   makedirsr   rk   rr   rn   rm   Z
Connectionr   rD   r   rT   r[   re   rp   r   rs   rz   r   r   r   r   r   r   r   r   r   r   rq   r   Zrouter   r   r   r   r   rt   r   r   Zpostr   r  r  r  r  r  r<  r>  r@  rF  rP  rV  rY  r`  rd  re  rh  rj  rk  runrB   rB   rB   rC   <module>   s   4	





.	%







4W
'



~


 
 &
&2 

