a
    pNjZ                     @   s  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	 dZ
ee jZdZed d Zed d Zed d	 Zed d
 Zed Zed Zed Zed ZddgZdgZg dZdgZg dZed Zed Zed ZdZ ee  Z!dZ"dZ#dZ$i a%dd Z&e'ddd Z(ed!d"d#Z)ed!d$d%Z*d&d' Z+ed!d(d)Z,e'e'e-d*d+d,Z.e'd-d.d/Z/ee'e'd0d1d2Z0d3d4 Z1d5d6 Z2d7d8 Z3d9d: Z4d;d< Z5d=d> Z6d?d@ Z7dAdB Z8dCdD Z9dEdF Z:e'dGdHdIZ;dRdKdLZ<dMdN Z=dOdP Z>e?dQkre>  dS )S    N)Path)quote   z7https://files.vmvproductions.net/downloads/FikaSPT4_DB/	manifestszclient_mods_manifest.jsonzserver_mods_manifest.jsonzdb_include_index.jsonz.rebuild_db_include_indexinclude_list.txtinclude_fika_client.txtexclude_list.txtexclude_fika_client.txtzBepInEx/patcherszBepInEx/pluginszSPT/user/mods)zBepInEx/plugins/sptzBepInEx/plugins/Fikaz!BepInEx/patchers/spt-prepatch.dllzSPT/user/mods/fika-server)	r   logsz&fika_unified_generator_and_notifier.pyfika_changelog_cache.jsonfika_hash_cache.jsonr   r   r   r	   r   r   r
   zfika_unified.logi      zyhttps://discord.com/api/webhooks/1429795721560854638/jE04Pl-PICQQU3-A0Pywlw7eE5Le4JkV9EvqHsDP9hM1YbfskaTcV3KE4e08GVFAveO1c                  C   s   t jddd t sd S t jtk r,d S t t dt  } |  rN| 	  t
td ddD ]>}t t d|  }t t d|d   }| r^|| q^t t d }t| d S )NTparentsexist_ok.   r   z.1)LOG_DIRmkdirLOG_FILEexistsstatst_sizeMAX_LOG_SIZEBASE_LOG_NAMEMAX_LOG_FILESunlinkrangerename)ZoldestisrcZdstZrotated r"   d/mnt/user/appdata/webserver-php8.4/html/downloads/FikaSPT4_DB/fika_unified_generator_and_notifier.pyrotate_logsl   s    r$   )msgc                 C   sr   t jddd td}| d|  }t|dd tjddd}||d	  W d    n1 sd0    Y  d S )
NTr   z[%Y-%m-%d %H:%M:%S] )flushautf-8encoding
)r   r   timestrftimeprintr   openwrite)r%   Ztslinefr"   r"   r#   log   s    
r4   )pathc              
   C   s   z>| j ddd}t|W  d    W S 1 s20    Y  W nT tyT   | Y S  ty } z(td| j d| d |W  Y d }~S d }~0 0 d S )Nrr)   r*   u    ⚠️ Failed to read JSON from : u    — using default.)r0   jsonloadFileNotFoundError	Exceptionr4   name)r5   defaultr3   er"   r"   r#   	load_json   s    .r?   c                 C   sn   | j jddd | | jd }|jddd"}tj||ddd	 W d    n1 sV0    Y  ||  d S )
NTr   z.tmpwr)   r*   r   F)indentZensure_ascii)parentr   Zwith_suffixsuffixr0   r8   dumpreplace)r5   datatmpr3   r"   r"   r#   	save_json   s
    0rH   c                    s<   g }| D ].}|   t fdd|D r,q|| q|S )z:Remove items that were excluded due to protected_prefixes.c                 3   s$   | ]} |kp  |d  V  qdS )/N)
startswith.0pZrlr"   r#   	<genexpr>       z,filter_protected_removals.<locals>.<genexpr>)loweranyappend)Zremoved_listprotected_prefixesoutrelr"   rN   r#   filter_protected_removals   s    rW   c                 C   sv   |   sg S g }| jdddD}|D ].}| }|r$|dr@q$||dd q$W d   n1 sh0    Y  |S )zi
    Read non-empty, non-comment lines from a text file and normalize
    to POSIX-style separators.
    r6   r)   r*   #\rI   N)r   r0   striprJ   rS   rE   )r5   linesr3   rawr2   r"   r"   r#   load_patterns   s    2r]   )rel_pathprefixreturnc                 C   s$   | d}| |krdS | |d S )z8True if rel_path is exactly prefix or directly under it.rI   T)rstriprJ   )r^   r_   r"   r"   r#   is_prefix_match   s    
rb   r^   c                    s   t  fdd|D S )Nc                 3   s   | ]}t  |V  qd S Nrb   rK   rc   r"   r#   rO      rP   z!should_exclude.<locals>.<genexpr>)rR   )r^   Zexcludesr"   rc   r#   should_exclude   s    rf   )r5   rel_keyr`   c           
      C   s   |   }|j}t|j}t|}|rJ|d|krJ|d|krJ|d S t }| d,}|	d}|snqz|
| q^W d   n1 s0    Y  | }	|	||dt|< |	S )zm
    SHA1 hash with caching by relative key.
    Cache entry: { "hash": ..., "size": ..., "mtime": ... }
    sizemtimehashrbi   N)rj   rh   ri   )r   r   intst_mtime
HASH_CACHEgethashlibZsha1r0   readupdateZ	hexdigest)
r5   rg   strh   ri   cachedhr3   chunkdigestr"   r"   r#   
quick_hash   s    

 
*rx   c                 #   s  t  }| D ]}| }|sq
t| }| s>td|  q
| r|dd}|}| }||v s
t||rrq
|	| t
||}||fV  q
t|D ]
\}	}
}t|	t }|dd}| }g }|
D ]J}|dkr| d| n|}|  t fdd|D rq|| q||
dd< |D ]t}t|	| t }|dd}| }||v rnq2t||r~q2|	| t
t|	| |}||fV  q2qq
dS )z
    Walk the given root-relative paths (dirs or files) and yield
    (rel_path, hash) pairs, preserving ORIGINAL CASE for output.
    All exclusion/prefix logic is done using lowercase copies.
    u    ⚠️ Included path not found: rY   rI   r   c                 3   s   | ]}t  |V  qd S rd   re   rK   Zsub_rel_lowerr"   r#   rO     rP   zwalk_roots.<locals>.<genexpr>N)setrZ   BASE_DIRr   r4   is_filerE   rQ   rf   addrx   oswalkr   relative_toas_posixrR   rS   )Zroot_rel_pathsZexclude_prefixesseenZroot_relZ	root_pathrV   rel_norm	rel_lowerrw   dirpathdirnames	filenamesdir_reldir_rel_normZdir_rel_lowerpruneddZsub_relfnamer"   ry   r#   
walk_roots   sR    




r   c                  C   s   t t} ttt|  }t t}dd t| D }td|  |rTtd|  g }i }t	||D ]2\}}t
t|dd }||||d |||< qf|jdd	 d
 tt|}	td||	d}
tt|
 tdt dt| d |
|fS )a  
    Client manifest:
      - roots: BepInEx/patchers, BepInEx/plugins + include_list.txt
      - excludes: CLIENT_EXCLUDES + exclude_list.txt
      - output: manifests/client_mods_manifest.json
      - also writes protected_prefixes for GUI to ignore/prune-around
    c                 S   s   g | ]}|  d qS z/\rQ   ra   rK   r"   r"   r#   
<listcomp>5  rP   z)build_client_manifest.<locals>.<listcomp>u   🧭 Client scan roots: u)   🧹 Client protected/excluded prefixes: rI   Zsafer<   rj   Zurlc                 S   s   | d S Nr<   r"   xr"   r"   r#   <lambda>I  rP   z'build_client_manifest.<locals>.<lambda>key%a %b %d %H:%M:%S %Z %Ygenerated_atfilesrT   u   📝 Client manifest written:  ( files))r]   INCLUDE_LISTlistdictfromkeysCLIENT_ROOTSEXCLUDE_LISTCLIENT_EXCLUDESr4   r   BASE_URLr   rS   sortsortedrz   r-   r.   rH   CLIENT_MANIFEST_PATHlenZextra_rootsZ	all_rootsZextra_excludesZall_excludesr   mappingrV   rw   Zsafe_urlrT   manifestr"   r"   r#   build_client_manifest'  s4    

r   c                  C   s   t t} ttt|  }t t}dd t| D }td|  |rTtd|  g }i }t	||D ]2\}}t
t|dd }||||d |||< qf|jdd	 d
 tt|}	td||	d}
tt|
 tdt dt| d |
|fS )a  
    Server manifest:
      - roots: SPT/user/mods + include_fika_client.txt
      - excludes: SERVER_EXCLUDES + exclude_fika_client.txt
      - output: manifests/server_mods_manifest.json
      - also writes protected_prefixes for GUI to ignore/prune-around
    c                 S   s   g | ]}|  d qS r   r   rK   r"   r"   r#   r   b  rP   z)build_server_manifest.<locals>.<listcomp>u   🧭 Server scan roots: u)   🧹 Server protected/excluded prefixes: rI   r   r   c                 S   s   | d S r   r"   r   r"   r"   r#   r   v  rP   z'build_server_manifest.<locals>.<lambda>r   r   r   u   📝 Server manifest written: r   r   )r]   INCLUDE_FIKA_CLIENTr   r   r   SERVER_ROOTSEXCLUDE_FIKA_CLIENTSERVER_EXCLUDESr4   r   r   r   rS   r   r   rz   r-   r.   rH   SERVER_MANIFEST_PATHr   r   r"   r"   r#   build_server_manifestV  s4    

r   c                  C   sd   t jdd  } | dv r<t jdd }d|p8dfS t sHdS t r`ddtj fS d	S )
a3  Return (rebuild_required, reason) for the broad DB Include index.

    The normal client/server manifests are rebuilt every run. The DB Include
    index is broader and walks the whole database, so it is rebuilt only when
    the watcher requests it, the index is missing, or a manual flag file exists.
    ZFIKA_REBUILD_DB_INCLUDE_INDEX >   1trueZforceZyesZonZ$FIKA_REBUILD_DB_INCLUDE_INDEX_REASONzwatcher requestT)Tz db_include_index.json is missingzmanual flag present: )Fz#no DB Include-only changes detected)	r~   environro   rZ   rQ   DB_INCLUDE_INDEX_PATHr   DB_INCLUDE_INDEX_REBUILD_FLAGr<   )r\   reasonr"   r"   r#   should_rebuild_db_include_index  s    r   c               
   C   sL   zt  rt   W n2 tyF }  ztd|   W Y d } ~ n
d } ~ 0 0 d S )Nu0   ⚠️ Could not clear DB Include rebuild flag: )r   r   r   OSErrorr4   )r>   r"   r"   r#   clear_db_include_rebuild_flag  s
    r   c                  C   s  dd t D } g }i }g }t }t }ttD ]6\}}}t|t }	|	dkrZdn
|	dd}
g }|D ]`}|
r|
 d| n|}|	 }t
|| rqn|| ||vrn|| ||ddd  qn||dd< |D ]}t|| t }|dd}|	 }||v st
|| r$q|| tt|| |}|||tt|dd d	 |||< qq0|jd
d d |jtj	d |||fS )a  Inventory every downloadable DB file and directory once.

    The two normal manifests remain authoritative for their own sync scopes.
    This index is intentionally broader: it is used only when a server admin
    explicitly selects a DB Include path outside those manifests.
    c                 S   s   g | ]}|  d qS r   r   rK   r"   r"   r#   r     rP   z)scan_db_include_index.<locals>.<listcomp>r   r   rY   rI   Nr   r   c                 S   s   | d   S r   )rQ   )itemr"   r"   r#   r     rP   z'scan_db_include_index.<locals>.<lambda>r   )DB_INCLUDE_INDEX_EXCLUDESrz   r~   r   r{   r   r   r   rE   rQ   rf   rS   r}   rx   r   r   r   str)Zexcludedr   r   foldersZ
seen_filesZseen_foldersr   r   r   r   r   r   dirnameZ	child_relZchild_lowerr   rV   r   r   rw   r"   r"   r#   scan_db_include_index  sJ    



r   c                 C   sF   dt d| |d}tt| tdt dt|  dt| d |S )z;Atomically publish the Include index after the settle scan.r   r   )Zschemar   r   r   u"   🗂️ DB Include index written: r   z files, z	 folders))r-   r.   rH   r   r4   r   )r   r   r   r"   r"   r#   write_db_include_index  s    
$r   c                  C   s*   t ti } | di }| di }||fS )Nclientserver)r?   
STATE_FILEro   )rF   Zclient_prevZserver_prevr"   r"   r#   load_last_state  s    
r   c                 C   s   | |d}t t| d S )N)r   r   )rH   r   )
client_map
server_maprF   r"   r"   r#   
save_state  s    r   c                    sV   t   }t  }t|| }t|| }t fdd||@ D }|||fS )Nc                 3   s"   | ]} | | kr|V  qd S rd   r"   )rL   kcurrentpreviousr"   r#   rO     s   zdiff_maps.<locals>.<genexpr>)rz   keysr   )r   r   Zcur_keysZ	prev_keysaddedremovedupdatedr"   r   r#   	diff_maps  s    r   )contentc              
   C   s   t std dS zdtd| i}tjdddddd	d
|t g	ddd}|jdkrjtd|j d|j   ntd W n2 t	y } ztd|  W Y d}~n
d}~0 0 dS )uW   
    Send Discord webhook without 'requests' — uses curl so Unraid always works.
    u5   ⚠️ WEBHOOK_URL not set — skipping Discord post.Nr   Zcurlz-sz-HzContent-Type: application/jsonz-XZPOSTz-dT)Zcapture_outputtextr   u   ⚠️ curl failed with code r7   u*   📨 Posted changelog to Discord via curl.u(   ⚠️ Failed to send webhook via curl: )
WEBHOOK_URLr4   r8   dumps
subprocessrun
returncodestderrrZ   r;   )r   Zpayloadresultr>   r"   r"   r#   send_discord_message  s*    
r   r   c                    sJ   d|  dg d fdd}|dd| |dd	| |d
d|  S )zG
    Build Discord formatted section for one side (Client/Server).
    u   
— u    —(   c                    sx   |sd S   |  dt| d |d  D ]}  d| d |  q.t|krt  dt|  d d S )Nr   z):z  r&   u
     … and z more)rS   r   )ZlabelZmarkeritemsr3   r[   Zmax_listr_   r"   r#   	add_block+  s    z!format_section.<locals>.add_blocku	   ➕ Added+u   🟠 Updated~u   ➖ Removed-r"   )titler   r   r   r_   r   r"   r   r#   format_section$  s    	r   c                 C   sZ   | \}}}|s"|s"|s"t d d S dg}|td||| dd| d }t| d S )NuE   ✔️ No new CLIENT file changes detected — skipping Discord post.zUpdate summary for Fika Mods:zClient changesz```
r,   z
```)r4   extendr   joinr   )client_diffserver_diffZc_addedZ	c_updatedZ	c_removedr[   r%   r"   r"   r#   post_changelog;  s    
r   c               	   C   s&  t   td tti atdtt d t \} }t \}}t	t
 t \}}t \}}dd }|||}	|||}
t \}}|rtd| d t \}}}t|| t  ntd| d t|t|	krtd	 t|t|
krtd
 |}|}ttt tdtt d t \}}t|	|}t|
|}|dg }|dg }t|d |}t|d |}|d |d |f}|d |d |f}tdt|d  dt|d  dt|d   tdt|d  dt|d  dt|d   t|| t|	|
 td td td d S )Nu&   🚀 Running Fika unified generator...u   📚 Loaded hash cache with z	 entries.c                    s    fdd|  D S )Nc                    s*   i | ]"\}}| v r | |kr||qS r"   r"   )rL   r   vmap1r"   r#   
<dictcomp>t  s   z-main.<locals>.stable_only.<locals>.<dictcomp>)r   )r   Zmap2r"   r   r#   stable_onlys  s    
zmain.<locals>.stable_onlyu/   🗂️ DB Include index rebuild requested — r   u'   🗂️ DB Include index unchanged — uB   🕒 Some client files still uploading — deferred until next runuB   🕒 Some server files still uploading — deferred until next runu   💾 Hash cache updated with rT   r   r   r   u   🧾 Client changes — Added: z, Updated: z, Removed: u   🧾 Server changes — Added: u   💾 Change-state saved.u%   ✅ Fika unified generator completed.z<------------------------------------------------------------)r$   r4   r?   HASH_CACHE_FILErn   r   r   r   r-   sleepSETTLE_DELAY_SECONDSr   r   r   r   rH   r   r   ro   rW   r   r   )Zclient_manifest_1Zclient_map_1Zserver_manifest_1Zserver_map_1Zclient_manifest_2Zclient_map_2Zserver_manifest_2Zserver_map_2r   r   r   Zrebuild_include_indexZinclude_index_reasonZinclude_index_filesZinclude_index_mapZinclude_index_foldersZclient_manifestZserver_manifestZprev_clientZprev_serverZclient_diff_rawZserver_diff_rawZclient_protectedZserver_protectedZclient_removed_filteredZserver_removed_filteredr   r   r"   r"   r#   mainS  s    



















r   __main__)r   )@r~   sysr-   r8   rp   r   pathlibr   Zurllib.parser   r   __file__ZresolverB   r{   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rn   r$   r   r4   r?   rH   rW   r]   boolrb   rf   rx   r   r   r   r   r   r   r   r   r   r   r   r   r   r   __name__r"   r"   r"   r#   <module>   sz   
B/04!
v
