mirror of
https://github.com/Garmelon/PFERD.git
synced 2023-12-21 10:23:01 +01:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
ecaedea709 | |||
f05d1b1261 | |||
6aaa3071f9 | |||
c26c9352f1 | |||
d9ea688145 | |||
e8be6e498e | |||
e4b1fac045 | |||
402ae81335 | |||
52f31e2783 | |||
739522a151 | |||
6c034209b6 | |||
f6fbd5e4bb | |||
7024db1f13 | |||
23bfa42a0d | |||
fdb57884ed |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
__pycache__/
|
||||
.venv/
|
||||
venv/
|
||||
.idea/
|
||||
build/
|
||||
.mypy_cache/
|
||||
.tmp/
|
||||
.env
|
||||
|
69
PFERD/download_summary.py
Normal file
69
PFERD/download_summary.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
Provides a summary that keeps track of new modified or deleted files.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
class DownloadSummary:
|
||||
"""
|
||||
Keeps track of all new, modified or deleted files and provides a summary.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._new_files: List[Path] = []
|
||||
self._modified_files: List[Path] = []
|
||||
self._deleted_files: List[Path] = []
|
||||
|
||||
@property
|
||||
def new_files(self) -> List[Path]:
|
||||
"""
|
||||
Returns all new files.
|
||||
"""
|
||||
return self._new_files.copy()
|
||||
|
||||
@property
|
||||
def modified_files(self) -> List[Path]:
|
||||
"""
|
||||
Returns all modified files.
|
||||
"""
|
||||
return self._modified_files.copy()
|
||||
|
||||
@property
|
||||
def deleted_files(self) -> List[Path]:
|
||||
"""
|
||||
Returns all deleted files.
|
||||
"""
|
||||
return self._deleted_files.copy()
|
||||
|
||||
def merge(self, summary: 'DownloadSummary') -> None:
|
||||
"""
|
||||
Merges ourselves with the passed summary. Modifies this object, but not the passed one.
|
||||
"""
|
||||
self._new_files += summary.new_files
|
||||
self._modified_files += summary.modified_files
|
||||
self._deleted_files += summary.deleted_files
|
||||
|
||||
def add_deleted_file(self, path: Path) -> None:
|
||||
"""
|
||||
Registers a file as deleted.
|
||||
"""
|
||||
self._deleted_files.append(path)
|
||||
|
||||
def add_modified_file(self, path: Path) -> None:
|
||||
"""
|
||||
Registers a file as changed.
|
||||
"""
|
||||
self._modified_files.append(path)
|
||||
|
||||
def add_new_file(self, path: Path) -> None:
|
||||
"""
|
||||
Registers a file as new.
|
||||
"""
|
||||
self._new_files.append(path)
|
||||
|
||||
def has_updates(self) -> bool:
|
||||
"""
|
||||
Returns whether this summary has any updates.
|
||||
"""
|
||||
return bool(self._new_files or self._modified_files or self._deleted_files)
|
@ -425,7 +425,8 @@ class IliasCrawler:
|
||||
results: List[IliasCrawlerEntry] = []
|
||||
|
||||
# We can download everything directly!
|
||||
if len(direct_download_links) == len(video_links):
|
||||
# FIXME: Sadly the download button is currently broken, so never do that
|
||||
if False and len(direct_download_links) == len(video_links):
|
||||
for link in direct_download_links:
|
||||
results += self._crawl_single_video(video_dir_path, link, True)
|
||||
else:
|
||||
|
@ -3,14 +3,18 @@ Contains a few logger utility functions and implementations.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from rich import print as rich_print
|
||||
from rich._log_render import LogRender
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
from rich.theme import Theme
|
||||
|
||||
from .download_summary import DownloadSummary
|
||||
from .utils import PathLike, to_path
|
||||
|
||||
STYLE = "{"
|
||||
@ -111,6 +115,15 @@ class PrettyLogger:
|
||||
f"[bold green]Created {self._format_path(path)}.[/bold green]"
|
||||
)
|
||||
|
||||
def deleted_file(self, path: PathLike) -> None:
|
||||
"""
|
||||
A file has been deleted.
|
||||
"""
|
||||
|
||||
self.logger.info(
|
||||
f"[bold red]Deleted {self._format_path(path)}.[/bold red]"
|
||||
)
|
||||
|
||||
def ignored_file(self, path: PathLike, reason: str) -> None:
|
||||
"""
|
||||
File was not downloaded or modified.
|
||||
@ -138,6 +151,23 @@ class PrettyLogger:
|
||||
f"([/dim]{reason}[dim]).[/dim]"
|
||||
)
|
||||
|
||||
def summary(self, download_summary: DownloadSummary) -> None:
|
||||
"""
|
||||
Prints a download summary.
|
||||
"""
|
||||
self.logger.info("")
|
||||
self.logger.info("[bold cyan]Download Summary[/bold cyan]")
|
||||
if not download_summary.has_updates():
|
||||
self.logger.info("[bold dim]Nothing changed![/bold dim]")
|
||||
return
|
||||
|
||||
for new_file in download_summary.new_files:
|
||||
self.new_file(new_file)
|
||||
for modified_file in download_summary.modified_files:
|
||||
self.modified_file(modified_file)
|
||||
for deleted_files in download_summary.deleted_files:
|
||||
self.deleted_file(deleted_files)
|
||||
|
||||
def starting_synchronizer(
|
||||
self,
|
||||
target_directory: PathLike,
|
||||
|
@ -9,6 +9,7 @@ import shutil
|
||||
from pathlib import Path, PurePath
|
||||
from typing import List, Set
|
||||
|
||||
from .download_summary import DownloadSummary
|
||||
from .location import Location
|
||||
from .logging import PrettyLogger
|
||||
from .utils import prompt_yes_no
|
||||
@ -32,6 +33,8 @@ class Organizer(Location):
|
||||
# Keep the root dir
|
||||
self._known_files.add(path.resolve())
|
||||
|
||||
self.download_summary = DownloadSummary()
|
||||
|
||||
def accept_file(self, src: Path, dst: PurePath) -> None:
|
||||
"""Move a file to this organizer and mark it."""
|
||||
src_absolute = src.resolve()
|
||||
@ -65,10 +68,14 @@ class Organizer(Location):
|
||||
# Bail out, nothing more to do
|
||||
PRETTY.ignored_file(dst_absolute, "same file contents")
|
||||
self.mark(dst)
|
||||
# Touch it to update the timestamp
|
||||
dst_absolute.touch()
|
||||
return
|
||||
|
||||
self.download_summary.add_modified_file(dst_absolute)
|
||||
PRETTY.modified_file(dst_absolute)
|
||||
else:
|
||||
self.download_summary.add_new_file(dst_absolute)
|
||||
PRETTY.new_file(dst_absolute)
|
||||
|
||||
# Create parent dir if needed
|
||||
@ -115,9 +122,9 @@ class Organizer(Location):
|
||||
if start_dir.resolve() not in self._known_files and dir_empty:
|
||||
start_dir.rmdir()
|
||||
|
||||
@staticmethod
|
||||
def _delete_file_if_confirmed(path: Path) -> None:
|
||||
def _delete_file_if_confirmed(self, path: Path) -> None:
|
||||
prompt = f"Do you want to delete {path}"
|
||||
|
||||
if prompt_yes_no(prompt, False):
|
||||
self.download_summary.add_deleted_file(path)
|
||||
path.unlink()
|
||||
|
@ -9,6 +9,7 @@ from typing import Callable, List, Optional, Union
|
||||
from .cookie_jar import CookieJar
|
||||
from .diva import (DivaDownloader, DivaDownloadStrategy, DivaPlaylistCrawler,
|
||||
diva_download_new)
|
||||
from .download_summary import DownloadSummary
|
||||
from .errors import FatalException, swallow_and_print_errors
|
||||
from .ilias import (IliasAuthenticator, IliasCrawler, IliasDirectoryFilter,
|
||||
IliasDownloader, IliasDownloadInfo, IliasDownloadStrategy,
|
||||
@ -42,6 +43,7 @@ class Pferd(Location):
|
||||
):
|
||||
super().__init__(Path(base_dir))
|
||||
|
||||
self._download_summary = DownloadSummary()
|
||||
self._tmp_dir = TmpDir(self.resolve(tmp_dir))
|
||||
self._test_run = test_run
|
||||
|
||||
@ -139,7 +141,8 @@ class Pferd(Location):
|
||||
# This authenticator only works with the KIT ilias instance.
|
||||
authenticator = KitShibbolethAuthenticator(username=username, password=password)
|
||||
PRETTY.starting_synchronizer(target, "ILIAS", course_id)
|
||||
return self._ilias(
|
||||
|
||||
organizer = self._ilias(
|
||||
target=target,
|
||||
base_url="https://ilias.studium.kit.edu/",
|
||||
crawl_function=lambda crawler: crawler.crawl_course(course_id),
|
||||
@ -151,6 +154,16 @@ class Pferd(Location):
|
||||
clean=clean,
|
||||
)
|
||||
|
||||
self._download_summary.merge(organizer.download_summary)
|
||||
|
||||
return organizer
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""
|
||||
Prints the accumulated download summary.
|
||||
"""
|
||||
PRETTY.summary(self._download_summary)
|
||||
|
||||
@swallow_and_print_errors
|
||||
def ilias_kit_personal_desktop(
|
||||
self,
|
||||
|
@ -7,8 +7,7 @@ from types import TracebackType
|
||||
from typing import Optional, Type
|
||||
|
||||
import requests
|
||||
from rich.console import Console, ConsoleOptions, Control, RenderResult
|
||||
from rich.live_render import LiveRender
|
||||
from rich.console import Console
|
||||
from rich.progress import (BarColumn, DownloadColumn, Progress, TaskID,
|
||||
TextColumn, TimeRemainingColumn,
|
||||
TransferSpeedColumn)
|
||||
@ -23,7 +22,8 @@ _progress: Progress = Progress(
|
||||
TransferSpeedColumn(),
|
||||
"•",
|
||||
TimeRemainingColumn(),
|
||||
console=Console(file=sys.stdout)
|
||||
console=Console(file=sys.stdout),
|
||||
transient=True
|
||||
)
|
||||
|
||||
|
||||
@ -61,18 +61,6 @@ def progress_for(settings: Optional[ProgressSettings]) -> 'ProgressContextManage
|
||||
return ProgressContextManager(settings)
|
||||
|
||||
|
||||
class _OneLineUp(LiveRender):
|
||||
"""
|
||||
Render a control code for moving one line upwards.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("not rendered")
|
||||
|
||||
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
||||
yield Control(f"\r\x1b[1A")
|
||||
|
||||
|
||||
class ProgressContextManager:
|
||||
"""
|
||||
A context manager used for displaying progress.
|
||||
@ -113,9 +101,6 @@ class ProgressContextManager:
|
||||
_progress.stop()
|
||||
_progress.refresh()
|
||||
|
||||
# And we existed, so remove the line above (remove_task leaves one behind)
|
||||
Console().print(_OneLineUp())
|
||||
|
||||
return None
|
||||
|
||||
def advance(self, amount: float) -> None:
|
||||
|
14
README.md
14
README.md
@ -9,11 +9,19 @@ Ensure that you have at least Python 3.8 installed.
|
||||
To install PFERD or update your installation to the latest version, run this
|
||||
wherever you want to install/have installed PFERD:
|
||||
```
|
||||
$ pip install git+https://github.com/Garmelon/PFERD@v2.0.0
|
||||
$ pip install git+https://github.com/Garmelon/PFERD@v2.1.2
|
||||
```
|
||||
|
||||
The use of [venv](https://docs.python.org/3/library/venv.html) is recommended.
|
||||
|
||||
### Upgrading from 2.0.0 to 2.1.0+
|
||||
|
||||
The `IliasDirectoryType` type was renamed to `IliasElementType` and is now far
|
||||
more detailed.
|
||||
The new values are: REGULAR_FOLDER, VIDEO_FOLDER,
|
||||
EXERCISE_FOLDER, REGULAR_FILE, VIDEO_FILE, FORUM, EXTERNAL_LINK.
|
||||
Forums and external links are skipped automatically if you use the `kit_ilias` helper.
|
||||
|
||||
## Example setup
|
||||
|
||||
In this example, `python3` refers to at least Python 3.8.
|
||||
@ -29,8 +37,8 @@ $ mkdir Vorlesungen
|
||||
$ cd Vorlesungen
|
||||
$ python3 -m venv .venv
|
||||
$ .venv/bin/activate
|
||||
$ pip install git+https://github.com/Garmelon/PFERD@v2.0.0
|
||||
$ curl -O https://raw.githubusercontent.com/Garmelon/PFERD/v2.0.0/example_config.py
|
||||
$ pip install git+https://github.com/Garmelon/PFERD@v2.1.2
|
||||
$ curl -O https://raw.githubusercontent.com/Garmelon/PFERD/v2.1.2/example_config.py
|
||||
$ python3 example_config.py
|
||||
$ deactivate
|
||||
```
|
||||
|
@ -124,6 +124,8 @@ def main() -> None:
|
||||
cookies="ilias_cookies.txt",
|
||||
)
|
||||
|
||||
# Prints a summary listing all new, modified or deleted files
|
||||
pferd.print_summary()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -30,6 +30,9 @@ def main() -> None:
|
||||
cookies="ilias_cookies.txt",
|
||||
)
|
||||
|
||||
# Prints a summary listing all new, modified or deleted files
|
||||
pferd.print_summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Reference in New Issue
Block a user