-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
569 additions
and
161 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
*.mp3 | ||
*.m4a | ||
*.yaml | ||
*.db | ||
!.github/**/*.yaml | ||
|
||
archive/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,100 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, Any | ||
|
||
from rich import progress | ||
from rich.console import Console | ||
|
||
if TYPE_CHECKING: | ||
from podcast_archiver.config import Settings | ||
from podcast_archiver.models import Episode | ||
from podcast_archiver.types import ProgressCallback | ||
|
||
console = Console() | ||
|
||
|
||
PROGRESS_COLUMNS = ( | ||
progress.SpinnerColumn(finished_text="[bar.finished]✔[/]"), | ||
progress.TextColumn("[blue]{task.fields[date]:%Y-%m-%d}"), | ||
progress.TextColumn("[progress.description]{task.description}"), | ||
progress.BarColumn(bar_width=25), | ||
progress.TaskProgressColumn(), | ||
progress.TimeRemainingColumn(), | ||
progress.DownloadColumn(), | ||
progress.TransferSpeedColumn(), | ||
) | ||
|
||
|
||
def noop_callback(total: int | None = None, completed: int | None = None) -> None: | ||
pass | ||
|
||
|
||
class ProgressDisplay: | ||
disabled: bool | ||
|
||
_progress: progress.Progress | ||
_state: dict[Episode, progress.TaskID] | ||
|
||
def __init__(self, settings: Settings) -> None: | ||
self.disabled = settings.verbose > 1 or settings.quiet | ||
self._progress = progress.Progress( | ||
*PROGRESS_COLUMNS, | ||
console=console, | ||
disable=self.disabled, | ||
) | ||
self._progress.live.vertical_overflow = "visible" | ||
self._state = {} | ||
|
||
def _get_task_id(self, episode: Episode) -> progress.TaskID: | ||
return self._state.get(episode, self.register(episode)) | ||
|
||
def __enter__(self) -> ProgressDisplay: | ||
if not self.disabled: | ||
self._progress.start() | ||
return self | ||
|
||
def __exit__(self, *args: Any) -> None: | ||
if not self.disabled: | ||
self._progress.stop() | ||
self._state = {} | ||
|
||
def shutdown(self) -> None: | ||
for task in self._progress.tasks or []: | ||
if not task.finished: | ||
task.visible = False | ||
self._progress.stop() | ||
|
||
def register(self, episode: Episode) -> progress.TaskID: | ||
task_id = self._progress.add_task( | ||
description=episode.title, | ||
date=episode.published_time, | ||
total=episode.enclosure.length, | ||
visible=False, | ||
) | ||
self._state[episode] = task_id | ||
return task_id | ||
|
||
def update(self, episode: Episode, visible: bool = True, **kwargs: Any) -> None: | ||
if self.disabled: | ||
return | ||
|
||
task_id = self._get_task_id(episode) | ||
self._progress.update(task_id, visible=visible, **kwargs) | ||
|
||
def completed(self, episode: Episode, visible: bool = True, **kwargs: Any) -> None: | ||
if self.disabled: | ||
return | ||
|
||
task_id = self._get_task_id(episode) | ||
self._progress.update(task_id, visible=visible, completed=episode.enclosure.length, **kwargs) | ||
|
||
def get_callback(self, episode: Episode) -> ProgressCallback: | ||
if self.disabled: | ||
return noop_callback | ||
|
||
task_id = self._get_task_id(episode) | ||
|
||
def _callback(total: int | None = None, completed: int | None = None) -> None: | ||
self._progress.update(task_id, total=total, completed=completed, visible=True) | ||
|
||
return _callback |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
from __future__ import annotations | ||
|
||
import sqlite3 | ||
from abc import abstractmethod | ||
from contextlib import contextmanager | ||
from threading import Lock | ||
from typing import TYPE_CHECKING, Iterator | ||
|
||
from podcast_archiver.logging import logger | ||
|
||
if TYPE_CHECKING: | ||
from podcast_archiver.models import Episode | ||
|
||
|
||
class BaseDatabase: | ||
@abstractmethod | ||
def add(self, episode: Episode) -> None: | ||
pass # pragma: no cover | ||
|
||
@abstractmethod | ||
def exists(self, episode: Episode) -> bool: | ||
pass # pragma: no cover | ||
|
||
|
||
class DummyDatabase(BaseDatabase): | ||
def add(self, episode: Episode) -> None: | ||
pass | ||
|
||
def exists(self, episode: Episode) -> bool: | ||
return False | ||
|
||
|
||
class Database(BaseDatabase): | ||
filename: str | ||
ignore_existing: bool | ||
|
||
lock = Lock() | ||
|
||
def __init__(self, filename: str, ignore_existing: bool) -> None: | ||
self.filename = filename | ||
self.ignore_existing = ignore_existing | ||
self.migrate() | ||
|
||
@contextmanager | ||
def get_conn(self) -> Iterator[sqlite3.Connection]: | ||
with self.lock, sqlite3.connect(self.filename) as conn: | ||
yield conn | ||
|
||
def migrate(self) -> None: | ||
logger.debug(f"Migrating database at {self.filename}") | ||
with self.get_conn() as conn: | ||
conn.execute( | ||
"""\ | ||
CREATE TABLE IF NOT EXISTS episodes( | ||
guid TEXT UNIQUE NOT NULL, | ||
title TEXT | ||
)""" | ||
) | ||
|
||
def add(self, episode: Episode) -> None: | ||
with self.get_conn() as conn: | ||
try: | ||
conn.execute( | ||
"INSERT INTO episodes(guid, title) VALUES (?, ?)", | ||
(episode.guid, episode.title), | ||
) | ||
except sqlite3.IntegrityError: | ||
logger.debug(f"Episode exists: {episode}") | ||
|
||
def exists(self, episode: Episode) -> bool: | ||
if self.ignore_existing: | ||
return False | ||
with self.get_conn() as conn: | ||
result = conn.execute( | ||
"SELECT EXISTS(SELECT 1 FROM episodes WHERE guid = ?)", | ||
(episode.guid,), | ||
) | ||
return bool(result.fetchone()[0]) |
Oops, something went wrong.