rcdl 2.2.2__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.
@@ -0,0 +1,188 @@
1
+ # scripts/migrate_old_format_to_db.py
2
+
3
+ """
4
+ Use this script to migrate from pre-2.0 to 2+.
5
+ Script migrate_old_format_to_db version: v1.0
6
+
7
+ old_format:
8
+ - no db
9
+ cdl/
10
+ creator1/
11
+ date_title1_p0.mp4
12
+ date_title2_p0.mp4
13
+ date_title2_p1.mp4
14
+ ...
15
+ ...
16
+
17
+ for each creator:
18
+ - [x] get all posts
19
+ - [x] match each videos to its posts
20
+ - [x] update db
21
+ - [ ] check db with local videos
22
+ """
23
+
24
+ import logging
25
+ import os
26
+ from pathlib import Path
27
+ import shutil
28
+
29
+ from rcdl.core.config import Config
30
+ from rcdl.core.models import Creator, Video, VideoStatus
31
+ from rcdl.core.file_io import load_json
32
+ from rcdl.core.api import URL
33
+ from rcdl.core.db import DB
34
+ import rcdl.core.downloader as dl
35
+ import rcdl.core.parser as parser
36
+
37
+ Config.ensure_dirs()
38
+ Config.ensure_files()
39
+
40
+ with DB() as db:
41
+ db.init_table()
42
+
43
+ CDL_PATH = Path.home() / "Videos" / "cdl"
44
+ CREATORS_JSON = CDL_PATH / ".cache" / "creators.json"
45
+ TEMP_JSON = str(CDL_PATH / "temp.json")
46
+ LOG_PATH = CDL_PATH / "migrate_log.log"
47
+ MV_TXT = CDL_PATH / "mv.txt"
48
+ DEST_BASE = Path("/home/elitedesk/Videos/rcdl")
49
+
50
+ logging.basicConfig(
51
+ filename=LOG_PATH,
52
+ filemode="a",
53
+ encoding="utf-8",
54
+ level=logging.INFO,
55
+ format="{asctime} - {levelname} - {message}",
56
+ style="{",
57
+ datefmt="%Y-%m-%d %H:%M:%S",
58
+ )
59
+ console = logging.StreamHandler()
60
+ console.setFormatter(logging.Formatter("{levelname}: {message}", style="{"))
61
+ logging.getLogger().addHandler(console)
62
+
63
+ open(MV_TXT, "w").close()
64
+
65
+
66
+ def add_to_mvtxt(filepath: str):
67
+ with open(MV_TXT, "a") as f:
68
+ f.write(filepath + "\n")
69
+
70
+
71
+ def update_db_info(video: Video):
72
+ with DB() as db:
73
+ db._upsert_video(video)
74
+
75
+
76
+ def move_files(mv_txt: Path, dest_base: Path):
77
+ # Read all file paths
78
+ i = 0
79
+ with mv_txt.open("r", encoding="utf-8") as f:
80
+ paths = [line.strip() for line in f if line.strip()]
81
+
82
+ for src_path_str in paths:
83
+ src = Path(src_path_str)
84
+ if not src.exists():
85
+ print(f"Skipping missing file: {src}")
86
+ i += 1
87
+ continue
88
+
89
+ # Compute destination folder
90
+ relative_parts = src.parts[len(Path("/home/elitedesk/Videos/cdl").parts) :]
91
+ dest_dir = dest_base.joinpath(*relative_parts[:-1])
92
+ dest_dir.mkdir(parents=True, exist_ok=True)
93
+
94
+ # Destination path
95
+ dest = dest_dir / src.name
96
+
97
+ # Move the file
98
+ shutil.move(str(src), str(dest))
99
+ i += 1
100
+ print(f"Moved ({i}/{len(paths)}): {src} -> {dest}")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ logging.info("--- MIGRATE pre-v2 to +2.0 Script START ---")
105
+
106
+ # get all local creators
107
+ creators_json = load_json(CREATORS_JSON)
108
+ creators: list[Creator] = []
109
+ for creator in creators_json:
110
+ creators.append(
111
+ Creator(
112
+ creator_id=creator["creator_id"],
113
+ service=creator["service"],
114
+ domain=creator["domain"],
115
+ status=None,
116
+ )
117
+ )
118
+
119
+ for creator in creators:
120
+ # get posts
121
+ url = URL.get_creator_post_wo_param(creator)
122
+ print(f"Request {url} up to {15} max page")
123
+ pf = dl.PostsFetcher(url, TEMP_JSON, max_page=15)
124
+ pf.request()
125
+
126
+ posts = parser.filter_posts_with_videos_from_json(TEMP_JSON)
127
+ print(f"Found {len(posts)} posts with videos for creator {creator.creator_id}")
128
+
129
+ posts_videos = parser.convert_posts_to_videos(posts)
130
+ print(f"Converted {len(posts)} to Video")
131
+ for video in posts_videos:
132
+ video.status = VideoStatus.DOWNLOADED
133
+
134
+ # get local videos
135
+ creator_path = os.path.join(CDL_PATH, creator.creator_id)
136
+ print(f"Looking in {creator_path}")
137
+ files = os.listdir(creator_path)
138
+
139
+ local_videos: list[dict] = []
140
+ for file in files:
141
+ # ignore partial file
142
+ if file.endswith(".part") or file.endswith(".aria2"):
143
+ continue
144
+
145
+ # remove ext
146
+ name = file[:-4] if file.endswith(".mp4") else file
147
+
148
+ # part number
149
+ if "_p" not in name:
150
+ print(f"Skipped file due to missing part number: {file}")
151
+
152
+ try:
153
+ base, part_str = name.rsplit("_p", 1)
154
+
155
+ # extract date and title
156
+ date, *title_parts = base.split("_")
157
+ title = "_".join(title_parts) # keeps underscores in title
158
+
159
+ local_videos.append(
160
+ {
161
+ "date": date, # e.g. "2025-12-25"
162
+ "title": title,
163
+ "part": part_str, # e.g. "0"
164
+ }
165
+ )
166
+ except: # noqa E277
167
+ pass
168
+
169
+ print(f"Found {len(local_videos)} local videos")
170
+
171
+ # match local vid to videos list
172
+ for lv in local_videos:
173
+ for pv in posts_videos:
174
+ if (
175
+ lv["date"] == pv.published
176
+ and lv["title"] == pv.title
177
+ and lv["part"] == str(pv.part)
178
+ ):
179
+ print(f"Found a match for {pv.relative_path}")
180
+ update_db_info(pv)
181
+ add_to_mvtxt(os.path.join(creator_path, pv.relative_path))
182
+ break
183
+ else:
184
+ print(f"No match found {lv['date']}_{lv['title']}_{lv['part']}")
185
+
186
+ move_files(MV_TXT, DEST_BASE)
187
+ shutil.move(str(MV_TXT), str(MV_TXT) + f".{creator.creator_id}.txt")
188
+ open(MV_TXT, "w").close()
@@ -0,0 +1,98 @@
1
+ # scripts/upload_pypi.py
2
+
3
+ import subprocess
4
+ import sys
5
+ import os
6
+ import requests
7
+ from packaging.version import parse as parse_version
8
+ import tomllib
9
+
10
+ # config
11
+ PYPROJECT_FILE = "pyproject.toml"
12
+ PYPI_PACKAGE_NAME = "rcdl" # PyPI package name
13
+
14
+ # read local version
15
+ with open(PYPROJECT_FILE, "rb") as f:
16
+ data = tomllib.load(f)
17
+
18
+ local_version = data["project"]["version"]
19
+ print(f"Local version: {local_version}")
20
+
21
+ # check remote latest version
22
+ response = requests.get(f"https://pypi.org/pypi/{PYPI_PACKAGE_NAME}/json")
23
+ if response.status_code == 200:
24
+ latest_version = response.json()["info"]["version"]
25
+ print(f"Latest PyPI version: {latest_version}")
26
+ else:
27
+ latest_version = None
28
+ print("ERROR: Package not found on PyPI")
29
+
30
+ # chec version number
31
+ if latest_version and parse_version(local_version) <= parse_version(latest_version):
32
+ print("Error: Local version is not higher than PyPI version.")
33
+ sys.exit(1)
34
+
35
+ # build with flit
36
+ print("Building package...")
37
+ subprocess.run([sys.executable, "-m", "flit", "build"], check=True)
38
+
39
+ # upload to pypi
40
+ print("Uploading to PyPI...")
41
+ if not os.path.exists("api_key.txt"):
42
+ print(
43
+ "ERROR - you have to create an api_key.txt file in your root directory containing only your pypi api key"
44
+ )
45
+ quit()
46
+ with open("api_key.txt", "r") as f:
47
+ api_key = f.read().strip()
48
+ if api_key == "":
49
+ print("ERROR - api_key.txt is empty")
50
+
51
+ subprocess.run(
52
+ [
53
+ sys.executable,
54
+ "-m",
55
+ "twine",
56
+ "upload",
57
+ "-u",
58
+ "__token__",
59
+ "-p",
60
+ api_key,
61
+ "dist/*",
62
+ ],
63
+ check=True,
64
+ )
65
+
66
+ print("PYPI Upload complete!")
67
+
68
+ # find wheel file in dist/
69
+ dist_files = [f for f in os.listdir("dist") if f.endswith(".whl")]
70
+ if not dist_files:
71
+ raise FileNotFoundError("No .whl file found in dist/")
72
+
73
+ whl_file_path = os.path.join("dist", dist_files[0])
74
+ if local_version not in whl_file_path:
75
+ raise FileNotFoundError("Version of .whl does not match upload.")
76
+
77
+ # create github release
78
+ tag = f"v{local_version}"
79
+ print(f"Creating github release {tag}")
80
+
81
+
82
+ subprocess.run(
83
+ [
84
+ "gh",
85
+ "release",
86
+ "create",
87
+ tag,
88
+ "--title",
89
+ f"Release {tag}",
90
+ "--notes",
91
+ "",
92
+ whl_file_path,
93
+ ],
94
+ check=True,
95
+ )
96
+
97
+ print("GitHub release created!")
98
+ print("--END--")
rcdl/utils.py ADDED
@@ -0,0 +1,11 @@
1
+ # utils.py
2
+
3
+ import click
4
+
5
+
6
+ def echo(text: str):
7
+ click.echo(text)
8
+
9
+
10
+ def echoc(text: str, color: str):
11
+ click.echo(click.style(text, fg=color))
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: rcdl
3
+ Version: 2.2.2
4
+ Summary: Coomer/Kemono CLI Downloader
5
+ Keywords: downloader,video,media
6
+ Author: Anonymous
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Dist: click>=8.2
13
+ Requires-Dist: requests>=2.32
14
+ Requires-Dist: pathvalidate==3.3.1
15
+ Requires-Dist: moviepy==2.2.1
16
+ Requires-Dist: rich==14.2.0
17
+
18
+ # RCDL
19
+
20
+ Riton Coomer Download Manager
21
+ `rcdl` is a tool to automatically download the videos of your favorites creators from [coomer.st](https://coomer.st) and [kemono.cr](https://kemono.cr)
22
+
23
+
24
+ ## Install
25
+ ### Dependencies
26
+ - [yt-dlp](https://github.com/yt-dlp/yt-dlp)
27
+ - [aria2](https://github.com/aria2/aria2)
28
+ - [ffmpeg](https://www.ffmpeg.org/download.html) (Only for `fuse` command)
29
+ Recommended install:
30
+ ```bash
31
+ pipx install yt-dlp
32
+ sudo apt install aria2 ffmpeg
33
+ ```
34
+ ### Install RCDL
35
+ It is recommended to use pipx
36
+ ```bash
37
+ pipx install rcdl
38
+ ```
39
+ or else:
40
+ ```bash
41
+ pip install rcdl
42
+ ```
43
+
44
+ ## How to use
45
+
46
+ Run the CLI with:
47
+
48
+ ```bash
49
+ rcdl --help
50
+ ```
51
+
52
+ By default all files will live in `~/Videos/rcdl/`. Cache, configuration and log file will be in a hidden `rcdl/.cache/` folder.
53
+
54
+ ```bash
55
+ rcdl refresh # look creators.json and find all possible videos
56
+ rcdl dlsf # download all found videos
57
+ rcdl discover # WIP
58
+ rcdl fuse # WIP
59
+ rcdl log # debug only; show the log file
60
+ ```
61
+
62
+ Add, rm, list a creator:
63
+ ```bash
64
+ rcdl add [URL]
65
+ rcdl add [service]/[creator_id]
66
+ ```
67
+
68
+ ## Dev
69
+ ### Install
70
+ ```bash
71
+ git clone https://github.com/ritonun/cdl.git rcdl
72
+ cd rcdl
73
+ python3 -m venv .venv
74
+ source .venv/bin/activate
75
+ pip install -e .
76
+ ```
77
+
78
+ ## Deploy
79
+ ```bash
80
+ python3 -m pip install --upgrade build
81
+ python3 -m pip install --upgrade twine
82
+ pip install flit packaging requests # necessary to run auto release scripts
83
+
84
+ # Use convenience scripts in rcdl/scripts
85
+ # Create api_key.txt with the pypi api key in the root folder
86
+ python3 rcdl/scripts/upload_pypi.py
87
+ python3 rcdl/scripts/migrate_old_format_to_db.py
88
+ ```
89
+
@@ -0,0 +1,22 @@
1
+ rcdl/__init__.py,sha256=6TJotAX_12BuAA2z78HW-KH0MudTmZSSeZXy9G_PeW4,85
2
+ rcdl/__main__.py,sha256=sq5lbBnJf7_jh9HD1WAC5z1JDu0HDLWE3c9D-fiL6Jg,479
3
+ rcdl/utils.py,sha256=MMeV3UjDEC1r5nxkuFW3GxGIsqCmYtEUTP2G2YWNnHM,149
4
+ rcdl/core/api.py,sha256=OajUITAG2vfXEeoifL73wb96o_dsAypqQQkx2lK5LNo,1752
5
+ rcdl/core/config.py,sha256=NclmKraunK0parCdm_LFbJ4w4mq11SAnzeLo30I05-8,2487
6
+ rcdl/core/db.py,sha256=5Ajppng4gJ-dvTqUBLYs3SFZzBw7t6GUiZDOBy8_CEM,7607
7
+ rcdl/core/db_queries.py,sha256=xFS66bn3LSUOzgMIS1JvKOOknPfGI1NJleRSupHvtaM,1695
8
+ rcdl/core/downloader.py,sha256=L4huh-JI8zwjT6ChvyoYl-c3rmItYAOlEZNAUwLflXo,9019
9
+ rcdl/core/downloader_subprocess.py,sha256=xHyOX8K8gLFq6fA63TliRPAe10IV8H3NWeUbNbow5M8,5245
10
+ rcdl/core/file_io.py,sha256=NGkB9Mv7CRVe2qejyY_R9zmLmzLHbtwwxWIqwWVAHfo,731
11
+ rcdl/core/fuse.py,sha256=lBDI0EC-jyWnNRNnqBGucVVOdzkgnf1yqb8438swZ7Y,3879
12
+ rcdl/core/models.py,sha256=1_VZ68Gu7LH_o_NG7QJwQAzRCyOxIps--UCWwJGK9YI,1171
13
+ rcdl/core/parser.py,sha256=8e1vMWnMiVxkzwXtsJG_rxejvJUVLE35rHE7eVQsi2g,7685
14
+ rcdl/interface/cli.py,sha256=SiJmtmGvwOyF9b5ePTjgyNgB051cib7c8qGG4VTFeI8,3660
15
+ rcdl/interface/ui.py,sha256=rrY5Tq4nxDHpDJW6YdoPEc0WqOO8unDDbu0Vl3hlm_I,6180
16
+ rcdl/scripts/migrate_creators_json_txt.py,sha256=5JoySkRmfoQ2ghZCMgnPsVwa8IiOthl-wq2QGN-dqdQ,883
17
+ rcdl/scripts/migrate_old_format_to_db.py,sha256=zhn_L7LlYmWRAMbMBvQxxkrVbXEhMn4msE7ml5kwNYU,5496
18
+ rcdl/scripts/upload_pypi.py,sha256=dzXasvbcx4AjwTZeyamDtQIqE1dQIViLt5RfMpC198A,2325
19
+ rcdl-2.2.2.dist-info/entry_points.txt,sha256=TSFT1avCCO1d8pJnvuQLzX_1bA96uIDV_alKuruDN4E,42
20
+ rcdl-2.2.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
21
+ rcdl-2.2.2.dist-info/METADATA,sha256=BkOnWpy-ji1sC5IBh26mabyIrcMl7mP6-xiAaEeuW5g,2097
22
+ rcdl-2.2.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ rcdl=rcdl.__main__:cli
3
+