diff --git a/FAQ.md b/FAQ.md index c26c8c4e..5ba877b9 100644 --- a/FAQ.md +++ b/FAQ.md @@ -47,6 +47,7 @@ or the `environment:` section in `compose.yaml` file. | YTP_ALLOW_INTERNAL_URLS | Allow requests to internal URLs | `false` | | YTP_SIMPLE_MODE | Switch default interface to Simple mode. | `false` | | YTP_STATIC_UI_PATH | Path to custom static UI files. | `(not_set)` | +| YTP_AUTO_CLEAR_HISTORY_DAYS | Number of days after which completed download history is cleared. | `0` | > [!NOTE] > To raise the maximum workers for specific extractor, you need to add a ENV variable that follows the pattern `YTP_MAX_WORKERS_FOR_`. @@ -54,8 +55,12 @@ or the `environment:` section in `compose.yaml` file. > The limit should not exceed the `YTP_MAX_WORKERS` value as it will be ignored. > [!IMPORTANT] -> The env variable `YTP_SIMPLE_MODE` only control what being displayed for first time visitor, the users can still switch between the two modes via the WebUI settings page. +> The env variable `YTP_SIMPLE_MODE` only control what being displayed for first time visitor, the users can still switch between the two modes via the WebUI settings page. +## Notes about YTP_AUTO_CLEAR_HISTORY_DAYS + +- `0` days means no automatic clearing of the download history. lowest value that will trigger the clearing is `1` day. +- This setting will **NOT** delete the downloaded files, it will only clear the history from the database. # Browser extensions & bookmarklets diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index b5c38786..68b02946 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -140,6 +140,15 @@ class DownloadQueue(metaclass=Singleton): func=self._check_live, id=f"{__class__.__name__}.{__class__._check_live.__name__}", ) + + if self.config.auto_clear_history_days > 0: + Scheduler.get_instance().add( + # timer="8 */1 * * *", + timer="* * * * *", + func=self._delete_old_history, + id=f"{__class__.__name__}.{__class__._delete_old_history.__name__}", + ) + # app.on_shutdown.append(self.on_shutdown) async def test(self) -> bool: @@ -1262,6 +1271,48 @@ class DownloadQueue(metaclass=Singleton): LOG.exception(e) LOG.error(f"Failed to retry item '{item_ref}'. {e!s}") + async def _delete_old_history(self) -> None: + """ + Automatically delete old download history based on user specified days. + 0 or negative value disables this feature. + """ + if self.config.auto_clear_history_days < 0 or self.is_paused() or self.done.empty(): + return + + cutoff_date: datetime = datetime.now(UTC) - timedelta(days=self.config.auto_clear_history_days) + items_to_delete: list[tuple[str, ItemDTO]] = [] + + for key, download in self.done.items(): + info: ItemDTO = download.info + if not info or not info.timestamp: + continue + + if "finished" != info.status or not info.filename: + continue + + try: + timestamp_seconds: float = info.timestamp / 1e9 + item_datetime: datetime = datetime.fromtimestamp(timestamp_seconds, tz=UTC) + if item_datetime < cutoff_date: + items_to_delete.append((key, info)) + except (OSError, ValueError, OverflowError) as e: + LOG.error(f"Failed to parse timestamp '{info.timestamp}' for item '{info.title}': {e}") + + titles: list[str] = [] + for key, info in items_to_delete: + item_name: str = info.title or info.id or info._id + self._notify.emit( + Events.ITEM_DELETED, + data=info, + title="Download Cleared", + message=f"'{item_name}' record removed from history.", + ) + titles.append(item_name) + self.done.delete(key) + + if titles: + LOG.info(f"Automatically cleared '{', '.join(titles)}' from download history due to age.") + def _handle_task_exception(self, task: asyncio.Task) -> None: if task.cancelled(): return diff --git a/app/library/Utils.py b/app/library/Utils.py index efd77d19..358c26a2 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -482,6 +482,9 @@ def check_id(file: Path) -> bool | str: id: str | None = match.groupdict().get("id") try: + if not file.parent.exists(): + return False + for f in file.parent.iterdir(): if id not in f.stem: continue diff --git a/app/library/config.py b/app/library/config.py index 8825ecaf..05abc8f2 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -192,6 +192,9 @@ class Config(metaclass=Singleton): playlist_items_concurrency: int = 4 """The number of concurrent playlist items to be processed at same time.""" + auto_clear_history_days: int = 0 + """Number of days after which completed download history is automatically cleared. 0 to disable.""" + pictures_backends: list[str] = [ "https://unsplash.it/1920/1080?random", "https://picsum.photos/1920/1080", @@ -230,6 +233,7 @@ class Config(metaclass=Singleton): "playlist_items_concurrency", "download_path_depth", "download_info_expires", + "auto_clear_history_days", ) "The variables that are integers." diff --git a/ui/app/layouts/default.vue b/ui/app/layouts/default.vue index 8413251f..7242880e 100644 --- a/ui/app/layouts/default.vue +++ b/ui/app/layouts/default.vue @@ -103,7 +103,7 @@