@@ -818,33 +818,37 @@ async def get_dashboard():
818818
819819@app .get ("/api/recordings/{username}" )
820820async def list_recordings (username : str ):
821- """Liste uniquement les enregistrements MP4 convertis"""
821+ """Liste les enregistrements ( MP4 convertis ou TS bruts) """
822822 from datetime import datetime
823823 from .core .utils import format_bytes
824-
824+
825825 # Récupérer depuis SQLite
826826 recordings_db = await db .get_recordings (username )
827-
827+
828828 recordings = []
829829 thumbnails_dir = OUTPUT_DIR / "thumbnails" / username
830-
830+
831831 for rec in recordings_db :
832- # FILTRER: Ne retourner que les enregistrements convertis avec MP4
833- if not rec .get ('is_converted' ) or not rec .get ('mp4_path' ):
834- continue
835-
836- mp4_path = Path (rec ['mp4_path' ])
837-
838- # Vérifier que le fichier MP4 existe
839- if not mp4_path .exists ():
832+ # Determine the playable file: prefer MP4, fall back to TS
833+ is_converted = bool (rec .get ('is_converted' ))
834+ mp4_raw = rec .get ('mp4_path' )
835+ ts_raw = rec .get ('file_path' )
836+
837+ if is_converted and mp4_raw and Path (mp4_raw ).exists ():
838+ serve_path = Path (mp4_raw )
839+ file_size = rec .get ('mp4_size' ) or serve_path .stat ().st_size
840+ elif ts_raw and Path (ts_raw ).exists ():
841+ serve_path = Path (ts_raw )
842+ file_size = rec .get ('file_size' ) or serve_path .stat ().st_size
843+ else :
840844 continue
841-
842- stat = mp4_path .stat ()
843-
845+
846+ stat = serve_path .stat ()
847+
844848 # Miniature
845- thumb_path = thumbnails_dir / f"{ mp4_path .stem } .jpg"
846- thumb_url = f"/api/recording-thumbnail/{ username } /{ mp4_path .stem } .jpg"
847-
849+ thumb_path = thumbnails_dir / f"{ serve_path .stem } .jpg"
850+ thumb_url = f"/api/recording-thumbnail/{ username } /{ serve_path .stem } .jpg"
851+
848852 # Formater la durée
849853 duration_seconds = rec .get ('duration_seconds' , 0 )
850854 hours = duration_seconds // 3600
@@ -854,36 +858,35 @@ async def list_recordings(username: str):
854858 duration_str = f"{ hours } h{ minutes :02d} m"
855859 else :
856860 duration_str = f"{ minutes } m{ seconds :02d} s"
857-
861+
858862 # Calculer la taille en MB ou GB
859- mp4_size = rec .get ('mp4_size' , 0 )
860- if mp4_size >= 1000 * 1024 * 1024 : # >= 1000 MB
861- size_display = f"{ mp4_size / 1024 / 1024 / 1024 :.2f} GB"
863+ if file_size >= 1000 * 1024 * 1024 : # >= 1000 MB
864+ size_display = f"{ file_size / 1024 / 1024 / 1024 :.2f} GB"
862865 else :
863- size_display = f"{ mp4_size / 1024 / 1024 :.0f} MB"
864-
866+ size_display = f"{ file_size / 1024 / 1024 :.0f} MB"
867+
865868 recordings .append ({
866- "recordingId" : rec .get ('recording_id' , mp4_path .stem ),
867- "filename" : mp4_path .name ,
868- "date" : mp4_path .stem ,
869- "size" : mp4_size ,
870- "size_formatted" : format_bytes (mp4_size ),
871- "size_mb" : round (mp4_size / 1024 / 1024 , 2 ),
869+ "recordingId" : rec .get ('recording_id' , serve_path .stem ),
870+ "filename" : serve_path .name ,
871+ "date" : serve_path .stem ,
872+ "size" : file_size ,
873+ "size_formatted" : format_bytes (file_size ),
874+ "size_mb" : round (file_size / 1024 / 1024 , 2 ),
872875 "size_display" : size_display ,
873876 "modified" : datetime .fromtimestamp (stat .st_mtime ).isoformat (),
874- "url" : f"/streams/records/{ username } /{ mp4_path .name } " ,
877+ "url" : f"/streams/records/{ username } /{ serve_path .name } " ,
875878 "thumbnail" : thumb_url if thumb_path .exists () else None ,
876879 "duration" : duration_seconds ,
877880 "duration_str" : duration_str ,
878- "isConverted" : True ,
881+ "isConverted" : is_converted ,
879882 "mp4" : {
880- "filename" : mp4_path .name ,
881- "size" : mp4_size ,
882- "size_formatted" : format_bytes (mp4_size ),
883- "url" : f"/streams/records/{ username } /{ mp4_path .name } "
884- }
883+ "filename" : Path ( mp4_raw ) .name ,
884+ "size" : rec . get ( ' mp4_size' , 0 ) ,
885+ "size_formatted" : format_bytes (rec . get ( ' mp4_size' , 0 ) ),
886+ "url" : f"/streams/records/{ username } /{ Path ( mp4_raw ) .name } "
887+ } if is_converted and mp4_raw else None
885888 })
886-
889+
887890 return {"recordings" : recordings }
888891
889892
@@ -904,13 +907,22 @@ async def get_all_recordings(
904907
905908 recordings = []
906909 for rec in result ["recordings" ]:
907- mp4_path = rec .get ("mp4_path" )
908- if not mp4_path :
910+ rec_username = rec .get ("username" , "" )
911+ is_converted = bool (rec .get ("is_converted" ))
912+ mp4_raw = rec .get ("mp4_path" )
913+ ts_raw = rec .get ("file_path" )
914+
915+ # Determine the playable file: prefer MP4, fall back to TS
916+ if is_converted and mp4_raw and Path (mp4_raw ).exists ():
917+ serve_file = Path (mp4_raw )
918+ file_size = rec .get ("mp4_size" ) or serve_file .stat ().st_size
919+ elif ts_raw and Path (ts_raw ).exists ():
920+ serve_file = Path (ts_raw )
921+ file_size = rec .get ("file_size" ) or serve_file .stat ().st_size
922+ else :
909923 continue
910924
911- mp4_file = Path (mp4_path )
912- file_stem = mp4_file .stem
913- rec_username = rec .get ("username" , "" )
925+ file_stem = serve_file .stem
914926
915927 # Format duration
916928 duration_seconds = rec .get ("duration_seconds" , 0 )
@@ -922,22 +934,19 @@ async def get_all_recordings(
922934 else :
923935 duration_str = f"{ minutes } m{ seconds :02d} s"
924936
925- # Format size
926- mp4_size = rec .get ("mp4_size" ) or rec .get ("file_size" , 0 )
927-
928937 # Thumbnail
929938 thumb_path = OUTPUT_DIR / "thumbnails" / rec_username / f"{ file_stem } .jpg"
930939
931940 recordings .append ({
932941 "recordingId" : rec .get ("recording_id" , file_stem ),
933942 "username" : rec_username ,
934- "filename" : mp4_file .name ,
943+ "filename" : serve_file .name ,
935944 "date" : file_stem ,
936- "size" : mp4_size ,
937- "size_formatted" : format_bytes (mp4_size ),
945+ "size" : file_size ,
946+ "size_formatted" : format_bytes (file_size ),
938947 "duration" : duration_seconds ,
939948 "duration_str" : duration_str ,
940- "url" : f"/streams/records/{ rec_username } /{ mp4_file .name } " ,
949+ "url" : f"/streams/records/{ rec_username } /{ serve_file .name } " ,
941950 "thumbnail" : f"/api/recording-thumbnail/{ rec_username } /{ file_stem } .jpg" if thumb_path .exists () else None ,
942951 "createdAt" : rec .get ("created_at" ),
943952 })
@@ -1187,6 +1196,44 @@ async def set_blacklisted_tags(body: dict):
11871196 return {"tags" : tags }
11881197
11891198
1199+ # ============================================
1200+ # Recording Settings Endpoints
1201+ # ============================================
1202+
1203+ @app .get ("/api/settings/recording" )
1204+ async def get_recording_settings ():
1205+ """Get recording settings (auto_convert, keep_ts)"""
1206+ from .core .config import AUTO_CONVERT , KEEP_TS
1207+
1208+ auto_convert_val = await db .get_setting ("auto_convert" )
1209+ keep_ts_val = await db .get_setting ("keep_ts" )
1210+
1211+ # Fall back to env var defaults if not set in DB
1212+ if auto_convert_val is not None :
1213+ auto_convert = auto_convert_val .lower () in {"1" , "true" , "yes" }
1214+ else :
1215+ auto_convert = AUTO_CONVERT
1216+
1217+ if keep_ts_val is not None :
1218+ keep_ts = keep_ts_val .lower () in {"1" , "true" , "yes" }
1219+ else :
1220+ keep_ts = KEEP_TS
1221+
1222+ return {"auto_convert" : auto_convert , "keep_ts" : keep_ts }
1223+
1224+
1225+ @app .put ("/api/settings/recording" )
1226+ async def update_recording_settings (body : dict ):
1227+ """Update recording settings (auto_convert, keep_ts)"""
1228+ if "auto_convert" in body :
1229+ await db .set_setting ("auto_convert" , str (body ["auto_convert" ]).lower ())
1230+ if "keep_ts" in body :
1231+ await db .set_setting ("keep_ts" , str (body ["keep_ts" ]).lower ())
1232+
1233+ # Return current state
1234+ return await get_recording_settings ()
1235+
1236+
11901237# ============================================
11911238# Follow/Unfollow on Chaturbate
11921239# ============================================
@@ -1512,16 +1559,23 @@ async def cleanup_old_recordings_task():
15121559 for model in models :
15131560 username = model .get ('username' )
15141561 retention_days = model .get ('retention_days' , 30 ) # Défaut 30 jours
1515-
1562+
15161563 if not username :
15171564 continue
1518-
1565+
1566+ # retention_days == 0 means keep forever
1567+ if retention_days == 0 :
1568+ logger .debug ("Rétention infinie, skip" ,
1569+ task = "cleanup" ,
1570+ username = username )
1571+ continue
1572+
15191573 records_dir = OUTPUT_DIR / "records" / username
15201574 thumbnails_dir = OUTPUT_DIR / "thumbnails" / username
1521-
1575+
15221576 if not records_dir .exists ():
15231577 continue
1524-
1578+
15251579 # Date limite (aujourd'hui - rétention)
15261580 cutoff_date = datetime .now () - timedelta (days = retention_days )
15271581
0 commit comments