Skip to content
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

Add pause feature to backend #886

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

Conversation

wolfemir
Copy link
Contributor

@wolfemir wolfemir commented Nov 15, 2024

feat: Add media item pause functionality with FastAPI endpoints

Implement comprehensive pause/unpause API endpoints for media item processing control.

API Endpoints:

  • POST /items/{id}/pause - Pause single item
  • POST /items/pause?ids=id1,id2 - Bulk pause items
  • POST /items/{id}/unpause - Unpause single item
  • POST /items/unpause?ids=id1,id2 - Bulk unpause items
  • GET /items/{id}/pause - Get item pause state
  • GET /items/paused - List all paused items
  • GET /items/paused/count - Get count of paused items

Response Models:

  • PauseResponse: {message: str, ids: list[str]}
  • PauseStateResponse: {is_paused: bool, paused_at: str, item_id: str, title: str}
  • AllPausedResponse: {count: int, items: list[dict]}

Features:

  • Support for individual and bulk pause operations
  • Type-based filtering for paused item queries
  • Transaction-safe database operations
  • Detailed error handling and status codes
  • Integration with existing state transition system

Database Changes:

  • Add columns: is_paused (Boolean), paused_at (DateTime), paused_by (String)
  • Create index on is_paused for query optimization

Breaking changes: None
Migration: Required (adds new columns to MediaItem table)

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced pause and unpause functionality for media items, allowing users to manage media processing states.
    • Added new API endpoints to pause, unpause, and check the status of media items, as well as retrieve all paused items and their counts.
    • New response models enhance the structure of API responses related to media item operations.
  • Bug Fixes

    • Enhanced error handling for operations involving paused items and non-existent media.

These updates improve user control over media processing and streamline interactions with the system.

Copy link
Contributor

coderabbitai bot commented Nov 15, 2024

Walkthrough

This pull request introduces a new Alembic migration script that adds pause functionality to the MediaItem table, including four new columns: is_paused, paused_at, unpaused_at, and paused_by. It modifies the MediaItem class to incorporate these attributes and adds methods for pausing and unpausing media items. Additionally, the process_event function in the state transition module is updated to skip processing paused items. New API endpoints and response models are created to manage pause and unpause operations for media items.

Changes

File Path Change Summary
src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py Added migration script with new columns is_paused, paused_at, unpaused_at, paused_by, and an index on is_paused.
src/program/media/item.py Modified MediaItem class to include is_paused, paused_at, unpaused_at, and paused_by; added pause and unpause methods.
src/program/state_transition.py Updated process_event function to skip processing for paused items and simplify logic for handling various item states.
src/routers/secure/items.py Introduced new response models and endpoints for pausing/unpausing media items and retrieving pause states and counts.

Poem

In the burrow where the media flows,
A pause was added, as everyone knows.
With a hop and a skip, we now can say,
"Pause and unpause, let's play today!"
🐇✨
New states to track, like a dance in the night,
Media's now managed, all snug and tight!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 692a4e8 and 6e1aed6.

📒 Files selected for processing (1)
  • src/program/media/item.py (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/program/media/item.py

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (5)
src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (2)

30-31: Consider adding a composite index for query optimization.

Since the PR objectives mention querying paused items with their timestamps, consider adding a composite index on (is_paused, paused_at) to optimize these queries.

Add this after the existing index:

    # Add composite index for efficient querying of paused items by time
    op.create_index(
        op.f('ix_mediaitem_is_paused_paused_at'),
        'MediaItem',
        ['is_paused', 'paused_at']
    )

34-39: Update downgrade function if composite index is added.

If you implement the suggested composite index, ensure it's dropped in the downgrade function.

Add this before dropping the existing index:

    op.drop_index(op.f('ix_mediaitem_is_paused_paused_at'), table_name='MediaItem')
src/program/media/item.py (1)

416-433: Consider adding Paused state to handle paused items

The pause functionality is implemented, but the state determination logic (_determine_state) doesn't consider the paused state. This could lead to inconsistencies in state reporting.

Consider:

  1. Adding a Paused state to the States enum
  2. Updating _determine_state to return the Paused state when is_paused is True
  3. Ensuring that the state transitions handle the paused state appropriately

This would provide better visibility of paused items in the system and ensure consistent state reporting.

src/program/state_transition.py (1)

52-54: Consider refactoring duplicated filtering logic

The filtering logic for seasons and episodes is similar. To adhere to the DRY principle and improve maintainability, consider extracting this logic into a helper function.

Here's an example of how you might refactor the code:

+def filter_items(items):
+    return [
+        item for item in items
+        if not item.is_paused
+        and item.last_state != States.Completed
+        and Scraping.should_submit(item)
+    ]

 if existing_item.type == "show":
-    items_to_submit = [s for s in existing_item.seasons 
-                       if not s.is_paused and s.last_state != States.Completed and Scraping.should_submit(s)]
+    items_to_submit = filter_items(existing_item.seasons)
 elif existing_item.type == "season":
-    items_to_submit = [e for e in existing_item.episodes 
-                       if not e.is_paused and e.last_state != States.Completed and Scraping.should_submit(e)]
+    items_to_submit = filter_items(existing_item.episodes)

Also applies to: 56-58

src/routers/secure/items.py (1)

15-15: Confirm necessity of importing MediaType

Ensure that MediaType is required in this file. If it's not used, you can remove the import to clean up the code.

If MediaType is used in the code (which it appears to be), then this import is appropriate.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 24ee3d8 and 3e9ca0e.

📒 Files selected for processing (4)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1 hunks)
  • src/program/media/item.py (3 hunks)
  • src/program/state_transition.py (1 hunks)
  • src/routers/secure/items.py (2 hunks)
🧰 Additional context used
🪛 Ruff
src/routers/secure/items.py

449-449: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


470-470: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


511-511: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


547-547: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🔇 Additional comments (8)
src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1)

21-31: Verify transaction safety for the migration.

Multiple DDL operations are being performed. Ensure that the target database supports transactional DDL to maintain data consistency in case of migration failure.

✅ Verification successful

SQLite is used - transaction safety is guaranteed

The migration is safe as the application uses SQLite (configured as sqlite:////riven/data/media.db). SQLite automatically wraps DDL operations in transactions and provides full ACID compliance for schema changes. The migration will either complete fully or roll back completely on failure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the database configuration or environment variables indicate the database type
rg -l "DATABASE_URL|SQLALCHEMY_DATABASE_URI" --type ini --type env

Length of output: 96


Script:

#!/bin/bash
# Let's try different approaches to find database configuration
# 1. Look for common config files
fd -e yml -e yaml -e conf -e cfg -e env -e ini -e toml -e json

# 2. Search for database URL patterns in all files
rg -l "postgresql://|mysql://|sqlite://|oracle://"

# 3. Look for SQLAlchemy configuration
rg -l "SQLALCHEMY_DATABASE_URI|engine\s*=\s*create_engine"

# 4. Check alembic configuration
cat src/alembic/alembic.ini 2>/dev/null || true

Length of output: 794


Script:

#!/bin/bash
# Let's check the database configuration in docker-compose-dev.yml
cat docker-compose-dev.yml

# Also check if there are any other alembic migrations to understand the database setup pattern
fd . src/alembic/versions/ -e py

Length of output: 660

src/program/media/item.py (1)

264-265: LGTM!

The changes to to_dict() correctly expose the pause-related fields and handle the datetime conversion consistently with other similar fields.

src/program/state_transition.py (3)

20-24: LGTM: Correctly handles paused items by early return

The code appropriately checks if the existing_item is paused and skips further processing, ensuring that paused items are not processed further.


35-36: LGTM: Skips processing of paused seasons

The condition correctly skips seasons that are paused or in States.Completed or States.Unreleased, preventing unnecessary processing.


41-42: Verify consistency in handling 'Unreleased' state for episodes

In the condition for episodes, only States.Completed is checked. For seasons, both States.Completed and States.Unreleased are considered. Should episodes in States.Unreleased also be skipped to maintain consistency?

To confirm whether episodes can be in the States.Unreleased state, consider reviewing the possible states for episodes and adjust the condition if necessary.

src/routers/secure/items.py (3)

449-449: Confirm usage of Depends in default arguments aligns with FastAPI practices

The static analysis tool flagged the use of Depends in default argument values. In FastAPI, using Depends(get_db) in function parameters is the recommended way to inject dependencies.

Please verify that this use of Depends is appropriate in the FastAPI context.

Also applies to: 470-470, 491-491, 511-511, 547-547

🧰 Tools
🪛 Ruff

449-449: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


494-495: Check for potential AttributeError when accessing item properties

In get_pause_state, if item is None, attempting to access item.is_paused will raise an AttributeError. However, since you already handle the case where item is None, this should be safe.


446-489: LGTM!

The pause_items and unpause_items endpoints are well-structured and correctly implement the pause functionality.

🧰 Tools
🪛 Ruff

449-449: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


470-470: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

@@ -0,0 +1,39 @@
"""add_pause_functionality

Revision ID: [generate a new revision ID]
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace placeholder revision ID with an actual unique identifier.

The revision ID needs to be replaced with a valid unique identifier. Alembic typically uses a UUID-like string for this purpose.

Apply this change:

-Revision ID: [generate a new revision ID]
+Revision ID: c99239e3445f

And:

-revision: str = '[generate a new revision ID]'
+revision: str = 'c99239e3445f'

Also applies to: 15-15

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code rabbit has a point...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gaisberg, glad you agree. Let me know if you need any further assistance.

src/program/media/item.py Outdated Show resolved Hide resolved
Comment on lines 416 to 457
def pause(self) -> None:
"""Pause processing of this media item"""
logger.debug(f"Pausing {self.id}")
self.is_paused = True
self.paused_at = datetime.now()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")

# Get the session and flush changes
session = object_session(self)
if session:
session.flush()

def unpause(self) -> None:
"""Resume processing of this media item"""
self.is_paused = False
self.paused_at = None
logger.info(f"{self.log_string} unpaused")

Copy link
Contributor

@coderabbitai coderabbitai bot Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve transaction handling and logging consistency

Several issues need to be addressed in the pause/unpause methods:

  1. Inconsistent transaction handling: pause() flushes but unpause() doesn't
  2. Missing transaction management: changes could be lost if an error occurs
  3. Inconsistent logging: pause() has both debug and info logs, while unpause() only has info
  4. No validation of current state before pausing/unpausing

Consider this improved implementation:

     def pause(self) -> None:
         """Pause processing of this media item"""
+        if self.is_paused:
+            logger.debug(f"{self.log_string} is already paused")
+            return
+
         logger.debug(f"Pausing {self.id}")
-        self.is_paused = True
-        self.paused_at = datetime.now()
-        logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
-        
-        # Get the session and flush changes
-        session = object_session(self)
-        if session:
-            session.flush()
+        try:
+            self.is_paused = True
+            self.paused_at = datetime.now()
+            
+            session = object_session(self)
+            if session:
+                session.flush()
+                
+            logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
+        except Exception as e:
+            logger.error(f"Failed to pause {self.log_string}: {str(e)}")
+            raise

     def unpause(self) -> None:
         """Resume processing of this media item"""
-        self.is_paused = False
-        self.paused_at = None
-        logger.info(f"{self.log_string} unpaused")
+        if not self.is_paused:
+            logger.debug(f"{self.log_string} is not paused")
+            return
+
+        logger.debug(f"Unpausing {self.id}")
+        try:
+            self.is_paused = False
+            self.paused_at = None
+            
+            session = object_session(self)
+            if session:
+                session.flush()
+                
+            logger.info(f"{self.log_string} unpaused")
+        except Exception as e:
+            logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def pause(self) -> None:
"""Pause processing of this media item"""
logger.debug(f"Pausing {self.id}")
self.is_paused = True
self.paused_at = datetime.now()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
# Get the session and flush changes
session = object_session(self)
if session:
session.flush()
def unpause(self) -> None:
"""Resume processing of this media item"""
self.is_paused = False
self.paused_at = None
logger.info(f"{self.log_string} unpaused")
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gaisberg, glad you agree!

Comment on lines 550 to 578
# Build query using SQLAlchemy
query = select(func.count(MediaItem.id)).where(MediaItem.is_paused.is_(True))

if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
query = query.where(MediaItem.type == type)

logger.debug(f"Executing count query: {str(query)}")
count = db.scalar(query) or 0 # Default to 0 if None
logger.debug(f"Found {count} paused items")

return {
"total": count,
"type": type if type else "all"
}
except Exception as e:
logger.error(f"Error in get_paused_count: {str(e)}")
# Return 0 count on error
return {
"total": 0,
"type": type if type else "all",
"error": "Failed to get count"
}
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid catching broad exceptions and handle specific exceptions instead

Catching the base Exception class can mask other unexpected errors and make debugging more difficult. It's better to catch specific exceptions that you anticipate might occur.

Apply this diff to catch specific SQLAlchemy exceptions:

     try:
         # Build query using SQLAlchemy
         query = select(func.count(MediaItem.id)).where(MediaItem.is_paused.is_(True))
         # ... existing code ...
         count = db.scalar(query) or 0  # Default to 0 if None
         logger.debug(f"Found {count} paused items")
         
         return {
             "total": count,
             "type": type if type else "all"
         }
-    except Exception as e:
+    except SQLAlchemyError as e:
         logger.error(f"Error in get_paused_count: {str(e)}")
         # Return 0 count on error
         return {
             "total": 0,
             "type": type if type else "all",
             "error": "Failed to get count"
         }

Note: You'll need to import SQLAlchemyError from sqlalchemy.exc:

from sqlalchemy.exc import SQLAlchemyError

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gaisberg, glad to hear that you've made the change!


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines 460 to 469
for item in items:
item.pause()
db.commit()

Copy link
Contributor

@coderabbitai coderabbitai bot Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add exception handling for database commit operations

The db.commit() calls lack exception handling. If a commit fails, it may leave the session in an inconsistent state. It's important to handle exceptions and rollback transactions if necessary.

Consider wrapping the database operations in a try-except block:

try:
    for item in items:
        item.pause()  # or item.unpause()
    db.commit()
except Exception as e:
    db.rollback()
    logger.error(f"Failed to update items: {str(e)}")
    raise HTTPException(status_code=500, detail="Failed to update items.")

Also applies to: 481-484

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gaisberg, glad to hear that the suggestion was helpful! 😊

(_/)
(••)
(")
(")


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines 515 to 542
sql = "SELECT id, title, type, is_paused, paused_at FROM \"MediaItem\" WHERE is_paused = true"
if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
sql += f" AND type = '{type}'"

Copy link
Contributor

@coderabbitai coderabbitai bot Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use SQLAlchemy queries instead of raw SQL to prevent SQL injection

Concatenating user input directly into SQL statements can lead to SQL injection vulnerabilities. Even though there's input validation, it's safer and more maintainable to use SQLAlchemy's query builder methods.

Apply this diff to refactor the code using SQLAlchemy ORM:

-    sql = "SELECT id, title, type, is_paused, paused_at FROM \"MediaItem\" WHERE is_paused = true"
-    if type:
-        valid_types = [t.value for t in MediaType]
-        if type not in valid_types:
-            raise HTTPException(
-                status_code=400,
-                detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
-            )
-        sql += f" AND type = '{type}'"
-    
-    logger.debug(f"Executing SQL: {sql}")
-    result = db.execute(sql)
-    rows = result.fetchall()
+    query = select(
+        MediaItem.id,
+        MediaItem.title,
+        MediaItem.type,
+        MediaItem.is_paused,
+        MediaItem.paused_at
+    ).where(MediaItem.is_paused.is_(True))
+    
+    if type:
+        valid_types = [t.value for t in MediaType]
+        if type not in valid_types:
+            raise HTTPException(
+                status_code=400,
+                detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
+            )
+        query = query.where(MediaItem.type == type)
+    
+    logger.debug(f"Executing query: {str(query)}")
+    result = db.execute(query)
+    rows = result.fetchall()

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

big +1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gaisberg, glad to hear that you agree with refactoring to use SQLAlchemy queries. Let me know if you need any assistance with the implementation.

Copy link
Collaborator

@Gaisberg Gaisberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job, please look at the review comments. Also please squash the commits and make the new commit message follow conventional commits.

@@ -0,0 +1,39 @@
"""add_pause_functionality

Revision ID: [generate a new revision ID]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code rabbit has a point...

src/program/media/item.py Outdated Show resolved Hide resolved
logger.debug(f"Pausing {self.id}")
self.is_paused = True
self.paused_at = datetime.now()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up the log string, the latter attribute logs can be debug level if needed

Comment on lines 416 to 457
def pause(self) -> None:
"""Pause processing of this media item"""
logger.debug(f"Pausing {self.id}")
self.is_paused = True
self.paused_at = datetime.now()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")

# Get the session and flush changes
session = object_session(self)
if session:
session.flush()

def unpause(self) -> None:
"""Resume processing of this media item"""
self.is_paused = False
self.paused_at = None
logger.info(f"{self.log_string} unpaused")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@@ -17,45 +17,62 @@ def process_event(emitted_by: Service, existing_item: MediaItem | None = None, c
no_further_processing: ProcessedEvent = (None, [])
items_to_submit = []

#TODO - Reindex non-released badly indexed items here
# Skip processing if item is paused
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (lines: 20-25) is the only state_transition modification needed, remove the rest.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This modification is still missing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you misunderstood. Restore the original state_transition.py and just add the 3 line addition.

src/routers/secure/items.py Show resolved Hide resolved
}

@router.post("/{ids}/pause", response_model=PauseResponse, operation_id="pause_items")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use query parameters if you want to supply multiple ids, path parameters should only provide one.

Comment on lines 460 to 469
for item in items:
item.pause()
db.commit()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines 515 to 542
sql = "SELECT id, title, type, is_paused, paused_at FROM \"MediaItem\" WHERE is_paused = true"
if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
sql += f" AND type = '{type}'"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

big +1

Comment on lines 550 to 578
# Build query using SQLAlchemy
query = select(func.count(MediaItem.id)).where(MediaItem.is_paused.is_(True))

if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
query = query.where(MediaItem.type == type)

logger.debug(f"Executing count query: {str(query)}")
count = db.scalar(query) or 0 # Default to 0 if None
logger.debug(f"Found {count} paused items")

return {
"total": count,
"type": type if type else "all"
}
except Exception as e:
logger.error(f"Error in get_paused_count: {str(e)}")
# Return 0 count on error
return {
"total": 0,
"type": type if type else "all",
"error": "Failed to get count"
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (1)
src/routers/secure/items.py (1)

512-553: Consider adding pagination for large result sets.

While the implementation is good, returning all paused items without pagination could cause performance issues if there are many items.

Consider adding pagination parameters similar to the existing get_items endpoint:

@router.get("/paused", response_model=AllPausedResponse)
async def get_all_paused(
    request: Request, 
    type: Optional[str] = None,
    limit: Optional[int] = 50,
    page: Optional[int] = 1,
    db: Session = Depends(get_db)
):
    # ... existing type validation ...
    
    query = query.offset((page - 1) * limit).limit(limit)
    total_count = db.scalar(select(func.count(MediaItem.id)).where(MediaItem.is_paused.is_(True)))
    
    return AllPausedResponse(
        count=total_count,
        items=items,
        page=page,
        limit=limit,
        total_pages=(total_count + limit - 1) // limit
    )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3e9ca0e and 256fb13.

📒 Files selected for processing (3)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1 hunks)
  • src/program/media/item.py (5 hunks)
  • src/routers/secure/items.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py
🧰 Additional context used
🪛 Ruff
src/program/media/item.py

417-417: SyntaxError: unindent does not match any outer indentation level


419-419: SyntaxError: unindent does not match any outer indentation level


423-423: SyntaxError: unindent does not match any outer indentation level


424-424: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level


439-439: SyntaxError: unindent does not match any outer indentation level


443-443: SyntaxError: unindent does not match any outer indentation level


444-444: SyntaxError: Unexpected indentation

src/routers/secure/items.py

461-461: SyntaxError: Expected an indented block after try statement


464-464: SyntaxError: Expected a statement


464-464: SyntaxError: Expected a statement


464-465: SyntaxError: Expected an expression


465-465: SyntaxError: Unexpected indentation


474-474: SyntaxError: Expected a statement

🔇 Additional comments (7)
src/routers/secure/items.py (3)

47-63: LGTM! Well-structured response models.

The response models are well-documented and use appropriate types for their fields.


495-510: LGTM! Well-implemented pause state endpoint.

The endpoint properly handles the not found case and formats the datetime response correctly.


555-590: LGTM! Well-implemented count endpoint.

The endpoint has proper error handling and type validation. Good practice returning 0 with an error message on failure instead of throwing an exception.

src/program/media/item.py (4)

80-82: LGTM: Column definitions follow best practices

The new columns are properly defined with appropriate types and constraints.


93-93: Index addition previously reviewed


265-266: LGTM: Dictionary representation properly handles new fields

The implementation correctly handles type conversion and null cases.


417-456: LGTM: Robust implementation of pause/unpause functionality

The implementation includes:

  • State validation before operations
  • Proper transaction handling with session.flush()
  • Comprehensive error handling
  • Appropriate logging at debug/info levels
🧰 Tools
🪛 Ruff

417-417: SyntaxError: unindent does not match any outer indentation level


419-419: SyntaxError: unindent does not match any outer indentation level


423-423: SyntaxError: unindent does not match any outer indentation level


424-424: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level


439-439: SyntaxError: unindent does not match any outer indentation level


443-443: SyntaxError: unindent does not match any outer indentation level


444-444: SyntaxError: Unexpected indentation

Comment on lines 460 to 467
for item in items:
item.pause() # or item.unpause()
db.commit()
except Exception as e:
db.rollback()
logger.error(f"Failed to update items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to update items.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation and improve error handling.

The try-except block has incorrect indentation and should be more specific about the exceptions it catches.

Apply this diff:

    try:
-    for item in items:
-        item.pause()  # or item.unpause()
-    db.commit()
-except Exception as e:
-    db.rollback()
-    logger.error(f"Failed to update items: {str(e)}")
-    raise HTTPException(status_code=500, detail="Failed to update items.")
+        for item in items:
+            item.pause()
+        db.commit()
+    except SQLAlchemyError as e:
+        db.rollback()
+        logger.error(f"Failed to pause items: {str(e)}")
+        raise HTTPException(status_code=500, detail="Failed to pause items")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
for item in items:
item.pause() # or item.unpause()
db.commit()
except Exception as e:
db.rollback()
logger.error(f"Failed to update items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to update items.")
try:
for item in items:
item.pause()
db.commit()
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Failed to pause items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to pause items")
🧰 Tools
🪛 Ruff

461-461: SyntaxError: Expected an indented block after try statement


464-464: SyntaxError: Expected a statement


464-464: SyntaxError: Expected a statement


464-465: SyntaxError: Expected an expression


465-465: SyntaxError: Unexpected indentation

}

@router.post("/{ids}/pause", response_model=PauseResponse, operation_id="pause_items")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use query parameters for multiple IDs.

The path parameter should be used for single ID operations. For multiple IDs, use query parameters instead.

Apply this diff:

-@router.post("/{ids}/pause", response_model=PauseResponse, operation_id="pause_items")
+@router.post("/{id}/pause", response_model=PauseResponse, operation_id="pause_item")
+@router.post("/pause", response_model=PauseResponse, operation_id="pause_items")

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 474 to 499
async def unpause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
"""Unpause media items to resume processing"""
item_ids = handle_ids(ids)

items = db.execute(
select(MediaItem).where(MediaItem.id.in_(item_ids))
).unique().scalars().all()

if not items:
raise HTTPException(status_code=404, detail="No items found")

for item in items:
item.unpause()
db.commit()

return {
"message": f"Successfully unpaused {len(items)} items",
"ids": item_ids
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and use query parameters.

Similar to the pause endpoint, this endpoint needs proper error handling and should use query parameters for multiple IDs.

Apply this diff:

-@router.post("/{ids}/unpause", response_model=PauseResponse, operation_id="unpause_items")
+@router.post("/{id}/unpause", response_model=PauseResponse, operation_id="unpause_item")
+@router.post("/unpause", response_model=PauseResponse, operation_id="unpause_items")
 async def unpause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
     """Unpause media items to resume processing"""
     item_ids = handle_ids(ids)
     
     items = db.execute(
         select(MediaItem).where(MediaItem.id.in_(item_ids))
     ).unique().scalars().all()
     
     if not items:
         raise HTTPException(status_code=404, detail="No items found")
     
+    try:
         for item in items:
             item.unpause()
-    db.commit()
+        db.commit()
+    except SQLAlchemyError as e:
+        db.rollback()
+        logger.error(f"Failed to unpause items: {str(e)}")
+        raise HTTPException(status_code=500, detail="Failed to unpause items")

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff

474-474: SyntaxError: Expected a statement

Comment on lines 417 to 457
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return

logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise

def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return

logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation issues in pause/unpause methods

The methods have inconsistent indentation that needs to be fixed to match the class level.

Apply this diff to fix the indentation:

     def pause(self) -> None:
         """Pause processing of this media item"""
-        if self.is_paused:
-            logger.debug(f"{self.log_string} is already paused")
-            return
+         if self.is_paused:
+             logger.debug(f"{self.log_string} is already paused")
+             return

         logger.debug(f"Pausing {self.id}")
-        try:
-            self.is_paused = True
-            self.paused_at = datetime.now()
+         try:
+             self.is_paused = True
+             self.paused_at = datetime.now()
            
-            session = object_session(self)
-            if session:
-                session.flush()
+             session = object_session(self)
+             if session:
+                 session.flush()
                
-            logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
-        except Exception as e:
-            logger.error(f"Failed to pause {self.log_string}: {str(e)}")
-            raise
+             logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
+         except Exception as e:
+             logger.error(f"Failed to pause {self.log_string}: {str(e)}")
+             raise

     def unpause(self) -> None:
         """Resume processing of this media item"""
-        if not self.is_paused:
-            logger.debug(f"{self.log_string} is not paused")
-            return
+         if not self.is_paused:
+             logger.debug(f"{self.log_string} is not paused")
+             return

         logger.debug(f"Unpausing {self.id}")
-        try:
-            self.is_paused = False
-            self.paused_at = None
+         try:
+             self.is_paused = False
+             self.paused_at = None
            
-            session = object_session(self)
-            if session:
-                session.flush()
+             session = object_session(self)
+             if session:
+                 session.flush()
                
-            logger.info(f"{self.log_string} unpaused")
-        except Exception as e:
-            logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
-            raise
+             logger.info(f"{self.log_string} unpaused")
+         except Exception as e:
+             logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
+             raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
🧰 Tools
🪛 Ruff

417-417: SyntaxError: unindent does not match any outer indentation level


419-419: SyntaxError: unindent does not match any outer indentation level


423-423: SyntaxError: unindent does not match any outer indentation level


424-424: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level


439-439: SyntaxError: unindent does not match any outer indentation level


443-443: SyntaxError: unindent does not match any outer indentation level


444-444: SyntaxError: Unexpected indentation

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (3)
src/routers/secure/items.py (2)

518-559: Consider adding pagination for large datasets.

While the current implementation works well, consider adding pagination to handle large numbers of paused items efficiently. This would be consistent with the existing get_items endpoint that already implements pagination.

🧰 Tools
🪛 Ruff

522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


561-596: Consider implementing caching for count queries.

For better performance, especially with large datasets, consider implementing caching for the count results with a short TTL (e.g., 5 minutes). This would reduce database load for frequently accessed count data.

🧰 Tools
🪛 Ruff

565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

src/program/media/item.py (1)

417-456: Consider refactoring pause/unpause methods to reduce code duplication.

The error handling and session management logic is duplicated between the methods.

Consider extracting the common logic into a helper method:

def _update_pause_state(self, pause: bool) -> None:
    """Helper method to update pause state with proper error handling."""
    try:
        self.is_paused = pause
        self.paused_at = datetime.now() if pause else None
        
        session = object_session(self)
        if session:
            session.flush()
            
        logger.info(f"{self.log_string} {'paused' if pause else 'unpaused'}")
    except Exception as e:
        logger.error(f"Failed to {'pause' if pause else 'unpause'} {self.log_string}: {str(e)}")
        raise

def pause(self) -> None:
    """Pause processing of this media item.
    
    Raises:
        Exception: If pausing fails due to database or other errors.
    """
    if self.is_paused:
        logger.debug(f"{self.log_string} is already paused")
        return
    
    logger.debug(f"Pausing {self.id}")
    self._update_pause_state(True)

def unpause(self) -> None:
    """Resume processing of this media item.
    
    Raises:
        Exception: If unpausing fails due to database or other errors.
    """
    if not self.is_paused:
        logger.debug(f"{self.log_string} is not paused")
        return
    
    logger.debug(f"Unpausing {self.id}")
    self._update_pause_state(False)
🧰 Tools
🪛 Ruff

419-419: SyntaxError: Expected an identifier, but found a keyword 'if' that cannot be used here


419-419: SyntaxError: Simple statements must be separated by newlines or semicolons


419-420: SyntaxError: Expected an expression


421-421: SyntaxError: Expected an identifier, but found a keyword 'return' that cannot be used here


423-423: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 256fb13 and 51682f0.

📒 Files selected for processing (2)
  • src/program/media/item.py (5 hunks)
  • src/routers/secure/items.py (2 hunks)
🧰 Additional context used
🪛 Ruff
src/program/media/item.py

419-419: SyntaxError: Expected an identifier, but found a keyword 'if' that cannot be used here


419-419: SyntaxError: Simple statements must be separated by newlines or semicolons


419-420: SyntaxError: Expected an expression


421-421: SyntaxError: Expected an identifier, but found a keyword 'return' that cannot be used here


423-423: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level

src/routers/secure/items.py

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

🔇 Additional comments (5)
src/routers/secure/items.py (3)

23-63: LGTM! Well-structured response models.

The response models are properly defined using Pydantic BaseModel with appropriate type hints and documentation.


501-516: LGTM! Well-implemented pause state check.

The endpoint properly handles item lookup and returns appropriate response with comprehensive details.

🧰 Tools
🪛 Ruff

502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


448-499: Verify pause state handling in event processing.

Ensure that the event processing system properly respects the pause state of items.

✅ Verification successful

Let me gather more specific information about how the pause state is handled.


Pause state is properly handled in event processing system

The verification shows that pause functionality is well-integrated:

  • State transition system explicitly checks is_paused and skips processing of paused items
  • Pause state is properly cascaded in show/season hierarchies, with paused items filtered out
  • MediaItem class has proper database schema support with indexed columns and complete pause/unpause implementations
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if event processing code handles paused items

# Search for pause state checks in event processing
rg -l "is_paused" --type py
ast-grep --pattern 'if $item.is_paused:
  $$$
'

Length of output: 238


Script:

#!/bin/bash
# Check implementation details of pause handling

# Check state transition logic
rg -A 10 "is_paused" src/program/state_transition.py

# Check MediaItem class implementation
rg -A 5 "is_paused" src/program/media/item.py

# Look for event processing logic
ast-grep --pattern 'def process_$event($$$) {
  $$$
}'

Length of output: 4316

🧰 Tools
🪛 Ruff

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

src/program/media/item.py (2)

80-82: LGTM! Well-structured pause-related fields and index.

The implementation includes:

  • Appropriate column types with correct nullability
  • Performance-optimizing index on is_paused
  • Clear documentation

Also applies to: 93-93


265-266: LGTM! Consistent dictionary representation of pause fields.

The implementation follows the established pattern for datetime fields and includes proper null handling.

src/routers/secure/items.py Outdated Show resolved Hide resolved
Comment on lines 417 to 456
def pause(self) -> None:
"""Pause processing of this media item"""
+ if self.is_paused:
+ logger.debug(f"{self.log_string} is already paused")
+ return

logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise

def unpause(self) -> None:
"""Resume processing of this media item"""

if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return

logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation issues in pause/unpause methods.

The methods have inconsistent indentation that needs to be fixed.

Apply this diff to fix the indentation:

     def pause(self) -> None:
         """Pause processing of this media item"""
-         if self.is_paused:
-             logger.debug(f"{self.log_string} is already paused")
-             return
+        if self.is_paused:
+            logger.debug(f"{self.log_string} is already paused")
+            return

-         logger.debug(f"Pausing {self.id}")
-         try:
-             self.is_paused = True
-             self.paused_at = datetime.now()
+        logger.debug(f"Pausing {self.id}")
+        try:
+            self.is_paused = True
+            self.paused_at = datetime.now()
            
-             session = object_session(self)
-             if session:
-                 session.flush()
+            session = object_session(self)
+            if session:
+                session.flush()
                
-             logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
-         except Exception as e:
-             logger.error(f"Failed to pause {self.log_string}: {str(e)}")
-             raise
+            logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
+        except Exception as e:
+            logger.error(f"Failed to pause {self.log_string}: {str(e)}")
+            raise

     def unpause(self) -> None:
         """Resume processing of this media item"""
-         
-         if not self.is_paused:
-             logger.debug(f"{self.log_string} is not paused")
-             return
+        if not self.is_paused:
+            logger.debug(f"{self.log_string} is not paused")
+            return

-         logger.debug(f"Unpausing {self.id}")
-         try:
-             self.is_paused = False
-             self.paused_at = None
+        logger.debug(f"Unpausing {self.id}")
+        try:
+            self.is_paused = False
+            self.paused_at = None
            
-             session = object_session(self)
-             if session:
-                 session.flush()
+            session = object_session(self)
+            if session:
+                session.flush()
                
-             logger.info(f"{self.log_string} unpaused")
-         except Exception as e:
-             logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
-             raise
+            logger.info(f"{self.log_string} unpaused")
+        except Exception as e:
+            logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def pause(self) -> None:
"""Pause processing of this media item"""
+ if self.is_paused:
+ logger.debug(f"{self.log_string} is already paused")
+ return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
🧰 Tools
🪛 Ruff

419-419: SyntaxError: Expected an identifier, but found a keyword 'if' that cannot be used here


419-419: SyntaxError: Simple statements must be separated by newlines or semicolons


419-420: SyntaxError: Expected an expression


421-421: SyntaxError: Expected an identifier, but found a keyword 'return' that cannot be used here


423-423: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (1)
src/program/media/item.py (1)

417-456: Consider refactoring pause/unpause methods to reduce duplication.

The pause and unpause methods share similar patterns for session handling and error handling. Consider extracting common functionality into a helper method.

Here's a suggested refactoring:

def _update_pause_state(self, pause: bool) -> None:
    """Helper method to update pause state with proper error handling."""
    action = "pause" if pause else "unpause"
    
    if pause == self.is_paused:
        logger.debug(f"{self.log_string} is already {'paused' if pause else 'not paused'}")
        return
        
    logger.debug(f"{action.capitalize()}ing {self.id}")
    try:
        self.is_paused = pause
        self.paused_at = datetime.now() if pause else None
        
        session = object_session(self)
        if session:
            session.flush()
            
        logger.info(f"{self.log_string} {action}d")
    except Exception as e:
        logger.error(f"Failed to {action} {self.log_string}: {str(e)}")
        raise

def pause(self) -> None:
    """Pause processing of this media item"""
    self._update_pause_state(True)

def unpause(self) -> None:
    """Resume processing of this media item"""
    self._update_pause_state(False)
🧰 Tools
🪛 Ruff

419-419: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 51682f0 and 048a761.

📒 Files selected for processing (1)
  • src/program/media/item.py (5 hunks)
🧰 Additional context used
🪛 Ruff
src/program/media/item.py

419-419: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level

🔇 Additional comments (3)
src/program/media/item.py (3)

80-82: LGTM: Well-structured column definitions.

The new pause-related columns are properly defined with appropriate types, nullability, and defaults.


93-93: LGTM: Index properly added.

The requested index for the is_paused column has been added as suggested.


265-266: LGTM: Consistent dictionary representation.

The pause-related fields are properly added to the dictionary representation, with consistent datetime handling.

Comment on lines 417 to 456
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return

logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise

def unpause(self) -> None:
"""Resume processing of this media item"""

if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return

logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation in pause/unpause methods.

The methods have inconsistent indentation that needs to be fixed.

Apply this diff to fix the indentation:

     def pause(self) -> None:
         """Pause processing of this media item"""
-         if self.is_paused:
-             logger.debug(f"{self.log_string} is already paused")
-             return
+        if self.is_paused:
+            logger.debug(f"{self.log_string} is already paused")
+            return

-         logger.debug(f"Pausing {self.id}")
-         try:
-             self.is_paused = True
-             self.paused_at = datetime.now()
+        logger.debug(f"Pausing {self.id}")
+        try:
+            self.is_paused = True
+            self.paused_at = datetime.now()
            
-             session = object_session(self)
-             if session:
-                 session.flush()
+            session = object_session(self)
+            if session:
+                session.flush()
                
-             logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
-         except Exception as e:
-             logger.error(f"Failed to pause {self.log_string}: {str(e)}")
-             raise
+            logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
+        except Exception as e:
+            logger.error(f"Failed to pause {self.log_string}: {str(e)}")
+            raise

     def unpause(self) -> None:
         """Resume processing of this media item"""
-         
-         if not self.is_paused:
-             logger.debug(f"{self.log_string} is not paused")
-             return
+        if not self.is_paused:
+            logger.debug(f"{self.log_string} is not paused")
+            return

-         logger.debug(f"Unpausing {self.id}")
-         try:
-             self.is_paused = False
-             self.paused_at = None
+        logger.debug(f"Unpausing {self.id}")
+        try:
+            self.is_paused = False
+            self.paused_at = None
            
-             session = object_session(self)
-             if session:
-                 session.flush()
+            session = object_session(self)
+            if session:
+                session.flush()
                
-             logger.info(f"{self.log_string} unpaused")
-         except Exception as e:
-             logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
-             raise
+            logger.info(f"{self.log_string} unpaused")
+        except Exception as e:
+            logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
🧰 Tools
🪛 Ruff

419-419: SyntaxError: Unexpected indentation


437-437: SyntaxError: unindent does not match any outer indentation level

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
src/program/media/item.py (1)

265-266: Consider consistent datetime string conversion.

The paused_at conversion could be made consistent with other datetime fields in the dictionary (e.g., requested_at, scraped_at).

-            "paused_at": str(self.paused_at) if self.paused_at else None,
+            "paused_at": str(self.paused_at) if self.paused_at else None,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 048a761 and 33f7930.

📒 Files selected for processing (1)
  • src/program/media/item.py (5 hunks)
🧰 Additional context used
🪛 Ruff
src/program/media/item.py

444-444: SyntaxError: unindent does not match any outer indentation level


449-449: SyntaxError: unindent does not match any outer indentation level


450-450: SyntaxError: Unexpected indentation

🔇 Additional comments (2)
src/program/media/item.py (2)

80-82: LGTM: Well-defined pause-related attributes.

The new attributes are properly typed and have appropriate default values.


93-93: Index addition already reviewed.

Comment on lines 417 to 436
def pause(self) -> None:
"""Pause processing of this media item"""

if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return


logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()


session = object_session(self)
if session:
session.flush()


logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation in pause method.

The method has inconsistent indentation that should be fixed.

     def pause(self) -> None:
         """Pause processing of this media item"""
-
         if self.is_paused:
             logger.debug(f"{self.log_string} is already paused")
             return

-
         logger.debug(f"Pausing {self.id}")
         try:
             self.is_paused = True
             self.paused_at = datetime.now()
             
-
             session = object_session(self)
             if session:
                 session.flush()
                 
-
             logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
         except Exception as e:
             logger.error(f"Failed to pause {self.log_string}: {str(e)}")
             raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise

Comment on lines 441 to 457
def unpause(self) -> None:
"""Resume processing of this media item"""

if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return


logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None


session = object_session(self)
if session:
session.flush()


logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation and improve logging in unpause method.

The method has indentation issues and inconsistent logging compared to the pause method.

     def unpause(self) -> None:
-         """Resume processing of this media item"""
+        """Resume processing of this media item"""
 
         if not self.is_paused:
             logger.debug(f"{self.log_string} is not paused")
             return
 
-
         logger.debug(f"Unpausing {self.id}")
         try:
             self.is_paused = False
             self.paused_at = None
             
-
             session = object_session(self)
             if session:
                 session.flush()
                 
-
-            logger.info(f"{self.log_string} unpaused")
+            logger.info(f"{self.log_string} unpaused, is_paused={self.is_paused}, paused_at={self.paused_at}")
         except Exception as e:
             logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
             raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
def unpause(self) -> None:
"""Resume processing of this media item"""
if not self.is_paused:
logger.debug(f"{self.log_string} is not paused")
return
logger.debug(f"Unpausing {self.id}")
try:
self.is_paused = False
self.paused_at = None
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} unpaused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
raise
🧰 Tools
🪛 Ruff

444-444: SyntaxError: unindent does not match any outer indentation level


449-449: SyntaxError: unindent does not match any outer indentation level


450-450: SyntaxError: Unexpected indentation

Copy link
Contributor Author

@wolfemir wolfemir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@@ -17,45 +17,62 @@ def process_event(emitted_by: Service, existing_item: MediaItem | None = None, c
no_further_processing: ProcessedEvent = (None, [])
items_to_submit = []

#TODO - Reindex non-released badly indexed items here
# Skip processing if item is paused
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This modification is still missing

@wolfemir
Copy link
Contributor Author

wolfemir commented Nov 19, 2024

@Gaisberg i have removed the rest in state transition, please let me know if anything else need to be changed

@Gaisberg
Copy link
Collaborator

@Gaisberg i have removed the rest in state transition, please let me know if anything else need to be changed

Please squash and amend the commit message to follow conventional commits

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (2)
src/routers/secure/items.py (2)

501-516: Optimize database query and fix dependency injection.

The endpoint is well-structured but could be improved:

  1. Fix dependency injection pattern
  2. Select only required fields in the database query

Apply this diff:

 @router.get("/{id}/pause", response_model=PauseStateResponse, operation_id="get_pause_state")
-async def get_pause_state(request: Request, id: str, db: Session = Depends(get_db)):
+async def get_pause_state(request: Request, id: str):
+    db = next(get_db())
     """Check if a media item is paused"""
     item = db.execute(
-        select(MediaItem).where(MediaItem.id == id)
+        select(
+            MediaItem.id,
+            MediaItem.title,
+            MediaItem.is_paused,
+            MediaItem.paused_at
+        ).where(MediaItem.id == id)
     ).unique().scalar_one_or_none()
🧰 Tools
🪛 Ruff

502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


518-559: Optimize query performance in get_all_paused endpoint.

The implementation is good but could be improved:

  1. Add pagination to prevent potential memory issues with large result sets
  2. Consider adding sorting options

Would you like me to provide an implementation with pagination and sorting support?

🧰 Tools
🪛 Ruff

522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0f75517 and 7a103f6.

📒 Files selected for processing (4)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1 hunks)
  • src/program/media/item.py (5 hunks)
  • src/program/state_transition.py (1 hunks)
  • src/routers/secure/items.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py
  • src/program/media/item.py
  • src/program/state_transition.py
🧰 Additional context used
🪛 Ruff
src/routers/secure/items.py

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

🔇 Additional comments (3)
src/routers/secure/items.py (3)

23-63: LGTM! Well-structured response models.

The response models are well-defined with proper type hints and clear docstrings.


561-596: LGTM! Well-implemented count endpoint.

The implementation includes:

  • Proper error handling with SQLAlchemyError
  • Type validation
  • Clear logging
  • Sensible defaults
🧰 Tools
🪛 Ruff

565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)


448-499: Verify integration with state transition system.

The pause functionality may impact the state transition system. Let's verify the integration:

✅ Verification successful

Pause functionality is safely integrated with state transition system

Based on the codebase analysis:

  1. The pause functionality is properly isolated from the state transition system:

    • is_paused and paused_at are separate fields from the state machine
    • State transitions are skipped for paused items (src/program/state_transition.py: if existing_item and existing_item.is_paused:)
    • Pause/unpause operations don't interfere with the item's state (last_state remains unchanged)
  2. The implementation includes proper safeguards:

    • Database schema has an index on is_paused for efficient queries
    • Pause operations are wrapped in try-catch blocks with proper rollback
    • State transitions are preserved when items are unpaused
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for state transition handling of paused items

# Search for state transition logic handling paused items
ast-grep --pattern 'if $item.is_paused:
  $$$
'

# Search for potential race conditions in state transitions
rg -A 5 'process_event.*is_paused'

Length of output: 86


Script:

#!/bin/bash
# Let's check the MediaItem model and state transition logic

# Find MediaItem class definition and its state-related methods
ast-grep --pattern 'class MediaItem($$$):
  $$$
  pause($$$) {
    $$$
  }
  $$$
'

# Look for state transition handling
rg -l "state.*transition|process.*state|state.*machine" 

# Check for pause-related state fields
rg -A 3 "is_paused|paused_at|state"

# Look for event processing related to pause
rg -A 5 "def process|handle.*event"

Length of output: 55351

🧰 Tools
🪛 Ruff

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
src/routers/secure/items.py (1)

561-596: LGTM! Well-implemented error handling.

The get_paused_count endpoint has proper error handling and uses SQLAlchemy's query builder correctly. Just fix the dependency injection pattern.

Apply this small diff:

-async def get_paused_count(request: Request, type: Optional[str] = None, db: Session = Depends(get_db)):
+async def get_paused_count(request: Request, type: Optional[str] = None):
+    db = next(get_db())
🧰 Tools
🪛 Ruff

565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 7a103f6 and 31677a2.

📒 Files selected for processing (4)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1 hunks)
  • src/program/media/item.py (5 hunks)
  • src/program/state_transition.py (1 hunks)
  • src/routers/secure/items.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py
  • src/program/media/item.py
  • src/program/state_transition.py
🧰 Additional context used
🪛 Ruff
src/routers/secure/items.py

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

🔇 Additional comments (2)
src/routers/secure/items.py (2)

23-63: LGTM! Well-structured response models.

The response models are well-defined with appropriate type hints and documentation.


518-559: 🛠️ Refactor suggestion

Add error handling and improve query building.

The endpoint needs error handling for database operations and should use parameterized queries consistently.

Apply this diff:

-async def get_all_paused(request: Request, type: Optional[str] = None, db: Session = Depends(get_db)):
+async def get_all_paused(request: Request, type: Optional[str] = None):
+    db = next(get_db())
     """Get all paused media items, optionally filtered by type"""
-    # Use raw SQL to verify data
-    query = select(
-        MediaItem.id,
-        MediaItem.title,
-        MediaItem.type,
-        MediaItem.is_paused,
-        MediaItem.paused_at
-    ).where(MediaItem.is_paused.is_(True))
+    try:
+        query = select(
+            MediaItem.id,
+            MediaItem.title,
+            MediaItem.type,
+            MediaItem.is_paused,
+            MediaItem.paused_at
+        ).where(MediaItem.is_paused.is_(True))

Likely invalid or redundant comment.

🧰 Tools
🪛 Ruff

522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

src/routers/secure/items.py Outdated Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (3)
src/program/state_transition.py (1)

26-26: Remove the uncertain comment.

Based on previous review comments from Gaisberg, the state transition modification for paused items is the only change needed. You can safely remove this comment.

src/routers/secure/items.py (2)

448-500: Consider refactoring to reduce code duplication

The pause_items and unpause_items functions share significant duplicated code. Refactoring common logic into a helper function can improve maintainability.

You can create a helper function that handles pausing and unpausing items based on a parameter. This reduces duplication and centralizes the logic.

🧰 Tools
🪛 Ruff (0.8.0)

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


525-525: Update misleading comment

The comment # Use raw SQL to verify data is misleading because the code uses SQLAlchemy's query builder, not raw SQL. Consider updating the comment to accurately reflect the code.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 31677a2 and c147a75.

📒 Files selected for processing (3)
  • src/program/media/item.py (5 hunks)
  • src/program/state_transition.py (1 hunks)
  • src/routers/secure/items.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.8.0)
src/routers/secure/items.py

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

🔇 Additional comments (8)
src/program/state_transition.py (1)

21-25: LGTM! Early exit for paused items is well implemented.

The implementation correctly checks for paused items and exits early with appropriate logging.

src/program/media/item.py (4)

64-66: LGTM! Well-structured model attributes.

The pause-related columns are properly defined with appropriate types, nullability, and defaults.


77-77: LGTM! Index added as specified.

The index on is_paused has been added with proper naming convention, which will optimize queries filtering on the pause state.


249-250: LGTM! Consistent dictionary representation.

The pause-related fields are properly included in the API response with consistent datetime formatting and null handling.


421-440: 🛠️ Refactor suggestion

Improve unpause method implementation.

Similar improvements needed for consistency with the pause method:

  1. Remove extra blank lines
  2. Make logging consistent with pause method
  3. Consider moving session handling to a separate method

Apply this diff:

     def unpause(self) -> None:
         """Resume processing of this media item"""
-
         if not self.is_paused:
             logger.debug(f"{self.log_string} is not paused")
             return
-
         logger.debug(f"Unpausing {self.id}")
         try:
             self.is_paused = False
             self.paused_at = None
-            
+            self.paused_by = None
             session = object_session(self)
             if session:
                 session.flush()
-                
             logger.info(f"{self.log_string} unpaused, is_paused={self.is_paused}, paused_at={self.paused_at}")
         except Exception as e:
             logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
             raise

Likely invalid or redundant comment.

src/routers/secure/items.py (3)

491-491: ⚠️ Potential issue

Undefined name SQLAlchemyError

The exception SQLAlchemyError is used without importing it. Please import it from sqlalchemy.exc.

Apply this diff:

+from sqlalchemy.exc import SQLAlchemyError  # Add at the top with other imports
🧰 Tools
🪛 Ruff (0.8.0)

491-491: Undefined name SQLAlchemyError

(F821)


589-589: ⚠️ Potential issue

Undefined name SQLAlchemyError

The exception SQLAlchemyError is used without importing it. Please import it from sqlalchemy.exc.

Apply this diff:

+from sqlalchemy.exc import SQLAlchemyError  # Add at the top with other imports
🧰 Tools
🪛 Ruff (0.8.0)

589-589: Undefined name SQLAlchemyError

(F821)


565-565: 🛠️ Refactor suggestion

Avoid function calls in default argument values

Avoid using Depends(get_db) in default arguments. Obtain the database session inside the function.

Apply this diff:

-async def get_paused_count(
-    request: Request,
-    type: Optional[str] = None,
-    db: Session = Depends(get_db)
-):
+async def get_paused_count(
+    request: Request,
+    type: Optional[str] = None
+):
+    db: Session = Depends(get_db)

Likely invalid or redundant comment.

🧰 Tools
🪛 Ruff (0.8.0)

565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

src/program/state_transition.py Show resolved Hide resolved
Comment on lines 64 to 66
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing paused_by column.

The PR objectives mention a paused_by column, but it's not implemented in the model.

Add the following column definition:

     # Pause related fields
     is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False)
     paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
+    paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)

Comment on lines 401 to 419
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return

logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()

session = object_session(self)
if session:
session.flush()

logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve pause method implementation.

The pause method has several areas for improvement:

  1. Consider adding the paused_by field mentioned in PR objectives
  2. Remove extra blank lines for better readability
  3. Consider moving session handling to a separate method

Apply this diff:

     def pause(self) -> None:
         """Pause processing of this media item"""
         if self.is_paused:
             logger.debug(f"{self.log_string} is already paused")
             return
-
         logger.debug(f"Pausing {self.id}")
         try:
             self.is_paused = True
             self.paused_at = datetime.now()
-            
+            self.paused_by = None  # TODO: Add user context
             session = object_session(self)
             if session:
                 session.flush()
-                
             logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
         except Exception as e:
             logger.error(f"Failed to pause {self.log_string}: {str(e)}")
             raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise
def pause(self) -> None:
"""Pause processing of this media item"""
if self.is_paused:
logger.debug(f"{self.log_string} is already paused")
return
logger.debug(f"Pausing {self.id}")
try:
self.is_paused = True
self.paused_at = datetime.now()
self.paused_by = None # TODO: Add user context
session = object_session(self)
if session:
session.flush()
logger.info(f"{self.log_string} paused, is_paused={self.is_paused}, paused_at={self.paused_at}")
except Exception as e:
logger.error(f"Failed to pause {self.log_string}: {str(e)}")
raise

db.rollback()
logger.error(f"Failed to unpause items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to unpause items")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use exception chaining when re-raising exceptions

When raising an exception in an except block, use from e to maintain the original traceback information.

Apply this diff:

-        raise HTTPException(status_code=500, detail="Failed to unpause items")
+        raise HTTPException(status_code=500, detail="Failed to unpause items") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise HTTPException(status_code=500, detail="Failed to unpause items")
raise HTTPException(status_code=500, detail="Failed to unpause items") from e
🧰 Tools
🪛 Ruff (0.8.0)

494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

db.rollback()
logger.error(f"Failed to update items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to update items.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use exception chaining when re-raising exceptions

When raising an exception inside an except block, use from e to preserve the original traceback and provide better context.

Apply this diff:

-        raise HTTPException(status_code=500, detail="Failed to update items.")
+        raise HTTPException(status_code=500, detail="Failed to update items.") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise HTTPException(status_code=500, detail="Failed to update items.")
raise HTTPException(status_code=500, detail="Failed to update items.") from e
🧰 Tools
🪛 Ruff (0.8.0)

468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

@router.post("/{id}/unpause", response_model=PauseResponse, operation_id="unpause_item")
@router.post("/unpause", response_model=PauseResponse, operation_id="unpause_items")
async def unpause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid function calls in default argument values

Using Depends(get_db) in function defaults is discouraged. Obtain the database session within the function body.

Apply this diff:

-async def unpause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
+async def unpause_items(request: Request, ids: str = None):
+    db: Session = Depends(get_db)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.0)

477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


@router.get("/{id}/pause", response_model=PauseStateResponse, operation_id="get_pause_state")
async def get_pause_state(request: Request, id: str, db: Session = Depends(get_db)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid function calls in default argument values

Using Depends(get_db) in default arguments can lead to unexpected behavior. It's better to obtain the database session inside the function.

Apply this diff:

-async def get_pause_state(request: Request, id: str, db: Session = Depends(get_db)):
+async def get_pause_state(request: Request, id: str):
+    db: Session = Depends(get_db)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.0)

502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

request: Request,
type: Optional[str] = None,
db: Session = Depends(get_db)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid function calls in default argument values

Using Depends(get_db) as a default argument is not recommended. Acquire the database session within the function body instead.

Apply this diff:

-async def get_all_paused(
-    request: Request, 
-    type: Optional[str] = None,
-    db: Session = Depends(get_db)
-):
+async def get_all_paused(
+    request: Request, 
+    type: Optional[str] = None
+):
+    db: Session = Depends(get_db)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.0)

522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

@router.post("/{id}/pause", response_model=PauseResponse, operation_id="pause_item")
@router.post("/pause", response_model=PauseResponse, operation_id="pause_items")
async def pause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid function calls in default argument values

Using Depends(get_db) in function argument defaults can lead to unexpected behavior. It's recommended to obtain the database session inside the function body.

Apply this diff:

-async def pause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
+async def pause_items(request: Request, ids: str = None):
+    db: Session = Depends(get_db)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.0)

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (3)
src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1)

26-29: Consider using timezone-aware DateTime columns.

For accurate tracking of pause/unpause events across different timezones, consider using DateTime(timezone=True).

-        sa.Column('paused_at', sa.DateTime(), nullable=True))
+        sa.Column('paused_at', sa.DateTime(timezone=True), nullable=True))
-        sa.Column('unpaused_at', sa.DateTime(), nullable=True))
+        sa.Column('unpaused_at', sa.DateTime(timezone=True), nullable=True))
src/routers/secure/items.py (1)

561-596: Improve error handling and logging security.

  1. Avoid logging raw SQL queries
  2. Consider returning a 500 status code on database errors instead of a success response with error field

Apply this diff:

-        logger.debug(f"Executing count query: {str(query)}")
+        logger.debug("Executing get_paused_count query")
         count = db.scalar(query) or 0  # Default to 0 if None
         logger.debug(f"Found {count} paused items")
         
         return {
             "total": count,
             "type": type if type else "all"
         }
     except SQLAlchemyError as e:
         logger.error(f"Error in get_paused_count: {str(e)}")
-        # Return 0 count on error
-        return {
-            "total": 0,
-            "type": type if type else "all",
-            "error": "Failed to get count"
-        }
+        raise HTTPException(status_code=500, detail="Failed to get paused count") from e
🧰 Tools
🪛 Ruff (0.8.0)

565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

src/program/media/item.py (1)

425-446: Make logging consistent between pause/unpause methods.

The unpause method should follow the same logging pattern as the pause method for consistency.

Update the logging in unpause method:

-    logger.info(f"{self.log_string} unpaused, is_paused={self.is_paused}, unpaused_at={self.unpaused_at}")
+    logger.info(f"{self.log_string} unpaused, is_paused={self.is_paused}, paused_at={self.paused_at}, unpaused_at={self.unpaused_at}")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c147a75 and f395b7f.

📒 Files selected for processing (3)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1 hunks)
  • src/program/media/item.py (5 hunks)
  • src/routers/secure/items.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.8.0)
src/routers/secure/items.py

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


465-465: Undefined name SQLAlchemyError

(F821)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


565-565: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


589-589: Undefined name SQLAlchemyError

(F821)

🔇 Additional comments (9)
src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (4)

16-16: Replace placeholder revision ID with the actual identifier.

The revision ID on line 16 still contains a placeholder while lines 3-4 have the correct value.


33-34: LGTM! Index creation looks good.

The index on is_paused is well-named and will optimize the performance of queries filtering on pause state.


37-43: LGTM! Downgrade operations are complete and correctly ordered.

The downgrade function properly reverses all changes made in the upgrade function, maintaining referential integrity by dropping the index before the columns.


22-34: Verify the migration's impact on existing data.

The migration adds new columns but doesn't populate them with data. Please verify:

  1. If existing records need a data backfill strategy
  2. If the application code handles null values appropriately for these new columns
✅ Verification successful

Migration is safe, no data backfill needed

The codebase shows proper handling of the new pause-related columns:

  1. All new columns are nullable with appropriate defaults:

    • is_paused has default=False
    • paused_at, unpaused_at, and paused_by are nullable
  2. The application code in src/program/media/item.py properly handles these fields:

    • Null checks are present when accessing datetime fields
    • Clear pause/unpause logic that manages these fields appropriately
    • State transitions skip paused items safely
  3. API responses in src/routers/secure/items.py handle null values correctly through Optional types and null checks

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for null checks on these columns in the codebase
rg -A 5 "is_paused|paused_at|unpaused_at|paused_by" --type python

# Look for potential data population logic
ast-grep --pattern 'def upgrade() {
  $$$
  # Look for data updates
  op.execute($_)
  $$$
}'

Length of output: 194


Script:

#!/bin/bash
# Search for null checks and usage of these columns in the codebase
rg -A 5 "is_paused|paused_at|unpaused_at|paused_by"

# Look for models and schema definitions
rg -A 10 "class MediaItem"

# Look for any data population or migration scripts
fd "migration|populate" --type f

Length of output: 10241

src/routers/secure/items.py (2)

23-63: LGTM! Well-structured response models.

The response models are well-defined with proper type hints and documentation.


475-499: Apply the same fixes as the pause endpoint.

The unpause endpoint has the same dependency injection and exception handling issues.

🧰 Tools
🪛 Ruff (0.8.0)

477-477: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


491-491: Undefined name SQLAlchemyError

(F821)


494-494: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

src/program/media/item.py (3)

64-68: LGTM! Column definitions are well-structured.

The pause-related columns are properly defined with appropriate types, nullable constraints, and default values.


79-79: LGTM! Index on is_paused implemented as requested.

The index will improve query performance when filtering paused items.


770-770: LGTM! Clear error handling for invalid item types.

The error message clearly indicates the issue when an invalid item type is provided.

Comment on lines +30 to +31
op.add_column('MediaItem',
sa.Column('paused_by', sa.String(), nullable=True))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add length constraint to the paused_by String column.

Unbounded string columns can lead to storage issues. Consider adding a reasonable length constraint that aligns with your user ID or username field lengths.

-        sa.Column('paused_by', sa.String(), nullable=True))
+        sa.Column('paused_by', sa.String(length=255), nullable=True))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
op.add_column('MediaItem',
sa.Column('paused_by', sa.String(), nullable=True))
op.add_column('MediaItem',
sa.Column('paused_by', sa.String(length=255), nullable=True))

Comment on lines 501 to 516
async def get_pause_state(request: Request, id: str, db: Session = Depends(get_db)):
"""Check if a media item is paused"""
item = db.execute(
select(MediaItem).where(MediaItem.id == id)
).unique().scalar_one_or_none()

if not item:
raise HTTPException(status_code=404, detail="Item not found")

return {
"is_paused": item.is_paused,
"paused_at": str(item.paused_at) if item.paused_at else None,
"item_id": item.id,
"title": item.title
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for database operations.

The endpoint should handle potential database errors and use proper dependency injection.

Apply this diff:

-async def get_pause_state(request: Request, id: str, db: Session = Depends(get_db)):
+async def get_pause_state(request: Request, id: str):
+    db = next(get_db())
     """Check if a media item is paused"""
-    item = db.execute(
-        select(MediaItem).where(MediaItem.id == id)
-    ).unique().scalar_one_or_none()
+    try:
+        item = db.execute(
+            select(MediaItem).where(MediaItem.id == id)
+        ).unique().scalar_one_or_none()
+    except SQLAlchemyError as e:
+        logger.error(f"Failed to fetch pause state: {str(e)}")
+        raise HTTPException(status_code=500, detail="Failed to fetch pause state") from e

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.0)

502-502: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

Comment on lines 448 to 473
@router.post("/pause", response_model=PauseResponse, operation_id="pause_items")
async def pause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
"""Pause media items from being processed"""
item_ids = handle_ids(ids)

items = db.execute(
select(MediaItem).where(MediaItem.id.in_(item_ids))
).unique().scalars().all()

if not items:
raise HTTPException(status_code=404, detail="No items found")

try:
for item in items:
item.pause()
db.commit()
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Failed to pause items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to pause items")

return {
"message": f"Successfully paused {len(items)} items",
"ids": item_ids
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix dependency injection and exception handling.

Several improvements needed:

  1. Move database session acquisition into the function body
  2. Add exception chaining
  3. Import SQLAlchemyError

Apply this diff:

+from sqlalchemy.exc import SQLAlchemyError
+
-async def pause_items(request: Request, ids: str = None, db: Session = Depends(get_db)):
+async def pause_items(request: Request, ids: str = None):
+    db = next(get_db())
     """Pause media items from being processed"""
     item_ids = handle_ids(ids)
     # ... existing code ...
     except SQLAlchemyError as e:
         db.rollback()
         logger.error(f"Failed to pause items: {str(e)}")
-        raise HTTPException(status_code=500, detail="Failed to pause items")
+        raise HTTPException(status_code=500, detail="Failed to pause items") from e

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.0)

450-450: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


465-465: Undefined name SQLAlchemyError

(F821)


468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Comment on lines 518 to 559
async def get_all_paused(
request: Request,
type: Optional[str] = None,
db: Session = Depends(get_db)
):
"""Get all paused media items, optionally filtered by type"""
# Use raw SQL to verify data
query = select(
MediaItem.id,
MediaItem.title,
MediaItem.type,
MediaItem.is_paused,
MediaItem.paused_at
).where(MediaItem.is_paused.is_(True))

if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
query = query.where(MediaItem.type == type)

logger.debug(f"Executing query: {str(query)}")
result = db.execute(query)
rows = result.fetchall()
logger.debug(f"Raw SQL found {len(rows)} rows")

# Map results to response model
items = [
{
"id": row.id,
"title": row.title,
"type": row.type,
"paused_at": str(row.paused_at) if row.paused_at else None
}
for row in rows
]

return AllPausedResponse(count=len(items), items=items)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and improve logging security.

  1. Add error handling for database operations
  2. Avoid logging raw SQL queries as they might contain sensitive data

Apply this diff:

     if type:
         valid_types = [t.value for t in MediaType]
         if type not in valid_types:
             raise HTTPException(
                 status_code=400,
                 detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
             )
         query = query.where(MediaItem.type == type)
     
-    logger.debug(f"Executing query: {str(query)}")
+    logger.debug("Executing get_all_paused query")
-    result = db.execute(query)
-    rows = result.fetchall()
-    logger.debug(f"Raw SQL found {len(rows)} rows")
+    try:
+        result = db.execute(query)
+        rows = result.fetchall()
+        logger.debug(f"Found {len(rows)} paused items")
+    except SQLAlchemyError as e:
+        logger.error(f"Failed to fetch paused items: {str(e)}")
+        raise HTTPException(status_code=500, detail="Failed to fetch paused items") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.get("/paused", response_model=AllPausedResponse, operation_id="get_all_paused", responses={200: {"description": "Success, even if no items found"}}, response_model_exclude_none=True)
async def get_all_paused(
request: Request,
type: Optional[str] = None,
db: Session = Depends(get_db)
):
"""Get all paused media items, optionally filtered by type"""
# Use raw SQL to verify data
query = select(
MediaItem.id,
MediaItem.title,
MediaItem.type,
MediaItem.is_paused,
MediaItem.paused_at
).where(MediaItem.is_paused.is_(True))
if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
query = query.where(MediaItem.type == type)
logger.debug(f"Executing query: {str(query)}")
result = db.execute(query)
rows = result.fetchall()
logger.debug(f"Raw SQL found {len(rows)} rows")
# Map results to response model
items = [
{
"id": row.id,
"title": row.title,
"type": row.type,
"paused_at": str(row.paused_at) if row.paused_at else None
}
for row in rows
]
return AllPausedResponse(count=len(items), items=items)
@router.get("/paused", response_model=AllPausedResponse, operation_id="get_all_paused", responses={200: {"description": "Success, even if no items found"}}, response_model_exclude_none=True)
async def get_all_paused(
request: Request,
type: Optional[str] = None,
db: Session = Depends(get_db)
):
"""Get all paused media items, optionally filtered by type"""
# Use raw SQL to verify data
query = select(
MediaItem.id,
MediaItem.title,
MediaItem.type,
MediaItem.is_paused,
MediaItem.paused_at
).where(MediaItem.is_paused.is_(True))
if type:
valid_types = [t.value for t in MediaType]
if type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"Invalid type. Must be one of: {', '.join(valid_types)}"
)
query = query.where(MediaItem.type == type)
logger.debug("Executing get_all_paused query")
try:
result = db.execute(query)
rows = result.fetchall()
logger.debug(f"Found {len(rows)} paused items")
except SQLAlchemyError as e:
logger.error(f"Failed to fetch paused items: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to fetch paused items") from e
# Map results to response model
items = [
{
"id": row.id,
"title": row.title,
"type": row.type,
"paused_at": str(row.paused_at) if row.paused_at else None
}
for row in rows
]
return AllPausedResponse(count=len(items), items=items)
🧰 Tools
🪛 Ruff (0.8.0)

522-522: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

src/program/media/item.py Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (8)
src/routers/secure/items.py (6)

72-76: Handle empty strings in comma-separated list

The function should filter out empty strings when splitting comma-separated IDs.

 def handle_ids(ids: Optional[str]) -> list[str]:
     """Handle comma-separated IDs or single ID"""
     if not ids:
         raise HTTPException(status_code=400, detail="No item ID provided")
-    return [str(id) for id in ids.split(",")] if "," in ids else [str(ids)]
+    if "," in ids:
+        id_list = [str(id.strip()) for id in ids.split(",") if id.strip()]
+        if not id_list:
+            raise HTTPException(status_code=400, detail="No valid IDs provided")
+        return id_list
+    return [str(ids)]

11-12: Remove unused imports

The following imports are not used in the code:

  • NoResultFound from sqlalchemy.exc
  • with_polymorphic from sqlalchemy.orm
🧰 Tools
🪛 Ruff (0.8.0)

11-11: sqlalchemy.exc.NoResultFound imported but unused

Remove unused import: sqlalchemy.exc.NoResultFound

(F401)


12-12: sqlalchemy.orm.with_polymorphic imported but unused

Remove unused import: sqlalchemy.orm.with_polymorphic

(F401)


269-271: Use exception chaining in error handling

When re-raising exceptions, use from e to preserve the original traceback.

     except SQLAlchemyError as e:
         logger.error(f"Database error: {e}")
-        raise HTTPException(status_code=500, detail="Database error")
+        raise HTTPException(status_code=500, detail="Database error") from e
🧰 Tools
🪛 Ruff (0.8.0)

271-271: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


455-455: Use Pythonic boolean comparisons

Avoid explicit comparisons with True.

-            .where(MediaItem.is_paused == True)
+            .where(MediaItem.is_paused.is_(True))

Also applies to: 508-508

🧰 Tools
🪛 Ruff (0.8.0)

455-455: Avoid equality comparisons to True; use if MediaItem.is_paused: for truth checks

Replace with MediaItem.is_paused

(E712)


488-491: Use exception chaining consistently

Add exception chaining to all error handlers to preserve the original traceback.

Example fix:

     except SQLAlchemyError as e:
         logger.error(f"Database error: {e}")
         raise HTTPException(
             status_code=500,
-            detail=f"Database error occurred: {str(e)}"
+            detail=f"Database error occurred: {str(e)}"
+        ) from e

Apply this pattern to all similar error handling blocks.

Also applies to: 522-525, 549-552, 601-604, 651-654, 690-693

🧰 Tools
🪛 Ruff (0.8.0)

488-491: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


554-654: Consider bulk database operations

The pause/unpause operations could be optimized using bulk updates instead of iterating through items.

Consider using SQLAlchemy's bulk update operations:

from sqlalchemy import update

# Example bulk update
stmt = (
    update(MediaItem)
    .where(MediaItem.id.in_(item_ids))
    .values(
        is_paused=True,
        paused_at=datetime.utcnow(),
        paused_by=request.state.user.username if hasattr(request.state, 'user') else None
    )
)
session.execute(stmt)
session.commit()
🧰 Tools
🪛 Ruff (0.8.0)

560-560: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


601-604: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


612-612: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


651-654: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

src/program/media/item.py (2)

21-26: Enhance enum documentation.

Consider adding more detailed documentation to describe the purpose and usage of each enum value.

 class MediaType(str, Enum):
-    """Media type enum"""
+    """Enum representing the different types of media items.
+    
+    Values:
+        MOVIE: Represents a single movie
+        SHOW: Represents a TV show containing seasons
+        SEASON: Represents a season within a show
+        EPISODE: Represents an episode within a season
+    """

431-452: Improve unpause method implementation.

Add type hints to docstring and remove extra blank lines for consistency.

     def unpause(self) -> None:
-        """Resume processing of this media item"""
+        """Resume processing of this media item.
+        
+        Note: The paused_at field is preserved for history tracking.
+        """
-
         if not self.is_paused:
             logger.debug(f"{self.log_string} is not paused")
             return
-
         logger.debug(f"Unpausing {self.id}")
         try:
             self.is_paused = False
             self.unpaused_at = datetime.now()
-            # Keep paused_at for history
-            
             session = object_session(self)
             if session:
                 session.flush()
-                
             logger.info(f"{self.log_string} unpaused, is_paused={self.is_paused}, unpaused_at={self.unpaused_at}")
         except Exception as e:
             logger.error(f"Failed to unpause {self.log_string}: {str(e)}")
             raise
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f395b7f and 692a4e8.

📒 Files selected for processing (3)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py (1 hunks)
  • src/program/media/item.py (6 hunks)
  • src/routers/secure/items.py (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/alembic/versions/20241114_1400_c99239e3445f_add_pause_functionality.py
🧰 Additional context used
🪛 Ruff (0.8.0)
src/routers/secure/items.py

11-11: sqlalchemy.exc.NoResultFound imported but unused

Remove unused import: sqlalchemy.exc.NoResultFound

(F401)


12-12: sqlalchemy.orm.with_polymorphic imported but unused

Remove unused import: sqlalchemy.orm.with_polymorphic

(F401)


254-254: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


271-271: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


443-443: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


455-455: Avoid equality comparisons to True; use if MediaItem.is_paused: for truth checks

Replace with MediaItem.is_paused

(E712)


488-491: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


496-496: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


508-508: Avoid equality comparisons to True; use if MediaItem.is_paused: for truth checks

Replace with MediaItem.is_paused

(E712)


522-525: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


530-530: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


549-552: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


560-560: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


601-604: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


612-612: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


651-654: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


666-666: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


690-693: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (2)
src/routers/secure/items.py (1)

21-64: Well-structured response models!

The response models are well-designed with:

  • Clear separation of concerns
  • Proper type hints
  • Descriptive docstrings
src/program/media/item.py (1)

257-259: Add missing paused_by field to dictionary representation.

The paused_by field is defined in the model but not included in the to_dict() method.

src/routers/secure/items.py Show resolved Hide resolved
Comment on lines +468 to +478
logger.debug(f"Query parameters: {query.compile().params}")

result = session.execute(query).scalars().unique().all()
logger.debug(f"Found {len(result)} paused items")

# Add debug logging for each item
for item in result:
logger.debug(f"Paused item: id={item.id}, type={item.type}, is_paused={item.is_paused}, class={item.__class__.__name__}")
logger.debug(f"Item dict before conversion: {vars(item)}")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reduce debug logging to prevent sensitive data exposure

The current debug logging includes raw SQL queries and object dumps which might expose sensitive data.

-    logger.debug(f"Executing query: {query}")
-    logger.debug(f"Query parameters: {query.compile().params}")
+    logger.debug("Executing get_all_paused query")
     
     result = session.execute(query).scalars().unique().all()
     logger.debug(f"Found {len(result)} paused items")
     
-    # Add debug logging for each item
-    for item in result:
-        logger.debug(f"Paused item: id={item.id}, type={item.type}, is_paused={item.is_paused}, class={item.__class__.__name__}")
-        logger.debug(f"Item dict before conversion: {vars(item)}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.debug(f"Executing query: {query}")
logger.debug(f"Query parameters: {query.compile().params}")
result = session.execute(query).scalars().unique().all()
logger.debug(f"Found {len(result)} paused items")
# Add debug logging for each item
for item in result:
logger.debug(f"Paused item: id={item.id}, type={item.type}, is_paused={item.is_paused}, class={item.__class__.__name__}")
logger.debug(f"Item dict before conversion: {vars(item)}")
logger.debug("Executing get_all_paused query")
result = session.execute(query).scalars().unique().all()
logger.debug(f"Found {len(result)} paused items")

Comment on lines +70 to +74
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False, nullable=True, index=True)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
unpaused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove duplicate index definition.

The is_paused column has an index defined both at the column level (index=True) and in __table_args__. This is redundant and could cause confusion. Remove the column-level index since the table-level index provides more control and visibility.

-    is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False, nullable=True, index=True)
+    is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False, nullable=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False, nullable=True, index=True)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
unpaused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False, nullable=True)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
unpaused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gaisberg, glad to hear that you've made the change! Let me know if you have any other questions.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

src/program/media/item.py Outdated Show resolved Hide resolved
Comment on lines +70 to +74
# Pause related fields
is_paused: Mapped[Optional[bool]] = mapped_column(sqlalchemy.Boolean, default=False, nullable=True, index=True)
paused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
unpaused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

logger.debug(f"{self.log_string} is already paused")
return

logger.debug(f"Pausing {self.id}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add a more identifyin log string here

@@ -17,45 +17,62 @@ def process_event(emitted_by: Service, existing_item: MediaItem | None = None, c
no_further_processing: ProcessedEvent = (None, [])
items_to_submit = []

#TODO - Reindex non-released badly indexed items here
# Skip processing if item is paused
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you misunderstood. Restore the original state_transition.py and just add the 3 line addition.

message: str
ids: list[str]

return [item.to_dict() for item in items]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was extended before

query = (
select(MediaItem)
.outerjoin(Movie, Movie.id == MediaItem.id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are all these outerjoins needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants