33import json
44import random
55import datetime
6+ import subprocess as sp
67from urllib .parse import quote , unquote
78from importlib .metadata import version , PackageNotFoundError
89from pathlib import Path
910
1011from flask import Flask , render_template , send_from_directory , request , redirect , send_file
12+ from PIL import Image
1113
1214# Service Imports
1315from tiklocal .services import LibraryService , FavoriteService , RecommendService , IMAGE_EXTENSIONS
@@ -272,9 +274,88 @@ def _collect_library_records(*, favorites_only: bool = False) -> list[dict]:
272274 records .sort (key = lambda item : (item ['mtime_ts' ], item ['name' ]), reverse = True )
273275 return records
274276
277+ def _read_media_dims_from_metadata (name : str ) -> tuple [int | None , int | None ]:
278+ payload = metadata_store .get (name )
279+ if not isinstance (payload , dict ):
280+ return None , None
281+ media_meta = payload .get ('media_meta' )
282+ if not isinstance (media_meta , dict ):
283+ return None , None
284+ try :
285+ width = int (media_meta .get ('width' ) or 0 )
286+ height = int (media_meta .get ('height' ) or 0 )
287+ except (TypeError , ValueError ):
288+ return None , None
289+ if width <= 0 or height <= 0 :
290+ return None , None
291+ return width , height
292+
293+ def _save_media_dims_to_metadata (name : str , media_type : str , width : int , height : int ) -> None :
294+ if width <= 0 or height <= 0 :
295+ return
296+ current = metadata_store .get (name )
297+ payload = dict (current ) if isinstance (current , dict ) else {}
298+ payload ['media_meta' ] = {
299+ 'type' : media_type ,
300+ 'width' : int (width ),
301+ 'height' : int (height ),
302+ 'updated_at' : datetime .datetime .utcnow ().isoformat () + 'Z' ,
303+ }
304+ metadata_store .set (name , payload , overwrite = True )
305+
306+ def _probe_media_dims (name : str , media_type : str ) -> tuple [int | None , int | None ]:
307+ target = library_service .resolve_path (name )
308+ if not target or not target .exists ():
309+ return None , None
310+
311+ if media_type == 'image' :
312+ try :
313+ with Image .open (target ) as img :
314+ width , height = img .size
315+ if int (width ) > 0 and int (height ) > 0 :
316+ return int (width ), int (height )
317+ except Exception :
318+ return None , None
319+ return None , None
320+
321+ try :
322+ cmd = [
323+ 'ffprobe' ,
324+ '-v' , 'error' ,
325+ '-select_streams' , 'v:0' ,
326+ '-show_entries' , 'stream=width,height' ,
327+ '-of' , 'json' ,
328+ str (target ),
329+ ]
330+ proc = sp .run (cmd , capture_output = True , text = True , timeout = 8 )
331+ if proc .returncode != 0 :
332+ return None , None
333+ payload = json .loads (proc .stdout or '{}' )
334+ streams = payload .get ('streams' ) or []
335+ if not streams :
336+ return None , None
337+ stream = streams [0 ] if isinstance (streams [0 ], dict ) else {}
338+ width = int (stream .get ('width' ) or 0 )
339+ height = int (stream .get ('height' ) or 0 )
340+ if width > 0 and height > 0 :
341+ return width , height
342+ except Exception :
343+ return None , None
344+ return None , None
345+
346+ def _get_or_probe_media_dims (name : str , media_type : str ) -> tuple [int | None , int | None ]:
347+ width , height = _read_media_dims_from_metadata (name )
348+ if width and height :
349+ return width , height
350+ width , height = _probe_media_dims (name , media_type )
351+ if width and height :
352+ _save_media_dims_to_metadata (name , media_type , width , height )
353+ return width , height
354+
275355 def _serialize_library_item (record : dict ) -> dict :
276356 name = str (record .get ('name' ) or '' )
277357 media_type = str (record .get ('media_type' ) or 'video' )
358+ width , height = _get_or_probe_media_dims (name , media_type )
278359 encoded = quote (name )
279360 media_url = f"/media?uri={ encoded } "
280361 return {
@@ -285,6 +366,8 @@ def _serialize_library_item(record: dict) -> dict:
285366 'thumb_url' : media_url if media_type == 'image' else f"/thumb?uri={ encoded } " ,
286367 'mtime_ts' : float (record .get ('mtime_ts' ) or 0 ),
287368 'size_bytes' : int (record .get ('size_bytes' ) or 0 ),
369+ 'width' : int (width ) if width else None ,
370+ 'height' : int (height ) if height else None ,
288371 }
289372
290373 def _apply_library_mode (records : list [dict ], * , mode : str , min_mb : int , seed : str ) -> list [dict ]:
@@ -895,8 +978,10 @@ def api_image_metadata():
895978 result ['prompt_source' ] = prompt_source
896979 result ['llm_source' ] = llm_source
897980 result .setdefault ('prompt_hash' , compute_prompt_hash (effective_prompt ))
898- metadata_store .set (uri , result , overwrite = True )
899- return {'success' : True , 'data' : result }
981+ merged = dict (existing ) if isinstance (existing , dict ) else {}
982+ merged .update (result )
983+ metadata_store .set (uri , merged , overwrite = True )
984+ return {'success' : True , 'data' : merged }
900985 except Exception as e :
901986 return {'success' : False , 'error' : str (e )}, 500
902987
0 commit comments