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
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""add_pause_functionality

Revision ID: c99239e3445f
revision: str = 'c99239e3445f'
Revises: c99709e3648f
Create Date: 2024-11-14 16:00:00.000000

"""
from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = '[generate a new revision ID]'
down_revision: Union[str, None] = 'c99709e3648f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# Add pause-related columns to MediaItem table
op.add_column('MediaItem',
sa.Column('is_paused', sa.Boolean(), nullable=True, default=False))
op.add_column('MediaItem',
sa.Column('paused_at', sa.DateTime(), nullable=True))
op.add_column('MediaItem',
sa.Column('unpaused_at', sa.DateTime(), nullable=True))
op.add_column('MediaItem',
sa.Column('paused_by', sa.String(), nullable=True))
Comment on lines +29 to +30
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))


# Add index for is_paused column
op.create_index(op.f('ix_mediaitem_is_paused'), 'MediaItem', ['is_paused'])


def downgrade() -> None:
# Remove pause-related columns from MediaItem table
op.drop_index(op.f('ix_mediaitem_is_paused'), table_name='MediaItem')
op.drop_column('MediaItem', 'paused_by')
op.drop_column('MediaItem', 'unpaused_at')
op.drop_column('MediaItem', 'paused_at')
op.drop_column('MediaItem', 'is_paused')
55 changes: 54 additions & 1 deletion src/program/media/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class MediaItem(db.Model):
last_state: Mapped[Optional[States]] = mapped_column(sqlalchemy.Enum(States), default=States.Unknown)
subtitles: Mapped[list[Subtitle]] = relationship(Subtitle, back_populates="parent", lazy="selectin", cascade="all, delete-orphan")

# 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)
Gaisberg marked this conversation as resolved.
Show resolved Hide resolved
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)

unpaused_at: Mapped[Optional[datetime]] = mapped_column(sqlalchemy.DateTime, nullable=True)
paused_by: Mapped[Optional[str]] = mapped_column(sqlalchemy.String, nullable=True)

__mapper_args__ = {
"polymorphic_identity": "mediaitem",
"polymorphic_on":"type",
Expand All @@ -70,6 +76,7 @@ class MediaItem(db.Model):
__table_args__ = (
Index("ix_mediaitem_type", "type"),
Index("ix_mediaitem_requested_by", "requested_by"),
Index("ix_mediaitem_is_paused", "is_paused"),
Index("ix_mediaitem_title", "title"),
Index("ix_mediaitem_imdb_id", "imdb_id"),
Index("ix_mediaitem_tvdb_id", "tvdb_id"),
Expand Down Expand Up @@ -241,6 +248,9 @@ def to_dict(self):
"requested_by": self.requested_by,
"scraped_at": str(self.scraped_at),
"scraped_times": self.scraped_times,
"is_paused": self.is_paused,
"paused_at": str(self.paused_at) if self.paused_at else None,
"unpaused_at": str(self.unpaused_at) if self.unpaused_at else None,
Gaisberg marked this conversation as resolved.
Show resolved Hide resolved
}

def to_extended_dict(self, abbreviated_children=False, with_streams=True):
Expand Down Expand Up @@ -391,6 +401,49 @@ def _reset(self):
def log_string(self):
return self.title or self.id

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}")
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

try:
self.is_paused = True
self.paused_at = datetime.now()
self.unpaused_at = None

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

Gaisberg marked this conversation as resolved.
Show resolved Hide resolved
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.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

@property
def collection(self):
return self.parent.collection if self.parent else self.id
Expand Down Expand Up @@ -714,4 +767,4 @@ def copy_item(item):
elif isinstance(item, MediaItem):
return MediaItem(item={}).copy(item)
else:
raise ValueError(f"Cannot copy item of type {type(item)}")
raise ValueError(f"Cannot copy item of type {type(item)}")
69 changes: 6 additions & 63 deletions src/program/state_transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,67 +18,10 @@ 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
if content_item or (existing_item is not None and existing_item.last_state == States.Requested):
next_service = TraktIndexer
logger.debug(f"Submitting {content_item.log_string if content_item else existing_item.log_string} to trakt indexer")
return next_service, [content_item or existing_item]

elif existing_item is not None and existing_item.last_state in [States.PartiallyCompleted, States.Ongoing]:
if existing_item.type == "show":
for season in existing_item.seasons:
if season.last_state not in [States.Completed, States.Unreleased]:
_, sub_items = process_event(emitted_by, season, None)
items_to_submit += sub_items
elif existing_item.type == "season":
for episode in existing_item.episodes:
if episode.last_state != States.Completed:
_, sub_items = process_event(emitted_by, episode, None)
items_to_submit += sub_items

elif existing_item is not None and existing_item.last_state == States.Indexed:
next_service = Scraping
if emitted_by != Scraping and Scraping.should_submit(existing_item):
items_to_submit = [existing_item]
elif existing_item.type == "show":
items_to_submit = [s for s in existing_item.seasons if s.last_state != States.Completed and Scraping.should_submit(s)]
elif existing_item.type == "season":
items_to_submit = [e for e in existing_item.episodes if e.last_state != States.Completed and Scraping.should_submit(e)]

elif existing_item is not None and existing_item.last_state == States.Scraped:
next_service = Downloader
items_to_submit = [existing_item]

elif existing_item is not None and existing_item.last_state == States.Downloaded:
next_service = Symlinker
items_to_submit = [existing_item]

elif existing_item is not None and existing_item.last_state == States.Symlinked:
next_service = Updater
items_to_submit = [existing_item]

elif existing_item is not None and existing_item.last_state == States.Completed:
# If a user manually retries an item, lets not notify them again
if emitted_by not in ["RetryItem", PostProcessing]:
notify(existing_item)
# Avoid multiple post-processing runs
if emitted_by != PostProcessing:
if settings_manager.settings.post_processing.subliminal.enabled:
next_service = PostProcessing
if existing_item.type in ["movie", "episode"] and Subliminal.should_submit(existing_item):
items_to_submit = [existing_item]
elif existing_item.type == "show":
items_to_submit = [e for s in existing_item.seasons for e in s.episodes if e.last_state == States.Completed and Subliminal.should_submit(e)]
elif existing_item.type == "season":
items_to_submit = [e for e in existing_item.episodes if e.last_state == States.Completed and Subliminal.should_submit(e)]
if not items_to_submit:
return no_further_processing
else:

return no_further_processing

# if items_to_submit and next_service:
# for item in items_to_submit:
# logger.debug(f"Submitting {item.log_string} ({item.id}) to {next_service if isinstance(next_service, str) else next_service.__name__}")

# 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.

if existing_item and existing_item.is_paused:
logger.debug(f"Skipping {existing_item.log_string} - item is paused")
return no_further_processing

#not sure if i need to remove this
return next_service, items_to_submit
Gaisberg marked this conversation as resolved.
Show resolved Hide resolved
Loading