11import os
22import re
3+ from collections .abc import Sequence
34from datetime import datetime , timezone
45from typing import Annotated
56
3435
3536def _build_save_schema (
3637 save : Save ,
38+ syncs : Sequence [tuple [DeviceSaveSync , str | None ]] = (),
3739 device : Device | None = None ,
38- sync : DeviceSaveSync | None = None ,
3940) -> SaveSchema :
41+ """Attach one ``DeviceSyncSchema`` per device that has synced this save.
42+
43+ ``syncs`` is the full list of sync rows (paired with device name) for this
44+ save across every device, so clients can attribute the save to its creator.
45+ ``device`` is the caller's device, when supplied: its entry is emitted first
46+ for stable ordering and old-client compatibility, and a placeholder entry is
47+ synthesized when the caller has not yet synced this save.
48+ """
4049 save_schema = SaveSchema .model_validate (save )
4150
42- if device :
43- if sync :
44- is_current = to_utc (sync .last_synced_at ) >= to_utc (save .updated_at )
45- last_synced = sync .last_synced_at
46- is_untracked = sync .is_untracked
47- else :
48- is_current = False
49- last_synced = save .updated_at
50- is_untracked = False
51+ save_updated = to_utc (save .updated_at )
52+ caller_present = False
53+ entries : list [DeviceSyncSchema ] = []
54+ for sync , device_name in syncs :
55+ if device and sync .device_id == device .id :
56+ caller_present = True
57+ entries .append (
58+ DeviceSyncSchema (
59+ device_id = sync .device_id ,
60+ device_name = device_name ,
61+ last_synced_at = sync .last_synced_at ,
62+ is_untracked = sync .is_untracked ,
63+ is_current = to_utc (sync .last_synced_at ) >= save_updated ,
64+ )
65+ )
5166
52- save_schema .device_syncs = [
67+ if device and not caller_present :
68+ entries .append (
5369 DeviceSyncSchema (
5470 device_id = device .id ,
5571 device_name = device .name ,
56- last_synced_at = last_synced ,
57- is_untracked = is_untracked ,
58- is_current = is_current ,
72+ last_synced_at = save . updated_at ,
73+ is_untracked = False ,
74+ is_current = False ,
5975 )
60- ]
76+ )
6177
78+ if device :
79+ entries .sort (key = lambda entry : entry .device_id != device .id )
80+
81+ save_schema .device_syncs = entries
6282 return save_schema
6383
6484
85+ def _syncs_for_save (
86+ save_id : int , device : Device | None
87+ ) -> list [tuple [DeviceSaveSync , str | None ]]:
88+ """Fetch every device sync for a single save when a device is in context.
89+
90+ Device attribution is only meaningful to a device-scoped caller, so callers
91+ without a device get an empty list and no query is issued.
92+ """
93+ if not device :
94+ return []
95+ return db_device_save_sync_handler .get_syncs_for_saves ([save_id ]).get (save_id , [])
96+
97+
6598DATETIME_TAG_PATTERN = re .compile (r" \[\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\]" )
6699
67100
@@ -229,12 +262,9 @@ async def add_save(
229262 await fs_asset_handler .remove_file (f"{ saves_path } /{ actual_filename } " )
230263 except FileNotFoundError :
231264 pass
232- sync = None
233- if device :
234- sync = db_device_save_sync_handler .get_sync (
235- device_id = device .id , save_id = existing_by_hash .id
236- )
237- return _build_save_schema (existing_by_hash , device , sync )
265+ return _build_save_schema (
266+ existing_by_hash , _syncs_for_save (existing_by_hash .id , device ), device
267+ )
238268
239269 if db_save :
240270 update_data : dict = {
@@ -249,6 +279,7 @@ async def add_save(
249279 scanned_save .user_id = request .user .id
250280 scanned_save .emulator = emulator
251281 scanned_save .slot = slot
282+ scanned_save .origin_device_id = device .id if device else None
252283 db_save = db_save_handler .add_save (save = scanned_save )
253284
254285 if device :
@@ -322,12 +353,7 @@ async def add_save(
322353 rom_user .id , {"last_played" : datetime .now (timezone .utc )}
323354 )
324355
325- sync = None
326- if device :
327- sync = db_device_save_sync_handler .get_sync (
328- device_id = device .id , save_id = db_save .id
329- )
330- return _build_save_schema (db_save , device , sync )
356+ return _build_save_schema (db_save , _syncs_for_save (db_save .id , device ), device )
331357
332358
333359@protected_route (router .get , "" , [Scope .ASSETS_READ ])
@@ -350,13 +376,13 @@ def get_saves(
350376 if not device :
351377 return [_build_save_schema (save ) for save in saves ]
352378
353- syncs = db_device_save_sync_handler .get_syncs_for_device_and_saves (
354- device_id = device . id , save_ids = [s .id for s in saves ]
379+ syncs_by_save_id = db_device_save_sync_handler .get_syncs_for_saves (
380+ [s .id for s in saves ]
355381 )
356- sync_by_save_id = {s .save_id : s for s in syncs }
357382
358383 return [
359- _build_save_schema (save , device , sync_by_save_id .get (save .id )) for save in saves
384+ _build_save_schema (save , syncs_by_save_id .get (save .id , []), device )
385+ for save in saves
360386 ]
361387
362388
@@ -404,12 +430,7 @@ def get_save(request: Request, id: int, device_id: str | None = None) -> SaveSch
404430 detail = f"Save with ID { id } not found" ,
405431 )
406432
407- sync = None
408- if device :
409- sync = db_device_save_sync_handler .get_sync (
410- device_id = device .id , save_id = save .id
411- )
412- return _build_save_schema (save , device , sync )
433+ return _build_save_schema (save , _syncs_for_save (save .id , device ), device )
413434
414435
415436@protected_route (router .get , "/{id}/content" , [Scope .ASSETS_READ ])
@@ -475,14 +496,14 @@ def confirm_download(
475496 )
476497
477498 device = _resolve_device (device_id , request .user .id )
478- sync = db_device_save_sync_handler .upsert_sync (
499+ db_device_save_sync_handler .upsert_sync (
479500 device_id = device_id ,
480501 save_id = save .id ,
481502 synced_at = save .updated_at ,
482503 )
483504 db_device_handler .update_last_seen (device_id = device_id , user_id = request .user .id )
484505
485- return _build_save_schema (save , device , sync )
506+ return _build_save_schema (save , _syncs_for_save ( save . id , device ), device )
486507
487508
488509@protected_route (router .put , "/{id}" , [Scope .ASSETS_WRITE ])
@@ -581,12 +602,7 @@ async def update_save(
581602 )
582603 db_device_handler .update_last_seen (device_id = device .id , user_id = request .user .id )
583604
584- sync = None
585- if device :
586- sync = db_device_save_sync_handler .get_sync (
587- device_id = device .id , save_id = db_save .id
588- )
589- return _build_save_schema (db_save , device , sync )
605+ return _build_save_schema (db_save , _syncs_for_save (db_save .id , device ), device )
590606
591607
592608@protected_route (
@@ -661,11 +677,11 @@ def track_save(
661677 )
662678
663679 device = _resolve_device (device_id , request .user .id )
664- sync = db_device_save_sync_handler .set_untracked (
680+ db_device_save_sync_handler .set_untracked (
665681 device_id = device_id , save_id = id , untracked = False
666682 )
667683
668- return _build_save_schema (save , device , sync )
684+ return _build_save_schema (save , _syncs_for_save ( save . id , device ), device )
669685
670686
671687@protected_route (router .post , "/{id}/untrack" , [Scope .DEVICES_WRITE ])
@@ -683,8 +699,8 @@ def untrack_save(
683699 )
684700
685701 device = _resolve_device (device_id , request .user .id )
686- sync = db_device_save_sync_handler .set_untracked (
702+ db_device_save_sync_handler .set_untracked (
687703 device_id = device_id , save_id = id , untracked = True
688704 )
689705
690- return _build_save_schema (save , device , sync )
706+ return _build_save_schema (save , _syncs_for_save ( save . id , device ), device )
0 commit comments