-
-
Notifications
You must be signed in to change notification settings - Fork 86
fix: manual scraping crashing without existing item #1239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -310,6 +310,7 @@ async def start_manual_session( | |||||||||||||||||||||||||||||
| imdb_id: Optional[str] = None, | ||||||||||||||||||||||||||||||
| media_type: Optional[Literal["movie", "tv"]] = None, | ||||||||||||||||||||||||||||||
| magnet: Optional[str] = None, | ||||||||||||||||||||||||||||||
| session: Session = Depends(get_db), | ||||||||||||||||||||||||||||||
| ) -> StartSessionResponse: | ||||||||||||||||||||||||||||||
| session_manager.cleanup_expired(background_tasks) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -359,6 +360,10 @@ async def start_manual_session( | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if not item: | ||||||||||||||||||||||||||||||
| raise HTTPException(status_code=404, detail="Item not found") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if not item_id: | ||||||||||||||||||||||||||||||
| item = session.merge(item) | ||||||||||||||||||||||||||||||
| session.commit() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| container = downloader.get_instant_availability(info_hash, item.type) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -444,7 +449,7 @@ async def manual_update_attributes( | |||||||||||||||||||||||||||||
| request: Request, | ||||||||||||||||||||||||||||||
| session_id, | ||||||||||||||||||||||||||||||
| data: Union[DebridFile, ShowFileData], | ||||||||||||||||||||||||||||||
| session: Session = Depends(get_db), | ||||||||||||||||||||||||||||||
| db_session: Session = Depends(get_db), | ||||||||||||||||||||||||||||||
| ) -> UpdateAttributesResponse: | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Apply selected file attributes from a scraping session to the referenced media item(s). | ||||||||||||||||||||||||||||||
|
|
@@ -492,114 +497,113 @@ async def manual_update_attributes( | |||||||||||||||||||||||||||||
| prepared_item = MediaItem(item_data) | ||||||||||||||||||||||||||||||
| item = next(IndexerService().run(prepared_item), None) | ||||||||||||||||||||||||||||||
| if item: | ||||||||||||||||||||||||||||||
| session.merge(item) | ||||||||||||||||||||||||||||||
| session.commit() | ||||||||||||||||||||||||||||||
| db_session.merge(item) | ||||||||||||||||||||||||||||||
| db_session.commit() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if not item: | ||||||||||||||||||||||||||||||
| raise HTTPException(status_code=404, detail="Item not found") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item = session.merge(item) | ||||||||||||||||||||||||||||||
| item_ids_to_submit = set() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def update_item(item: MediaItem, data: DebridFile, session: ScrapingSession): | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Prepare and attach a filesystem entry and stream to a MediaItem based on a selected DebridFile within a scraping session. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Cancels any running processing job for the item and resets its state; ensures there is a staging FilesystemEntry for the given file (reusing an existing entry or creating a provisional one and persisting it), clears the item's existing filesystem_entries and links the staging entry, sets the item's active_stream to the session magnet and torrent id, appends a ranked ItemStream derived from the session, and records the item's id in the module-level item_ids_to_submit set. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Parameters: | ||||||||||||||||||||||||||||||
| item (MediaItem): The media item to update; will be merged into the active DB session as needed. | ||||||||||||||||||||||||||||||
| data (DebridFile): Selected file metadata (filename, filesize, optional download_url) used to create or locate the staging entry. | ||||||||||||||||||||||||||||||
| session (ScrapingSession): Scraping session containing the magnet and torrent_info used to set active_stream and rank the stream. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| request.app.program.em.cancel_job(item.id) | ||||||||||||||||||||||||||||||
| item.reset() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Ensure a staging MediaEntry exists and is linked | ||||||||||||||||||||||||||||||
| from sqlalchemy import select | ||||||||||||||||||||||||||||||
| from program.media.media_entry import MediaEntry | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fs_entry = None | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if item.filesystem_entry: | ||||||||||||||||||||||||||||||
| fs_entry = item.filesystem_entry | ||||||||||||||||||||||||||||||
| # Update source metadata on existing entry | ||||||||||||||||||||||||||||||
| fs_entry.original_filename = data.filename | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| # Create a provisional VIRTUAL entry (download_url/provider may be filled by downloader later) | ||||||||||||||||||||||||||||||
| fs_entry = MediaEntry.create_virtual_entry( | ||||||||||||||||||||||||||||||
| original_filename=data.filename, | ||||||||||||||||||||||||||||||
| download_url=getattr(data, "download_url", None), | ||||||||||||||||||||||||||||||
| provider=None, | ||||||||||||||||||||||||||||||
| provider_download_id=None, | ||||||||||||||||||||||||||||||
| file_size=(data.filesize or 0), | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| session.add(fs_entry) | ||||||||||||||||||||||||||||||
| session.commit() | ||||||||||||||||||||||||||||||
| session.refresh(fs_entry) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Link MediaItem to FilesystemEntry | ||||||||||||||||||||||||||||||
| # Clear existing entries and add the new one | ||||||||||||||||||||||||||||||
| item.filesystem_entries.clear() | ||||||||||||||||||||||||||||||
| item.filesystem_entries.append(fs_entry) | ||||||||||||||||||||||||||||||
| item = session.merge(item) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item.active_stream = { | ||||||||||||||||||||||||||||||
| "infohash": session.magnet, | ||||||||||||||||||||||||||||||
| "id": session.torrent_info.id, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| torrent = rtn.rank(session.torrent_info.name, session.magnet) | ||||||||||||||||||||||||||||||
| item = db_session.merge(item) | ||||||||||||||||||||||||||||||
| item_ids_to_submit = set() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def update_item(item: MediaItem, data: DebridFile, session: ScrapingSession): | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Prepare and attach a filesystem entry and stream to a MediaItem based on a selected DebridFile within a scraping session. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Cancels any running processing job for the item and resets its state; ensures there is a staging FilesystemEntry for the given file (reusing an existing entry or creating a provisional one and persisting it), clears the item's existing filesystem_entries and links the staging entry, sets the item's active_stream to the session magnet and torrent id, appends a ranked ItemStream derived from the session, and records the item's id in the module-level item_ids_to_submit set. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Ensure the item is properly attached to the session before adding streams | ||||||||||||||||||||||||||||||
| # This prevents SQLAlchemy warnings about detached objects | ||||||||||||||||||||||||||||||
| if object_session(item) is not session: | ||||||||||||||||||||||||||||||
| item = session.merge(item) | ||||||||||||||||||||||||||||||
| Parameters: | ||||||||||||||||||||||||||||||
| item (MediaItem): The media item to update; will be merged into the active DB session as needed. | ||||||||||||||||||||||||||||||
| data (DebridFile): Selected file metadata (filename, filesize, optional download_url) used to create or locate the staging entry. | ||||||||||||||||||||||||||||||
| session (ScrapingSession): Scraping session containing the magnet and torrent_info used to set active_stream and rank the stream. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| request.app.program.em.cancel_job(item.id) | ||||||||||||||||||||||||||||||
| item.reset() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item.streams.append(ItemStream(torrent)) | ||||||||||||||||||||||||||||||
| item_ids_to_submit.add(item.id) | ||||||||||||||||||||||||||||||
| # Ensure a staging MediaEntry exists and is linked | ||||||||||||||||||||||||||||||
| from sqlalchemy import select | ||||||||||||||||||||||||||||||
| from program.media.media_entry import MediaEntry | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if item.type == "movie": | ||||||||||||||||||||||||||||||
| update_item(item, data, session) | ||||||||||||||||||||||||||||||
| fs_entry = None | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if item.filesystem_entry: | ||||||||||||||||||||||||||||||
| fs_entry = item.filesystem_entry | ||||||||||||||||||||||||||||||
| # Update source metadata on existing entry | ||||||||||||||||||||||||||||||
| fs_entry.original_filename = data.filename | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| for season_number, episodes in data.root.items(): | ||||||||||||||||||||||||||||||
| for episode_number, episode_data in episodes.items(): | ||||||||||||||||||||||||||||||
| if item.type == "show": | ||||||||||||||||||||||||||||||
| if episode := item.get_absolute_episode( | ||||||||||||||||||||||||||||||
| episode_number, season_number | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| update_item(episode, episode_data, session) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||
| f"Failed to find episode {episode_number} for season {season_number} for {item.log_string}" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| elif item.type == "season": | ||||||||||||||||||||||||||||||
| if episode := item.parent.get_absolute_episode( | ||||||||||||||||||||||||||||||
| episode_number, season_number | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| update_item(episode, episode_data, session) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||
| f"Failed to find season {season_number} for {item.log_string}" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| elif item.type == "episode": | ||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||
| season_number != item.parent.number | ||||||||||||||||||||||||||||||
| and episode_number != item.number | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| update_item(item, episode_data, session) | ||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||
| # Create a provisional VIRTUAL entry (download_url/provider may be filled by downloader later) | ||||||||||||||||||||||||||||||
| fs_entry = MediaEntry.create_virtual_entry( | ||||||||||||||||||||||||||||||
| original_filename=data.filename, | ||||||||||||||||||||||||||||||
| download_url=getattr(data, "download_url", None), | ||||||||||||||||||||||||||||||
| provider=None, | ||||||||||||||||||||||||||||||
| provider_download_id=None, | ||||||||||||||||||||||||||||||
| file_size=(data.filesize or 0), | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| db_session.add(fs_entry) | ||||||||||||||||||||||||||||||
| db_session.commit() | ||||||||||||||||||||||||||||||
| db_session.refresh(fs_entry) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Link MediaItem to FilesystemEntry | ||||||||||||||||||||||||||||||
| # Clear existing entries and add the new one | ||||||||||||||||||||||||||||||
| item.filesystem_entries.clear() | ||||||||||||||||||||||||||||||
| item.filesystem_entries.append(fs_entry) | ||||||||||||||||||||||||||||||
| item = db_session.merge(item) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item.active_stream = { | ||||||||||||||||||||||||||||||
| "infohash": session.magnet, | ||||||||||||||||||||||||||||||
| "id": session.torrent_info.id, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| torrent = rtn.rank(session.torrent_info.name, session.magnet) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Ensure the item is properly attached to the session before adding streams | ||||||||||||||||||||||||||||||
| # This prevents SQLAlchemy warnings about detached objects | ||||||||||||||||||||||||||||||
| if object_session(item) is not db_session: | ||||||||||||||||||||||||||||||
| item = db_session.merge(item) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item.streams.append(ItemStream(torrent)) | ||||||||||||||||||||||||||||||
| item_ids_to_submit.add(item.id) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if item.type == "movie": | ||||||||||||||||||||||||||||||
| update_item(item, data, session) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| for season_number, episodes in data.root.items(): | ||||||||||||||||||||||||||||||
| for episode_number, episode_data in episodes.items(): | ||||||||||||||||||||||||||||||
| if item.type == "show": | ||||||||||||||||||||||||||||||
| if episode := item.get_absolute_episode( | ||||||||||||||||||||||||||||||
| episode_number, season_number | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| update_item(episode, episode_data, session) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| logger.error(f"Failed to find item type for {item.log_string}") | ||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||
| f"Failed to find episode {episode_number} for season {season_number} for {item.log_string}" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item.store_state() | ||||||||||||||||||||||||||||||
| log_string = item.log_string | ||||||||||||||||||||||||||||||
| session.merge(item) | ||||||||||||||||||||||||||||||
| session.commit() | ||||||||||||||||||||||||||||||
| elif item.type == "season": | ||||||||||||||||||||||||||||||
| if episode := item.parent.get_absolute_episode( | ||||||||||||||||||||||||||||||
| episode_number, season_number | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| update_item(episode, episode_data, session) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||
| f"Failed to find season {season_number} for {item.log_string}" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| elif item.type == "episode": | ||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||
| season_number != item.parent.number | ||||||||||||||||||||||||||||||
| and episode_number != item.number | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| update_item(item, episode_data, session) | ||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||
|
Comment on lines
+592
to
+598
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the episode matching guard. The guard uses - if (
- season_number != item.parent.number
- and episode_number != item.number
- ):
+ if (
+ season_number != item.parent.number
+ or episode_number != item.number
+ ):
continue📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+592
to
+598
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix episode guard condition With the current - if (
- season_number != item.parent.number
- and episode_number != item.number
- ):
+ if (
+ season_number != item.parent.number
+ or episode_number != item.number
+ ):🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| logger.error(f"Failed to find item type for {item.log_string}") | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| item.store_state() | ||||||||||||||||||||||||||||||
| log_string = item.log_string | ||||||||||||||||||||||||||||||
| db_session.merge(item) | ||||||||||||||||||||||||||||||
| db_session.commit() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Sync VFS to reflect any deleted/updated entries | ||||||||||||||||||||||||||||||
| # Must happen AFTER commit so the database reflects the changes | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep manual update atomic
Calling
db_session.commit()insideupdate_itemfinalizes part of the transaction before the rest of the loop finishes. If any later step raises, the request returns 500 but prior commits have already persisted partial state. Flush instead so the primary key is available without breaking atomicity.📝 Committable suggestion
🤖 Prompt for AI Agents