1919from cwa_db import CWA_DB
2020from kindle_epub_fixer import EPUBFixer
2121import audiobook
22+ import requests
2223
2324# Optional: enable GDrive sync and auto-send by importing cps modules when available
2425_GDRIVE_AVAILABLE = False
@@ -314,6 +315,27 @@ def __init__(self, filepath: str):
314315 self .library_dir = self .split_library ["split_path" ]
315316 self .calibre_env ['CALIBRE_OVERRIDE_DATABASE_PATH' ] = os .path .join (self .split_library ["db_path" ], "metadata.db" )
316317
318+ # Track the last added Calibre book id(s) from calibredb output
319+ self .last_added_book_id : int | None = None
320+ self .last_added_book_ids : list [int ] = []
321+
322+ @staticmethod
323+ def _parse_added_book_ids (output : str ) -> list [int ]:
324+ """Parse calibredb stdout for the 'Added book ids: X[, Y, ...]' line and return IDs.
325+
326+ Handles variations like 'Added book id: 4' or 'Added book ids: 4, 5'.
327+ """
328+ try :
329+ import re
330+ m = re .search (r"Added book id[s]?:\s*([0-9,\s]+)" , output , flags = re .IGNORECASE )
331+ if not m :
332+ return []
333+ nums = m .group (1 )
334+ ids = [int (x .strip ()) for x in nums .split (',' ) if x .strip ().isdigit ()]
335+ return ids
336+ except Exception :
337+ return []
338+
317339
318340 def get_split_library (self ) -> dict [str , str ] | None :
319341 """Checks whether or not the user has split library enabled. Returns None if they don't and the path of the Split Library location if True."""
@@ -552,7 +574,13 @@ def add_book_to_library(self, book_path:str, text: bool=True, format: str="text"
552574
553575 try :
554576 if text :
555- subprocess .run (["calibredb" , "add" , str (staged_path ), "--automerge" , self .cwa_settings ['auto_ingest_automerge' ], f"--library-path={ self .library_dir } " ], env = self .calibre_env , check = True )
577+ result = subprocess .run ([
578+ "calibredb" , "add" , str (staged_path ), "--automerge" , self .cwa_settings ['auto_ingest_automerge' ], f"--library-path={ self .library_dir } "
579+ ], env = self .calibre_env , check = True , capture_output = True , text = True )
580+ added_ids = self ._parse_added_book_ids ((result .stdout or '' ) + '\n ' + (result .stderr or '' ))
581+ if added_ids :
582+ self .last_added_book_ids = added_ids
583+ self .last_added_book_id = added_ids [- 1 ]
556584 else : # audiobook path
557585 meta = audiobook .get_audio_file_info (str (staged_path ), format , os .path .basename (str (staged_path )), False )
558586
@@ -593,7 +621,11 @@ def add_book_to_library(self, book_path:str, text: bool=True, format: str="text"
593621 if isinstance (ident , str ) and ":" in ident and ident .strip ():
594622 add_command .extend (["--identifier" , ident .strip ()])
595623
596- subprocess .run (add_command , env = self .calibre_env , check = True )
624+ result = subprocess .run (add_command , env = self .calibre_env , check = True , capture_output = True , text = True )
625+ added_ids = self ._parse_added_book_ids ((result .stdout or '' ) + '\n ' + (result .stderr or '' ))
626+ if added_ids :
627+ self .last_added_book_ids = added_ids
628+ self .last_added_book_id = added_ids [- 1 ]
597629
598630 print (f"[ingest-processor] Added { staged_path .stem } to Calibre database" , flush = True )
599631
@@ -606,11 +638,17 @@ def add_book_to_library(self, book_path:str, text: bool=True, format: str="text"
606638 # Optional post-import GDrive sync
607639 gdrive_sync_if_enabled ()
608640
609- # Fetch metadata if enabled
610- self .fetch_metadata_if_enabled (staged_path .stem )
641+ # Fetch metadata if enabled, prefer exact book id from calibredb
642+ if self .last_added_book_id is not None :
643+ self .fetch_metadata_if_enabled (book_id = self .last_added_book_id )
644+ else :
645+ self .fetch_metadata_if_enabled (staged_path .stem )
611646
612647 # Trigger auto-send for users who have it enabled
613- self .trigger_auto_send_if_enabled (staged_path .stem , book_path )
648+ if self .last_added_book_id is not None :
649+ self .trigger_auto_send_if_enabled (book_id = self .last_added_book_id , book_path = book_path )
650+ else :
651+ self .trigger_auto_send_if_enabled (staged_path .stem , book_path )
614652
615653 # CRITICAL FIX: Refresh Calibre-Web's database session to make new books visible
616654 # This solves the issue where multiple books don't appear until container restart
@@ -687,7 +725,7 @@ def run_kindle_epub_fixer(self, filepath:str, dest=None) -> None:
687725 print (f"[ingest-processor] An error occurred while processing { os .path .basename (filepath )} with the kindle-epub-fixer. See the following error:\n { e } " )
688726
689727
690- def fetch_metadata_if_enabled (self , book_title : str ) -> None :
728+ def fetch_metadata_if_enabled (self , book_title : str | None = None , book_id : int | None = None ) -> None :
691729 """Fetch and apply metadata for newly ingested books if enabled"""
692730 if not _CPS_AVAILABLE :
693731 print ("[ingest-processor] CPS modules not available, skipping metadata fetch" , flush = True )
@@ -698,19 +736,21 @@ def fetch_metadata_if_enabled(self, book_title: str) -> None:
698736 return
699737
700738 try :
701- # Find the book that was just added to get its ID
702739 calibre_db_path = os .path .join (self .library_dir , 'metadata.db' )
703740 with sqlite3 .connect (calibre_db_path , timeout = 30 ) as con :
704741 cur = con .cursor ()
705- # Get the most recently added book with this title
706- cur .execute ("SELECT id, title FROM books WHERE title LIKE ? ORDER BY timestamp DESC LIMIT 1" , (f"%{ book_title } %" ,))
742+ if book_id is not None :
743+ cur .execute ("SELECT id, title FROM books WHERE id = ?" , (int (book_id ),))
744+ else :
745+ # Fallback: most recently added book
746+ cur .execute ("SELECT id, title FROM books ORDER BY timestamp DESC LIMIT 1" )
707747 result = cur .fetchone ()
708748
709749 if not result :
710750 print (f"[ingest-processor] Could not find book ID for metadata fetch: { book_title } " , flush = True )
711751 return
712752
713- book_id = result [0 ]
753+ book_id = int ( result [0 ])
714754 actual_title = result [1 ]
715755
716756 print (f"[ingest-processor] Attempting to fetch metadata for: { actual_title } " , flush = True )
@@ -725,7 +765,7 @@ def fetch_metadata_if_enabled(self, book_title: str) -> None:
725765 print (f"[ingest-processor] Error fetching metadata: { e } " , flush = True )
726766
727767
728- def trigger_auto_send_if_enabled (self , book_title : str , book_path : str ) -> None :
768+ def trigger_auto_send_if_enabled (self , book_title : str | None = None , book_path : str | None = None , book_id : int | None = None ) -> None :
729769 """Trigger auto-send for users who have it enabled"""
730770 if not _CPS_AVAILABLE :
731771 print ("[ingest-processor] CPS modules not available, skipping auto-send" , flush = True )
@@ -736,19 +776,20 @@ def trigger_auto_send_if_enabled(self, book_title: str, book_path: str) -> None:
736776 return
737777
738778 try :
739- # Find the book that was just added to get its ID
740779 calibre_db_path = os .path .join (self .library_dir , 'metadata.db' )
741780 with sqlite3 .connect (calibre_db_path , timeout = 30 ) as con :
742781 cur = con .cursor ()
743- # Get the most recently added book(s) with this title
744- cur .execute ("SELECT id, title FROM books WHERE title LIKE ? ORDER BY timestamp DESC LIMIT 1" , (f"%{ book_title } %" ,))
782+ if book_id is not None :
783+ cur .execute ("SELECT id, title FROM books WHERE id = ?" , (int (book_id ),))
784+ else :
785+ cur .execute ("SELECT id, title FROM books ORDER BY timestamp DESC LIMIT 1" )
745786 result = cur .fetchone ()
746787
747788 if not result :
748789 print (f"[ingest-processor] Could not find book ID for auto-send: { book_title } " , flush = True )
749790 return
750791
751- book_id = result [0 ]
792+ book_id = int ( result [0 ])
752793 actual_title = result [1 ]
753794
754795 # Get users with auto-send enabled
@@ -768,19 +809,44 @@ def trigger_auto_send_if_enabled(self, book_title: str, book_path: str) -> None:
768809 print (f"[ingest-processor] No users with auto-send enabled found" , flush = True )
769810 return
770811
771- # Queue auto-send tasks for each user
812+ # Queue or schedule auto-send tasks for each user
772813 for user_id , username , kindle_mail in auto_send_users :
773814 try :
774815 delay_minutes = self .cwa_settings .get ('auto_send_delay_minutes' , 5 )
775-
776- # Create auto-send task
777- task_message = f"Auto-sending '{ actual_title } ' to { username } 's eReader(s)"
778- task = TaskAutoSend (task_message , book_id , user_id , delay_minutes )
779-
780- # Add to worker queue
781- WorkerThread .add (username , task )
782-
783- print (f"[ingest-processor] Queued auto-send for '{ actual_title } ' to user { username } ({ kindle_mail } )" , flush = True )
816+
817+ # Prefer to schedule in the long-lived web process so it shows in UI
818+ scheduled_via_api = False
819+ try :
820+ port = os .getenv ('CWA_PORT_OVERRIDE' , '8083' ).strip ()
821+ if not port .isdigit ():
822+ port = '8083'
823+ url = f"http://127.0.0.1:{ port } /cwa-internal/schedule-auto-send"
824+ payload = {
825+ 'book_id' : int (book_id ),
826+ 'user_id' : int (user_id ),
827+ 'delay_minutes' : int (delay_minutes ) if isinstance (delay_minutes , (int , float , str )) else 5 ,
828+ 'username' : username ,
829+ 'title' : actual_title ,
830+ }
831+ resp = requests .post (url , json = payload , timeout = 5 )
832+ if resp .status_code == 200 :
833+ try :
834+ run_at = resp .json ().get ('run_at' , 'soon' )
835+ except Exception :
836+ run_at = 'soon'
837+ print (f"[ingest-processor] Scheduled auto-send at { run_at } for '{ actual_title } ' to user { username } ({ kindle_mail } ) via web process" , flush = True )
838+ scheduled_via_api = True
839+ else :
840+ print (f"[ingest-processor] WARN: Web scheduling returned { resp .status_code } , falling back to immediate queue" , flush = True )
841+ except Exception as api_err :
842+ print (f"[ingest-processor] WARN: Failed to schedule via web API: { api_err } . Falling back to immediate queue." , flush = True )
843+
844+ if not scheduled_via_api :
845+ # Fallback: queue immediately in this process (task does not sleep)
846+ task_message = f"Auto-sending '{ actual_title } ' to { username } 's eReader(s)"
847+ task = TaskAutoSend (task_message , book_id , user_id , delay_minutes )
848+ WorkerThread .add (username , task )
849+ print (f"[ingest-processor] Queued auto-send immediately for '{ actual_title } ' to user { username } ({ kindle_mail } )" , flush = True )
784850
785851 except Exception as e :
786852 print (f"[ingest-processor] Error queuing auto-send for user { username } : { e } " , flush = True )
@@ -795,29 +861,20 @@ def refresh_cwa_session(self) -> None:
795861 This solves the issue where external calibredb adds aren't immediately visible
796862 in Calibre-Web until container restart.
797863 """
798- if not _CPS_AVAILABLE :
799- print ("[ingest-processor] CPS modules not available, skipping session refresh" , flush = True )
800- return
801-
864+ # Route DB reconnect via the long-lived web process to avoid cross-process config/session issues
802865 try :
803- # Import here to avoid circular imports and ensure CPS is available
804- from cps .tasks .database import TaskReconnectDatabase
805-
806- # Create and run the reconnect task
807- task = TaskReconnectDatabase ()
866+ port = os .getenv ('CWA_PORT_OVERRIDE' , '8083' ).strip ()
867+ if not port .isdigit ():
868+ port = '8083'
869+ url = f"http://127.0.0.1:{ port } /cwa-internal/reconnect-db"
808870 print ("[ingest-processor] Refreshing Calibre-Web database session..." , flush = True )
809-
810- # Run the task directly - this forces a database session refresh
811- # which makes newly imported books immediately visible in the UI
812- task .run (None ) # worker_thread not needed for direct execution
813-
814- print ("[ingest-processor] Database session refreshed successfully" , flush = True )
815-
816- except ImportError as e :
817- print (f"[ingest-processor] Could not import TaskReconnectDatabase: { e } " , flush = True )
871+ resp = requests .post (url , timeout = 5 )
872+ if resp .status_code == 200 :
873+ print ("[ingest-processor] Database session refresh enqueued" , flush = True )
874+ else :
875+ print (f"[ingest-processor] WARN: DB refresh endpoint returned { resp .status_code } " , flush = True )
818876 except Exception as e :
819- print (f"[ingest-processor] Error refreshing database session: { e } " , flush = True )
820- # Don't fail the import if session refresh fails
877+ print (f"[ingest-processor] WARN: Failed to call DB refresh endpoint: { e } " , flush = True )
821878 print ("[ingest-processor] Continuing despite session refresh failure - books may require manual refresh" , flush = True )
822879
823880
@@ -940,23 +997,20 @@ def main(filepath=None):
940997 print (f"[ingest-processor]: Retaining original format ({ nbp .input_format } ) for { nbp .filename } ..." , flush = True )
941998 # Find the book that was just added to get its ID
942999 try :
943- calibre_db_path = os .path .join (nbp .library_dir , 'metadata.db' )
944- with sqlite3 .connect (calibre_db_path , timeout = 30 ) as con :
945- cur = con .cursor ()
946- # Get the most recently added book - use title/author for more reliable matching
947- # in case of concurrent ingests
948- cur .execute ("""
949- SELECT id FROM books
950- WHERE path = (SELECT path FROM books ORDER BY timestamp DESC LIMIT 1)
951- ORDER BY timestamp DESC LIMIT 1
952- """ )
953- result = cur .fetchone ()
954-
955- if result :
956- book_id = result [0 ]
957- # Verify the original file still exists before trying to add it
1000+ # Prefer the exact id we just added if available
1001+ if nbp .last_added_book_id is not None :
1002+ target_book_id = nbp .last_added_book_id
1003+ else :
1004+ calibre_db_path = os .path .join (nbp .library_dir , 'metadata.db' )
1005+ with sqlite3 .connect (calibre_db_path , timeout = 30 ) as con :
1006+ cur = con .cursor ()
1007+ cur .execute ("SELECT id FROM books ORDER BY timestamp DESC LIMIT 1" )
1008+ res = cur .fetchone ()
1009+ target_book_id = res [0 ] if res else None
1010+
1011+ if target_book_id is not None :
9581012 if os .path .exists (filepath ) and os .path .getsize (filepath ) > 0 :
959- nbp .add_format_to_book (book_id , filepath )
1013+ nbp .add_format_to_book (int ( target_book_id ) , filepath )
9601014 else :
9611015 print (f"[ingest-processor] Original file no longer exists or is empty, cannot retain format: { filepath } " , flush = True )
9621016 else :
0 commit comments