rcdl 2.2.2__py3-none-any.whl → 3.0.0b13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rcdl/__init__.py +5 -0
- rcdl/__main__.py +15 -3
- rcdl/core/__init__.py +0 -0
- rcdl/core/adapters.py +241 -0
- rcdl/core/api.py +31 -9
- rcdl/core/config.py +133 -14
- rcdl/core/db.py +239 -191
- rcdl/core/db_queries.py +75 -44
- rcdl/core/downloader.py +184 -142
- rcdl/core/downloader_subprocess.py +257 -85
- rcdl/core/file_io.py +13 -6
- rcdl/core/fuse.py +115 -106
- rcdl/core/models.py +83 -34
- rcdl/core/opti.py +90 -0
- rcdl/core/parser.py +80 -78
- rcdl/gui/__init__.py +0 -0
- rcdl/gui/__main__.py +5 -0
- rcdl/gui/db_viewer.py +41 -0
- rcdl/gui/gui.py +54 -0
- rcdl/gui/video_manager.py +170 -0
- rcdl/interface/__init__.py +0 -0
- rcdl/interface/cli.py +100 -20
- rcdl/interface/ui.py +105 -116
- rcdl/utils.py +163 -5
- {rcdl-2.2.2.dist-info → rcdl-3.0.0b13.dist-info}/METADATA +48 -15
- rcdl-3.0.0b13.dist-info/RECORD +28 -0
- rcdl/scripts/migrate_creators_json_txt.py +0 -37
- rcdl/scripts/migrate_old_format_to_db.py +0 -188
- rcdl/scripts/upload_pypi.py +0 -98
- rcdl-2.2.2.dist-info/RECORD +0 -22
- {rcdl-2.2.2.dist-info → rcdl-3.0.0b13.dist-info}/WHEEL +0 -0
- {rcdl-2.2.2.dist-info → rcdl-3.0.0b13.dist-info}/entry_points.txt +0 -0
rcdl/interface/cli.py
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# interface/cli.py
|
|
2
2
|
|
|
3
|
+
"""Hold all cli commands"""
|
|
4
|
+
|
|
3
5
|
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import inspect
|
|
4
9
|
|
|
5
10
|
import click
|
|
6
11
|
|
|
7
12
|
from rcdl.core import downloader as dl
|
|
13
|
+
from rcdl.interface.ui import UI, print_db_info
|
|
14
|
+
from rcdl.core.fuse import fuse_medias
|
|
15
|
+
from rcdl.core.opti import optimize
|
|
8
16
|
from rcdl.core.config import Config
|
|
9
17
|
from rcdl.core.parser import (
|
|
10
18
|
get_creators,
|
|
@@ -13,9 +21,7 @@ from rcdl.core.parser import (
|
|
|
13
21
|
append_creator,
|
|
14
22
|
)
|
|
15
23
|
from rcdl.core.db import DB
|
|
16
|
-
from .
|
|
17
|
-
from rcdl.core.fuse import fuse_videos
|
|
18
|
-
|
|
24
|
+
from rcdl.utils import clean_all
|
|
19
25
|
|
|
20
26
|
from rcdl import __version__
|
|
21
27
|
|
|
@@ -33,27 +39,56 @@ def refresh():
|
|
|
33
39
|
dl.refresh_creators_videos()
|
|
34
40
|
|
|
35
41
|
with DB() as db:
|
|
36
|
-
info = db.
|
|
37
|
-
|
|
38
|
-
UI.db_videos_status_table(info)
|
|
42
|
+
info = db.get_nb_per_status()
|
|
43
|
+
print_db_info(info)
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
@click.command(help="Download all videos from all creator")
|
|
42
|
-
|
|
47
|
+
@click.option(
|
|
48
|
+
"--max-fail-count",
|
|
49
|
+
type=int,
|
|
50
|
+
help="Set maximum number of failed attempts. Take precedence over config.toml",
|
|
51
|
+
)
|
|
52
|
+
def dlsf(max_fail_count: int | None):
|
|
43
53
|
"""Download video based on DB information
|
|
44
54
|
|
|
45
55
|
- read databse
|
|
46
56
|
- for each video NOT_DOWNLOADED or FAILED & fail_count < settings, dl video
|
|
47
57
|
"""
|
|
48
58
|
UI.info("Welcome to RCDL dlsf")
|
|
49
|
-
dl.download_videos_to_be_dl()
|
|
59
|
+
dl.download_videos_to_be_dl(max_fail_count)
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
@click.command("fuse", help="Fuse part video into one")
|
|
53
63
|
def fuse():
|
|
54
64
|
"""Fuse videos"""
|
|
55
|
-
UI.info("
|
|
56
|
-
|
|
65
|
+
UI.info("Fuse/Concat video together")
|
|
66
|
+
fuse_medias()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.command(help="Optimized video size")
|
|
70
|
+
def opti():
|
|
71
|
+
"""Optimized video size"""
|
|
72
|
+
UI.info("Optimized video")
|
|
73
|
+
optimize()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@click.command(help="Clean patial download file, cache, etc...")
|
|
77
|
+
@click.option("--all", is_flag=True, help="Act as if al lthe flags are true")
|
|
78
|
+
@click.option(
|
|
79
|
+
"--partial", is_flag=True, help="Remove partial file from download (.aria2, .part)"
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--cache", is_flag=True, help="remove cache from yt-dlp and kill aria2 process"
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--medias-deleted",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
help="Remove media marked for deletion (TO_BE_DELETED status)",
|
|
88
|
+
)
|
|
89
|
+
def clean(all: bool, partial: bool, cache: bool, medias_deleted: bool):
|
|
90
|
+
"""Remove partial download, clear subprocesses cache"""
|
|
91
|
+
clean_all(all, partial, cache, medias_deleted)
|
|
57
92
|
|
|
58
93
|
|
|
59
94
|
@click.command(help="Discover videos/creators with tags")
|
|
@@ -67,7 +102,7 @@ def discover(tag, max_page):
|
|
|
67
102
|
msg = f"[cdl] discover with tag={tag} max_page={max_page}"
|
|
68
103
|
click.echo(msg)
|
|
69
104
|
logging.info(msg)
|
|
70
|
-
|
|
105
|
+
UI.info("WIP - UNIMPLEMENTED")
|
|
71
106
|
|
|
72
107
|
|
|
73
108
|
@click.command("add", help="Add a creator")
|
|
@@ -85,8 +120,9 @@ def add_creator(creator_input):
|
|
|
85
120
|
|
|
86
121
|
|
|
87
122
|
@click.command("remove", help="Remove a creator")
|
|
123
|
+
@click.option("--db", is_flag=True)
|
|
88
124
|
@click.argument("creator_input")
|
|
89
|
-
def remove_creator(creator_input):
|
|
125
|
+
def remove_creator(db, creator_input):
|
|
90
126
|
"""Remove a creator (excat line) from creators.txt"""
|
|
91
127
|
_service, creator_id = parse_creator_input(str(creator_input))
|
|
92
128
|
|
|
@@ -94,7 +130,7 @@ def remove_creator(creator_input):
|
|
|
94
130
|
all_creators = []
|
|
95
131
|
matched_creator = None
|
|
96
132
|
for creator in creators:
|
|
97
|
-
if creator.
|
|
133
|
+
if creator.id == creator_id:
|
|
98
134
|
matched_creator = creator
|
|
99
135
|
continue
|
|
100
136
|
all_creators.append(creator)
|
|
@@ -102,35 +138,79 @@ def remove_creator(creator_input):
|
|
|
102
138
|
if matched_creator is None:
|
|
103
139
|
UI.error(f"Could not find creator from {creator_input}")
|
|
104
140
|
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
with open(Config.CREATORS_FILE, "w", encoding="utf-8"):
|
|
144
|
+
pass
|
|
145
|
+
except OSError as e:
|
|
146
|
+
UI.error(
|
|
147
|
+
f"Failed to create creators file at {Config.CREATORS_FILE} due to: {e}"
|
|
111
148
|
)
|
|
149
|
+
return
|
|
150
|
+
for c in all_creators:
|
|
151
|
+
append_creator(c)
|
|
152
|
+
UI.info(f"Removed creator {matched_creator.id}@({matched_creator.service})")
|
|
153
|
+
if db:
|
|
154
|
+
UI.info("Not yet implemented")
|
|
112
155
|
|
|
113
156
|
|
|
114
157
|
@click.command("list", help="List all creators")
|
|
115
158
|
def list_creators():
|
|
159
|
+
"""List all creators in creators.txt"""
|
|
116
160
|
creators = get_creators()
|
|
117
161
|
UI.table_creators(creators)
|
|
118
162
|
|
|
119
163
|
|
|
164
|
+
@click.command("status", help="Print db info")
|
|
165
|
+
def db_status():
|
|
166
|
+
"""Print number of entry per status per tables in the database"""
|
|
167
|
+
with DB() as db:
|
|
168
|
+
info = db.get_nb_per_status()
|
|
169
|
+
print_db_info(info)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@click.command("gui", help="Launch GUI")
|
|
173
|
+
def launch_gui():
|
|
174
|
+
UI.info("Launching GUI...")
|
|
175
|
+
import importlib.resources as resources
|
|
176
|
+
|
|
177
|
+
gui_path = resources.files("rcdl.gui").joinpath("gui.py")
|
|
178
|
+
subprocess.run([sys.executable, "-m", "streamlit", "run", str(gui_path)])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@click.command("show-config")
|
|
182
|
+
def show_config():
|
|
183
|
+
"""Show all value in Config"""
|
|
184
|
+
for name, value in vars(Config).items():
|
|
185
|
+
if not name.startswith("__") and not inspect.isroutine(value):
|
|
186
|
+
UI.success(f"{name}: {value}")
|
|
187
|
+
|
|
188
|
+
|
|
120
189
|
# --- CLI GROUP ---
|
|
121
190
|
@click.group()
|
|
122
191
|
@click.option("--debug", is_flag=True)
|
|
123
192
|
@click.option("--dry-run", is_flag=True)
|
|
124
193
|
@click.version_option(version=__version__, prog_name=Config.APP_NAME)
|
|
125
194
|
def cli(debug, dry_run):
|
|
195
|
+
"""Init cli app. Assign Config var depending on flag used when calling prgm"""
|
|
126
196
|
Config.set_debug(debug)
|
|
127
197
|
Config.set_dry_run(dry_run)
|
|
128
198
|
|
|
129
199
|
|
|
200
|
+
# main commands
|
|
130
201
|
cli.add_command(dlsf)
|
|
131
202
|
cli.add_command(discover)
|
|
132
203
|
cli.add_command(refresh)
|
|
204
|
+
cli.add_command(fuse)
|
|
205
|
+
cli.add_command(opti)
|
|
206
|
+
cli.add_command(launch_gui)
|
|
207
|
+
|
|
208
|
+
# creators command
|
|
133
209
|
cli.add_command(add_creator)
|
|
134
210
|
cli.add_command(remove_creator)
|
|
135
211
|
cli.add_command(list_creators)
|
|
136
|
-
|
|
212
|
+
|
|
213
|
+
# helper command
|
|
214
|
+
cli.add_command(clean)
|
|
215
|
+
cli.add_command(db_status)
|
|
216
|
+
cli.add_command(show_config)
|
rcdl/interface/ui.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# interface/ui.py
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import click
|
|
4
5
|
from rich.console import Console, Group
|
|
5
6
|
from rich.table import Table
|
|
6
7
|
from rich.progress import (
|
|
@@ -9,11 +10,100 @@ from rich.progress import (
|
|
|
9
10
|
BarColumn,
|
|
10
11
|
TextColumn,
|
|
11
12
|
TimeRemainingColumn,
|
|
13
|
+
TimeElapsedColumn,
|
|
12
14
|
)
|
|
13
15
|
from rich import box
|
|
14
16
|
from rich.live import Live
|
|
15
17
|
from rich.text import Text
|
|
16
|
-
from rcdl.core.models import
|
|
18
|
+
from rcdl.core.models import Creator, FusedStatus, Status
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NestedProgress:
|
|
22
|
+
def __init__(self, console: Console):
|
|
23
|
+
self.console = console
|
|
24
|
+
self.global_progress: Progress | None = None
|
|
25
|
+
self.current_progress: Progress | None = None
|
|
26
|
+
self.live: Live | None = None
|
|
27
|
+
|
|
28
|
+
self.total_task: int | None = None
|
|
29
|
+
self.current_task: int | None = None
|
|
30
|
+
self.status_text = Text("", style="cyan")
|
|
31
|
+
|
|
32
|
+
def start(
|
|
33
|
+
self, *, total: int, total_label: str = "Total", current_label: str = "Current"
|
|
34
|
+
):
|
|
35
|
+
# Global progress (elapsed only)
|
|
36
|
+
self.global_progress = Progress(
|
|
37
|
+
SpinnerColumn(),
|
|
38
|
+
TextColumn("[bold cyan]{task.description}"),
|
|
39
|
+
BarColumn(),
|
|
40
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
41
|
+
TimeElapsedColumn(),
|
|
42
|
+
console=self.console,
|
|
43
|
+
transient=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Current task progress (ETA included)
|
|
47
|
+
self.current_progress = Progress(
|
|
48
|
+
SpinnerColumn(),
|
|
49
|
+
TextColumn("[bold cyan]{task.description}"),
|
|
50
|
+
BarColumn(),
|
|
51
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
52
|
+
TimeRemainingColumn(),
|
|
53
|
+
console=self.console,
|
|
54
|
+
transient=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
group = Group(self.global_progress, self.current_progress, self.status_text)
|
|
58
|
+
self.live = Live(group, console=self.console)
|
|
59
|
+
self.live.__enter__()
|
|
60
|
+
|
|
61
|
+
self.total_task = self.global_progress.add_task(total_label, total=total)
|
|
62
|
+
self.current_task = self.current_progress.add_task(
|
|
63
|
+
current_label, total=1, visible=False
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# total task helpers
|
|
67
|
+
def advance_total(self, step: int = 1):
|
|
68
|
+
if self.global_progress and self.total_task is not None:
|
|
69
|
+
self.global_progress.advance(self.total_task, step) # type: ignore
|
|
70
|
+
|
|
71
|
+
def start_current(self, description: str, total: int | None = None):
|
|
72
|
+
if not self.current_progress or self.current_task is None:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
self.current_progress.update(
|
|
76
|
+
self.current_task, # type: ignore
|
|
77
|
+
description=description,
|
|
78
|
+
total=total,
|
|
79
|
+
completed=0,
|
|
80
|
+
visible=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def advance_current(self, step: int | float = 1):
|
|
84
|
+
if self.current_progress and self.current_task is not None:
|
|
85
|
+
self.current_progress.advance(self.current_task, step) # type: ignore
|
|
86
|
+
|
|
87
|
+
def finish_current(self):
|
|
88
|
+
if self.current_progress and self.current_task is not None:
|
|
89
|
+
self.current_progress.update(self.current_task, visible=False) # type: ignore
|
|
90
|
+
|
|
91
|
+
# ---------------------
|
|
92
|
+
# Status line
|
|
93
|
+
# ---------------------
|
|
94
|
+
def set_status(self, cyan: str, green: str = ""):
|
|
95
|
+
self.status_text.plain = ""
|
|
96
|
+
self.status_text.append(cyan, style="cyan")
|
|
97
|
+
if green:
|
|
98
|
+
self.status_text.append(green, style="green")
|
|
99
|
+
|
|
100
|
+
# ---------------------
|
|
101
|
+
# Close
|
|
102
|
+
# ---------------------
|
|
103
|
+
def close(self):
|
|
104
|
+
if self.live:
|
|
105
|
+
self.live.__exit__(None, None, None)
|
|
106
|
+
self.live = None
|
|
17
107
|
|
|
18
108
|
|
|
19
109
|
class UI:
|
|
@@ -23,6 +113,7 @@ class UI:
|
|
|
23
113
|
_video_progress_text: Text | None = None
|
|
24
114
|
_concat_progress_text: Text | None = None
|
|
25
115
|
_live: Live | None = None
|
|
116
|
+
_generic_text: Text | None = None
|
|
26
117
|
|
|
27
118
|
@staticmethod
|
|
28
119
|
def _log_to_file(log_level, msg: str):
|
|
@@ -41,8 +132,8 @@ class UI:
|
|
|
41
132
|
|
|
42
133
|
@classmethod
|
|
43
134
|
def debug(cls, msg: str):
|
|
44
|
-
"""
|
|
45
|
-
cls.console.print(f"[dim]{msg}[/]")
|
|
135
|
+
"""Log debug msg"""
|
|
136
|
+
# cls.console.print(f"[dim]{msg}[/]")
|
|
46
137
|
cls._log_to_file(cls.logger.debug, msg)
|
|
47
138
|
|
|
48
139
|
@classmethod
|
|
@@ -57,26 +148,6 @@ class UI:
|
|
|
57
148
|
cls.console.print(f"[red]{msg}[/]")
|
|
58
149
|
cls._log_to_file(cls.logger.debug, msg)
|
|
59
150
|
|
|
60
|
-
@classmethod
|
|
61
|
-
def db_videos_status_table(cls, info: dict):
|
|
62
|
-
"""
|
|
63
|
-
Print to cli a table with info of numbers of videos per status.
|
|
64
|
-
Take in arg a dict: {
|
|
65
|
-
"not_downloaded": int,
|
|
66
|
-
"downloaded": int, "failed: int", "skipped: int", "ignored: int"}
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
table = Table(title="DB Videos status")
|
|
70
|
-
|
|
71
|
-
table.add_column("Video status")
|
|
72
|
-
table.add_column("Number of videos")
|
|
73
|
-
|
|
74
|
-
for status in VideoStatus:
|
|
75
|
-
name = status.value.replace("_", " ").capitalize()
|
|
76
|
-
table.add_row(name, str(info[status.value]))
|
|
77
|
-
|
|
78
|
-
cls.console.print(table)
|
|
79
|
-
|
|
80
151
|
@classmethod
|
|
81
152
|
def table_creators(cls, creators: list[Creator]):
|
|
82
153
|
"""Print to cli a table with all creators in creators.txt. Format is Creator ID | Service"""
|
|
@@ -84,7 +155,7 @@ class UI:
|
|
|
84
155
|
table.add_column("Creators ID")
|
|
85
156
|
table.add_column("Service")
|
|
86
157
|
for creator in creators:
|
|
87
|
-
table.add_row(creator.
|
|
158
|
+
table.add_row(creator.id, creator.service)
|
|
88
159
|
cls.console.print(table)
|
|
89
160
|
|
|
90
161
|
@classmethod
|
|
@@ -98,96 +169,14 @@ class UI:
|
|
|
98
169
|
)
|
|
99
170
|
return progress
|
|
100
171
|
|
|
101
|
-
@classmethod
|
|
102
|
-
def video_progress(cls, total: int):
|
|
103
|
-
"""Create video download progress output"""
|
|
104
|
-
progress = Progress(
|
|
105
|
-
SpinnerColumn(),
|
|
106
|
-
TextColumn("[bold cyan]{task.description}"),
|
|
107
|
-
BarColumn(),
|
|
108
|
-
TextColumn("{task.completed}/{task.total}"),
|
|
109
|
-
TimeRemainingColumn(),
|
|
110
|
-
console=cls.console,
|
|
111
|
-
transient=False, # remove the bar after completion
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
cls._video_progress_text = Text("Waiting...", style="Cyan")
|
|
115
|
-
group = Group(progress, cls._video_progress_text)
|
|
116
|
-
cls._live = Live(group, console=cls.console)
|
|
117
|
-
cls._live.__enter__()
|
|
118
|
-
|
|
119
|
-
task = progress.add_task("Downloading videos", total=total)
|
|
120
|
-
return progress, task
|
|
121
|
-
|
|
122
|
-
@classmethod
|
|
123
|
-
def set_current_video_progress(cls, creator_info: str, filename: str):
|
|
124
|
-
"""Update video download output
|
|
125
|
-
args:
|
|
126
|
-
creator_info: str = 'creator_id@(service)'
|
|
127
|
-
filename: str = video.relative_path
|
|
128
|
-
"""
|
|
129
|
-
if cls._video_progress_text is None:
|
|
130
|
-
return
|
|
131
|
-
cls._video_progress_text.plain = ""
|
|
132
|
-
cls._video_progress_text.append(f"{creator_info} -> ", style="Cyan")
|
|
133
|
-
cls._video_progress_text.append(filename, style="green")
|
|
134
|
-
|
|
135
|
-
@classmethod
|
|
136
|
-
def close_video_progress(cls):
|
|
137
|
-
"""Close video progress"""
|
|
138
|
-
if cls._live:
|
|
139
|
-
cls._live.__exit__(None, None, None)
|
|
140
|
-
cls._live = None
|
|
141
172
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
transient=False, # remove the bar after completion
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
cls._concat_progress_text = Text("Waiting...", style="Cyan")
|
|
156
|
-
group = Group(progress, cls._concat_progress_text)
|
|
157
|
-
cls._live = Live(group, console=cls.console)
|
|
158
|
-
cls._live.__enter__()
|
|
159
|
-
|
|
160
|
-
task = progress.add_task("Concatenating videos", total=total)
|
|
161
|
-
return progress, task
|
|
162
|
-
|
|
163
|
-
@classmethod
|
|
164
|
-
def set_current_concat_progress(cls, msg: str, filename: str):
|
|
165
|
-
"""Update video download output
|
|
166
|
-
args:
|
|
167
|
-
creator_info: str = 'creator_id@(service)'
|
|
168
|
-
filename: str = video.relative_path
|
|
169
|
-
"""
|
|
170
|
-
if cls._concat_progress_text is None:
|
|
171
|
-
return
|
|
172
|
-
cls._concat_progress_text.plain = ""
|
|
173
|
-
cls._concat_progress_text.append(f"{msg} -> ", style="Cyan")
|
|
174
|
-
cls._concat_progress_text.append(filename, style="green")
|
|
175
|
-
|
|
176
|
-
@classmethod
|
|
177
|
-
def close_concat_progress(cls):
|
|
178
|
-
"""Close video progress"""
|
|
179
|
-
if cls._live:
|
|
180
|
-
cls._live.__exit__(None, None, None)
|
|
181
|
-
cls._live = None
|
|
182
|
-
|
|
183
|
-
@classmethod
|
|
184
|
-
def progress_total_concat(cls):
|
|
185
|
-
progress = Progress(
|
|
186
|
-
SpinnerColumn(),
|
|
187
|
-
TextColumn("[progress.description]{task.description}"),
|
|
188
|
-
BarColumn(),
|
|
189
|
-
TextColumn("{task.completed}/{task.total}"),
|
|
190
|
-
console=cls.console,
|
|
191
|
-
transient=False, # remove progress bar after finish
|
|
192
|
-
)
|
|
193
|
-
return progress
|
|
173
|
+
def print_db_info(info: dict):
|
|
174
|
+
click.echo("--- TABLES ---")
|
|
175
|
+
click.echo("Posts:")
|
|
176
|
+
click.echo(f"\t{info['posts']} total")
|
|
177
|
+
click.echo("FusedMedias:")
|
|
178
|
+
for status in FusedStatus:
|
|
179
|
+
click.echo(f"\t{info['fuses'][status]} {status}")
|
|
180
|
+
click.echo("Medias:")
|
|
181
|
+
for status in Status:
|
|
182
|
+
click.echo(f"\t{info['medias'][status]} {status}")
|
rcdl/utils.py
CHANGED
|
@@ -1,11 +1,169 @@
|
|
|
1
1
|
# utils.py
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
"""Hold collection of useful functions"""
|
|
4
4
|
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
import rcdl.core.downloader_subprocess as dls
|
|
13
|
+
from rcdl.core.config import Config
|
|
14
|
+
from rcdl.interface.ui import UI
|
|
8
15
|
|
|
9
16
|
|
|
10
|
-
def
|
|
11
|
-
|
|
17
|
+
def get_date_now() -> str:
|
|
18
|
+
"""Return an str of current datetime in isoformat"""
|
|
19
|
+
return datetime.now(timezone.utc).isoformat()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_media_metadata(full_path: str) -> tuple[int, int, str]:
|
|
23
|
+
"""Get duration (if possible), file_size and checksum"""
|
|
24
|
+
path = Path(full_path)
|
|
25
|
+
if not os.path.exists(path):
|
|
26
|
+
UI.error(f"{path} path should exist but does not.")
|
|
27
|
+
|
|
28
|
+
file_size = path.stat().st_size
|
|
29
|
+
checksum = get_media_hash(path)
|
|
30
|
+
if checksum is None:
|
|
31
|
+
checksum = ""
|
|
32
|
+
duration = dls.ffprobe_get_duration(path)
|
|
33
|
+
if duration is None:
|
|
34
|
+
duration = 0
|
|
35
|
+
return duration, file_size, checksum
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_media_hash(path: Path, retries: int = Config.CHECKSUM_RETRY) -> str | None:
|
|
39
|
+
"""Return a hash of a video file"""
|
|
40
|
+
for attempt in range(retries + 1):
|
|
41
|
+
try:
|
|
42
|
+
sha256_hash = hashlib.sha256()
|
|
43
|
+
with path.open("rb") as f:
|
|
44
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
45
|
+
sha256_hash.update(chunk)
|
|
46
|
+
checksum = sha256_hash.hexdigest()
|
|
47
|
+
return checksum
|
|
48
|
+
except OSError as e:
|
|
49
|
+
if attempt >= retries:
|
|
50
|
+
UI.error(
|
|
51
|
+
f"OSError: Failed to checksum {path} "
|
|
52
|
+
f"at attempt {attempt + 1} due to: {e}"
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
UI.warning(
|
|
56
|
+
f"OSError: Failed to checksum {path} "
|
|
57
|
+
f"at attempt {attempt + 1} due to: {e}"
|
|
58
|
+
)
|
|
59
|
+
time.sleep(1.0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_json_hash(data: dict) -> tuple[str, str]:
|
|
63
|
+
"""Return a hash of a dict"""
|
|
64
|
+
raw_json = json.dumps(data, sort_keys=True)
|
|
65
|
+
json_hash = hashlib.sha256(raw_json.encode("utf-8")).hexdigest()
|
|
66
|
+
return raw_json, json_hash
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def bytes_to_mb(bytes: float | int):
|
|
70
|
+
return round(bytes / (1024 * 1024), 1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clean_all(all: bool, partial: bool, cache: bool, medias_deleted: bool):
|
|
74
|
+
"""Remove partial file & external programm cache"""
|
|
75
|
+
|
|
76
|
+
from rcdl.core.db import DB
|
|
77
|
+
from rcdl.core.models import Status
|
|
78
|
+
|
|
79
|
+
if all:
|
|
80
|
+
partial = True
|
|
81
|
+
cache = True
|
|
82
|
+
medias_deleted = True
|
|
83
|
+
|
|
84
|
+
# remove all partial file
|
|
85
|
+
if partial:
|
|
86
|
+
path = Config.BASE_DIR
|
|
87
|
+
folders = os.listdir(path)
|
|
88
|
+
|
|
89
|
+
for folder in folders:
|
|
90
|
+
if folder.startswith("."):
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
folder_full_path = os.path.join(path, folder)
|
|
94
|
+
files = os.listdir(folder_full_path)
|
|
95
|
+
|
|
96
|
+
for file in files:
|
|
97
|
+
if (
|
|
98
|
+
file.endswith(".aria2")
|
|
99
|
+
or file.endswith(".part")
|
|
100
|
+
or file.endswith(".opti.mp4")
|
|
101
|
+
):
|
|
102
|
+
full_path = os.path.join(folder_full_path, file)
|
|
103
|
+
os.remove(full_path)
|
|
104
|
+
UI.info(f"Removed {full_path}")
|
|
105
|
+
|
|
106
|
+
# cache
|
|
107
|
+
if cache:
|
|
108
|
+
# clear yt-dlp cache
|
|
109
|
+
dls.ytdlp_clear_cache()
|
|
110
|
+
UI.info("Cleared yt-dlp cache dir")
|
|
111
|
+
|
|
112
|
+
# kill aria2c
|
|
113
|
+
dls.kill_aria2c()
|
|
114
|
+
UI.info("Kill aria2c")
|
|
115
|
+
|
|
116
|
+
# medias with status deleted
|
|
117
|
+
if medias_deleted:
|
|
118
|
+
with DB() as db:
|
|
119
|
+
medias = db.query_media_by_status(Status.TO_BE_DELETED)
|
|
120
|
+
UI.info(f"Found {len(medias)} medias with DELETED status")
|
|
121
|
+
|
|
122
|
+
if len(medias) == 0:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
total_size = 0.0
|
|
126
|
+
total_vid = 0
|
|
127
|
+
for media in medias:
|
|
128
|
+
with DB() as db:
|
|
129
|
+
post = db.query_post_by_id(media.post_id)
|
|
130
|
+
if post is None:
|
|
131
|
+
UI.warning(f"Could not match post id {media.post_id} to a post")
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
path = os.path.join(Config.creator_folder(post.user), media.file_path)
|
|
135
|
+
if not os.path.exists:
|
|
136
|
+
UI.error(f"Path {path} should exist. Does not")
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
os.remove(path)
|
|
141
|
+
UI.info(f"Removed '{path}' ({bytes_to_mb(media.file_size)} MB)")
|
|
142
|
+
total_size += media.file_size
|
|
143
|
+
total_vid += 1
|
|
144
|
+
except Exception as e:
|
|
145
|
+
UI.error(f"Failed to rm media {media.post_id}/{media.url} due to: {e}")
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
with DB() as db:
|
|
149
|
+
media.status = Status.DELETED
|
|
150
|
+
db.update_media(media)
|
|
151
|
+
|
|
152
|
+
UI.info(
|
|
153
|
+
f"Removed a total of {total_vid} videos ({bytes_to_mb(total_size)}MB,"
|
|
154
|
+
f" {round(bytes_to_mb(total_size) / 100, 1)}GB)"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_seconds(seconds: int | float) -> str:
|
|
159
|
+
seconds = int(seconds)
|
|
160
|
+
h = seconds // 3600
|
|
161
|
+
m = (seconds % 3600) // 60
|
|
162
|
+
s = seconds % 60
|
|
163
|
+
|
|
164
|
+
if h > 0:
|
|
165
|
+
return f"{h}h:{m:02d}m:{s:02d}s"
|
|
166
|
+
elif m > 0:
|
|
167
|
+
return f"{m}m:{s:02d}s"
|
|
168
|
+
else:
|
|
169
|
+
return f"{s}s"
|