diff --git a/app/internal/sync.py b/app/internal/sync.py index 931044f4..39d8474c 100755 --- a/app/internal/sync.py +++ b/app/internal/sync.py @@ -8,8 +8,11 @@ from app.database.query import get_all_templates, get_interface from app.dependencies import get_database, get_preferences -from app.internal.series import add_series +from app.internal.cards import delete_cards +from app.internal.series import add_series, delete_series +from app.models.card import Card from app.models.connection import Connection +from app.models.loaded import Loaded from app.models.series import Series from app.models.sync import Sync from app.schemas.series import NewSeries @@ -24,27 +27,57 @@ def sync_all(*, log: Logger = log) -> None: Schedule-able function to run all defined Syncs in the Database. """ + # Wait if there is a Sync currently running + attempts = 5 + while ((preferences := get_preferences()).currently_running_sync is not None + and attempts > 0): + log.debug('Sync is current running, waiting..') + sleep(60) + attempts -= 1 + try: - # Get the Database with next(get_database()) as db: - # Get and run all Syncs - for sync in db.query(Sync).all(): + # Exit if there are no Syncs + if not (syncs := db.query(Sync).all()): + return None + + # Run each Sync + for sync in syncs: try: run_sync(db, sync, log=log) - except HTTPException as e: - log.exception(f'{sync} Error Syncing - {e.detail}', e) + except HTTPException as exc: + log.exception(f'{sync} Error Syncing - {exc.detail}') except OperationalError: - log.debug(f'Database is busy, sleeping..') + log.debug('Database is busy, sleeping..') sleep(30) + + # Remove un-synced Series if toggled + if preferences.delete_unsynced_series: + # Delete all Series which do not have an associated Sync + to_delete = db.query(Series)\ + .filter(Series.sync_id.is_(None))\ + .all() + for series in to_delete: + # Delete Cards and Loaded objects + delete_cards( + db, + db.query(Card).filter_by(series_id=series.id), + db.query(Loaded).filter_by(series_id=series.id), + commit=False, + log=log, + ) + # Delete Series itself + delete_series(db, series, commit_changes=False, log=log) db.commit() - except Exception as e: - log.exception(f'Failed to run all Syncs', e) - get_preferences().currently_running_sync = None + except Exception: + log.exception('Failed to run all Syncs') + + preferences.currently_running_sync = None def add_sync( db: Session, - new_sync: Union[NewEmbySync, NewJellyfinSync, NewPlexSync,NewSonarrSync], + new_sync: Union[NewEmbySync, NewJellyfinSync,NewPlexSync,NewSonarrSync], *, log: Logger = log, ) -> Sync: @@ -118,6 +151,7 @@ def run_sync( # Process all Series returned by Sync added: list[Series] = [] + existing_series: set[Series] = set() for series_info, lib_or_dir in all_series: # Look for existing Series existing = db.query(Series)\ @@ -153,6 +187,10 @@ def run_sync( # If already exists in Database, update IDs and libraries then skip if existing: + existing_series.add(existing) + # Assign Sync ID if one does not already exist + existing.sync_id = existing.sync_id or sync.id + # Add any new libraries for new in libraries: exists = any( @@ -167,17 +205,25 @@ def run_sync( # Update IDs existing.update_from_series_info(series_info) db.commit() - continue - # Create NewSeries for this entry - added.append(NewSeries( - name=series_info.name, year=series_info.year, libraries=libraries, - **series_info.ids, sync_id=sync.id, template_ids=sync.template_ids, - )) + else: + added.append(NewSeries( + name=series_info.name, year=series_info.year, + libraries=libraries, **series_info.ids, + sync_id=sync.id, template_ids=sync.template_ids, + )) # Nothing added, log if not added: - log.debug(f'{sync} No new Series synced') + log.info(f'{sync} No new Series synced') + + # Clear the Sync ID of all Series which were not in the latest sync + if preferences.delete_unsynced_series: + for series in sync.series: + if series not in existing_series: + series.sync_id = None + log.debug(f'Series[{series.id}].sync_id = None') + db.commit() # Process each newly added Series preferences.currently_running_sync = None diff --git a/app/models/preferences.py b/app/models/preferences.py index 7f0d777f..5464b074 100755 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -76,6 +76,7 @@ class Preferences: 'imported_blueprints', 'colorblind_mode', 'library_unique_cards', 'invalid_connections', 'home_page_table_view', 'reduced_animations', 'currently_running_sync', 'interactive_card_previews', + 'home_page_order', 'delete_unsynced_series', ) @@ -186,6 +187,7 @@ def __initialize_defaults(self) -> None: self.completely_delete_series = False self.sync_specials = True self.delete_missing_episodes = True + self.delete_unsynced_series = False self.simplified_data_table = True self.interactive_card_previews = True self.remote_card_types = {} @@ -197,7 +199,7 @@ def __initialize_defaults(self) -> None: self.default_templates: list[int] = [] self.global_extras: dict[str, dict[str, str]] = {} - self.currently_running_sync = None + self.currently_running_sync: Optional[int] = None self.invalid_connections: list[int] = [] self.use_emby = False self.use_jellyfin = False diff --git a/app/schemas/preferences.py b/app/schemas/preferences.py index 26422563..cbc9151a 100755 --- a/app/schemas/preferences.py +++ b/app/schemas/preferences.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Literal, Optional -from pydantic import DirectoryPath, PositiveInt, conint, constr, validator # pylint: disable=no-name-in-module +from pydantic import DirectoryPath, PositiveInt, conint, constr, validator from app.schemas.base import ( Base, InterfaceType, ImageSource, UpdateBase, UNSPECIFIED @@ -79,6 +79,7 @@ class UpdatePreferences(UpdateBase): season_folder_format: str = UNSPECIFIED sync_specials: bool = UNSPECIFIED delete_missing_episodes: bool = UNSPECIFIED + delete_unsynced_series: bool = UNSPECIFIED simplified_data_table: bool = UNSPECIFIED default_card_type: CardTypeIdentifier = UNSPECIFIED excluded_card_types: list[CardTypeIdentifier] = UNSPECIFIED @@ -157,6 +158,7 @@ class Preferences(Base): season_folder_format: str sync_specials: bool delete_missing_episodes: bool + delete_unsynced_series: bool simplified_data_table: bool is_docker: bool default_card_type: CardTypeIdentifier diff --git a/app/templates/js/settings.js b/app/templates/js/settings.js index 0a9242eb..329b0ce9 100755 --- a/app/templates/js/settings.js +++ b/app/templates/js/settings.js @@ -171,11 +171,12 @@ function updateGlobalSettings() { card_directory: $('input[name="card_directory"]').val(), source_directory: $('input[name="source_directory"]').val(), completely_delete_series: $('input[name="completely_delete_series"]').is(':checked'), - // Episode Data + // Series and Episode Data episode_data_source: $('input[name="episode_data_source"]').val(), image_source_priority: $('input[name="image_source_priority"]').val().split(','), sync_specials: $('input[name="sync_specials"]').is(':checked'), delete_missing_episodes: $('input[name="delete_missing_episodes"]').is(':checked'), + delete_unsynced_series: $('input[name="delete_unsynced_series"]').is(':checked'), // Title Cards default_card_type: $('input[name="default_card_type"]').val(), excluded_card_types: parseListString($('input[name="excluded_card_types"]').val()), diff --git a/app/templates/settings.html b/app/templates/settings.html index c9fe8438..8c8ab06f 100755 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -84,8 +84,8 @@

- - Episode Data + + Series and Episode Data

@@ -143,6 +143,22 @@

Whether to delete Episodes no longer present in the assigned Episode Data Source.

+ +
+
+ + {% if preferences.delete_unsynced_series %} + + {% else %} + + {% endif %} +

+ Whether to delete Series which no longer appear in any Syncs. +
+ This will delete all Title Cards and manually added Series (if they are not in any Sync). +

+
+
@@ -189,7 +205,7 @@