rcdl 3.0.0b20__tar.gz
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-3.0.0b20/PKG-INFO +122 -0
- rcdl-3.0.0b20/README.md +104 -0
- rcdl-3.0.0b20/pyproject.toml +29 -0
- rcdl-3.0.0b20/rcdl/__init__.py +10 -0
- rcdl-3.0.0b20/rcdl/__main__.py +37 -0
- rcdl-3.0.0b20/rcdl/core/__init__.py +0 -0
- rcdl-3.0.0b20/rcdl/core/adapters.py +241 -0
- rcdl-3.0.0b20/rcdl/core/api.py +76 -0
- rcdl-3.0.0b20/rcdl/core/config.py +212 -0
- rcdl-3.0.0b20/rcdl/core/db.py +283 -0
- rcdl-3.0.0b20/rcdl/core/db_queries.py +97 -0
- rcdl-3.0.0b20/rcdl/core/downloader.py +307 -0
- rcdl-3.0.0b20/rcdl/core/downloader_subprocess.py +366 -0
- rcdl-3.0.0b20/rcdl/core/file_io.py +41 -0
- rcdl-3.0.0b20/rcdl/core/fuse.py +127 -0
- rcdl-3.0.0b20/rcdl/core/models.py +105 -0
- rcdl-3.0.0b20/rcdl/core/opti.py +90 -0
- rcdl-3.0.0b20/rcdl/core/parser.py +282 -0
- rcdl-3.0.0b20/rcdl/gui/__init__.py +0 -0
- rcdl-3.0.0b20/rcdl/gui/__main__.py +5 -0
- rcdl-3.0.0b20/rcdl/gui/db_viewer.py +41 -0
- rcdl-3.0.0b20/rcdl/gui/gui.py +54 -0
- rcdl-3.0.0b20/rcdl/gui/video_manager.py +186 -0
- rcdl-3.0.0b20/rcdl/interface/__init__.py +0 -0
- rcdl-3.0.0b20/rcdl/interface/cli.py +216 -0
- rcdl-3.0.0b20/rcdl/interface/ui.py +194 -0
- rcdl-3.0.0b20/rcdl/utils.py +180 -0
rcdl-3.0.0b20/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rcdl
|
|
3
|
+
Version: 3.0.0b20
|
|
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: rich==14.2.0
|
|
16
|
+
Requires-Dist: streamlit==1.52.2
|
|
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
|
+
- [HandBrakeCLI](https://handbrake.fr/docs/en/latest/cli/cli-options.html) (Only for `opti` command)
|
|
30
|
+
Recommended install:
|
|
31
|
+
```bash
|
|
32
|
+
pipx install yt-dlp streamlit
|
|
33
|
+
sudo apt update
|
|
34
|
+
sudo apt install aria2 ffmpeg handbrake-cli python3-tk
|
|
35
|
+
```
|
|
36
|
+
### Install RCDL
|
|
37
|
+
It is recommended to use `pipx` to install `rcdl`
|
|
38
|
+
```bash
|
|
39
|
+
pipx install rcdl
|
|
40
|
+
```
|
|
41
|
+
or else:
|
|
42
|
+
```bash
|
|
43
|
+
pip install rcdl
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## How to use
|
|
47
|
+
|
|
48
|
+
Run the CLI with:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rcdl --help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
By default all files will live in `~/Videos/rcdl/`. Cache, configuration and log file will be in a hidden `rcdl/.cache/` folder.
|
|
55
|
+
|
|
56
|
+
Main function:
|
|
57
|
+
```bash
|
|
58
|
+
rcdl refresh # look creators.json and find all possible videos
|
|
59
|
+
rcdl dlsf # download all found videos
|
|
60
|
+
rcdl discover # Discover new creator (WIP)
|
|
61
|
+
rcdl opti # Optimized video to reduce disk storage usage
|
|
62
|
+
rcdl fuse # Fuse all videos within a same post if they are fully downloaded
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Manage creators:
|
|
66
|
+
```bash
|
|
67
|
+
rcdl list # list all current creators
|
|
68
|
+
rcdl add [URL]
|
|
69
|
+
rcdl add [service]/[creator_id]
|
|
70
|
+
rcdl remove [creator_id]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Helper function:
|
|
74
|
+
```bash
|
|
75
|
+
rcdl status # give number of entry in the database per tables and status
|
|
76
|
+
rcdl clean --all # remove all partially downloaded file, external dependencies cache, etc...
|
|
77
|
+
rcdl show-config # print all config var and theirs value (paths, etc...)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Settings
|
|
81
|
+
Default settings file:
|
|
82
|
+
```toml
|
|
83
|
+
[app]
|
|
84
|
+
default_max_page = 10
|
|
85
|
+
max_fail_count = 7
|
|
86
|
+
timeout = 10
|
|
87
|
+
|
|
88
|
+
[fuse]
|
|
89
|
+
max_width = 1920
|
|
90
|
+
max_height = 1080
|
|
91
|
+
fps = 30
|
|
92
|
+
preset = "veryfast"
|
|
93
|
+
threads = 0
|
|
94
|
+
|
|
95
|
+
[paths]
|
|
96
|
+
base_dir = "~/Videos/rcdl"
|
|
97
|
+
handbrake_run_cmd = "HandBrakeCLI"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
In `rcdl/.cache/config.toml`:
|
|
101
|
+
```toml
|
|
102
|
+
[paths]
|
|
103
|
+
handbrake_run_cmd = "HandBrakeCLI" # if installed via apt
|
|
104
|
+
handbrake_run_cmd = "flatpak run --command=HandBrakeCLI fr.handbrake.ghb" # if installed via flatpak
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Dev
|
|
108
|
+
### Install
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/ritonun/cdl.git rcdl
|
|
111
|
+
cd rcdl
|
|
112
|
+
python3 -m venv .venv
|
|
113
|
+
source .venv/bin/activate
|
|
114
|
+
pip install -e .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Deploy
|
|
118
|
+
```bash
|
|
119
|
+
python3 -m pip install --upgrade build
|
|
120
|
+
python3 -m pip install --upgrade twine
|
|
121
|
+
```
|
|
122
|
+
|
rcdl-3.0.0b20/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# RCDL
|
|
2
|
+
|
|
3
|
+
Riton Coomer Download Manager
|
|
4
|
+
`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)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
### Dependencies
|
|
9
|
+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
|
10
|
+
- [aria2](https://github.com/aria2/aria2)
|
|
11
|
+
- [ffmpeg](https://www.ffmpeg.org/download.html) (Only for `fuse` command)
|
|
12
|
+
- [HandBrakeCLI](https://handbrake.fr/docs/en/latest/cli/cli-options.html) (Only for `opti` command)
|
|
13
|
+
Recommended install:
|
|
14
|
+
```bash
|
|
15
|
+
pipx install yt-dlp streamlit
|
|
16
|
+
sudo apt update
|
|
17
|
+
sudo apt install aria2 ffmpeg handbrake-cli python3-tk
|
|
18
|
+
```
|
|
19
|
+
### Install RCDL
|
|
20
|
+
It is recommended to use `pipx` to install `rcdl`
|
|
21
|
+
```bash
|
|
22
|
+
pipx install rcdl
|
|
23
|
+
```
|
|
24
|
+
or else:
|
|
25
|
+
```bash
|
|
26
|
+
pip install rcdl
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How to use
|
|
30
|
+
|
|
31
|
+
Run the CLI with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rcdl --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
By default all files will live in `~/Videos/rcdl/`. Cache, configuration and log file will be in a hidden `rcdl/.cache/` folder.
|
|
38
|
+
|
|
39
|
+
Main function:
|
|
40
|
+
```bash
|
|
41
|
+
rcdl refresh # look creators.json and find all possible videos
|
|
42
|
+
rcdl dlsf # download all found videos
|
|
43
|
+
rcdl discover # Discover new creator (WIP)
|
|
44
|
+
rcdl opti # Optimized video to reduce disk storage usage
|
|
45
|
+
rcdl fuse # Fuse all videos within a same post if they are fully downloaded
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Manage creators:
|
|
49
|
+
```bash
|
|
50
|
+
rcdl list # list all current creators
|
|
51
|
+
rcdl add [URL]
|
|
52
|
+
rcdl add [service]/[creator_id]
|
|
53
|
+
rcdl remove [creator_id]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Helper function:
|
|
57
|
+
```bash
|
|
58
|
+
rcdl status # give number of entry in the database per tables and status
|
|
59
|
+
rcdl clean --all # remove all partially downloaded file, external dependencies cache, etc...
|
|
60
|
+
rcdl show-config # print all config var and theirs value (paths, etc...)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Settings
|
|
64
|
+
Default settings file:
|
|
65
|
+
```toml
|
|
66
|
+
[app]
|
|
67
|
+
default_max_page = 10
|
|
68
|
+
max_fail_count = 7
|
|
69
|
+
timeout = 10
|
|
70
|
+
|
|
71
|
+
[fuse]
|
|
72
|
+
max_width = 1920
|
|
73
|
+
max_height = 1080
|
|
74
|
+
fps = 30
|
|
75
|
+
preset = "veryfast"
|
|
76
|
+
threads = 0
|
|
77
|
+
|
|
78
|
+
[paths]
|
|
79
|
+
base_dir = "~/Videos/rcdl"
|
|
80
|
+
handbrake_run_cmd = "HandBrakeCLI"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
In `rcdl/.cache/config.toml`:
|
|
84
|
+
```toml
|
|
85
|
+
[paths]
|
|
86
|
+
handbrake_run_cmd = "HandBrakeCLI" # if installed via apt
|
|
87
|
+
handbrake_run_cmd = "flatpak run --command=HandBrakeCLI fr.handbrake.ghb" # if installed via flatpak
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Dev
|
|
91
|
+
### Install
|
|
92
|
+
```bash
|
|
93
|
+
git clone https://github.com/ritonun/cdl.git rcdl
|
|
94
|
+
cd rcdl
|
|
95
|
+
python3 -m venv .venv
|
|
96
|
+
source .venv/bin/activate
|
|
97
|
+
pip install -e .
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Deploy
|
|
101
|
+
```bash
|
|
102
|
+
python3 -m pip install --upgrade build
|
|
103
|
+
python3 -m pip install --upgrade twine
|
|
104
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rcdl"
|
|
3
|
+
version = "3.0.0b20"
|
|
4
|
+
description = "Coomer/Kemono CLI Downloader"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"click>=8.2",
|
|
8
|
+
"requests>=2.32",
|
|
9
|
+
"pathvalidate==3.3.1",
|
|
10
|
+
"rich==14.2.0",
|
|
11
|
+
"streamlit==1.52.2"
|
|
12
|
+
]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Anonymous"},
|
|
15
|
+
]
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
keywords = ["downloader", "video", "media"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
rcdl = "rcdl.__main__:cli"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["flit_core<4"]
|
|
29
|
+
build-backend = "flit_core.buildapi"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# __main__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
__main__: entry in the programm
|
|
5
|
+
Setup logging, create files/folders structures, check dependencies,
|
|
6
|
+
init Config global variables, init database,
|
|
7
|
+
Then call cli group
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from rcdl.core.config import Config, setup_logging, check_dependencies
|
|
13
|
+
|
|
14
|
+
# setup file structure
|
|
15
|
+
Config.ensure_dirs()
|
|
16
|
+
Config.ensure_files()
|
|
17
|
+
|
|
18
|
+
# load config file settings
|
|
19
|
+
Config.load_config()
|
|
20
|
+
|
|
21
|
+
# setup logging
|
|
22
|
+
setup_logging(Config.LOG_FILE, level=0)
|
|
23
|
+
|
|
24
|
+
# check dependencies
|
|
25
|
+
check_dependencies()
|
|
26
|
+
|
|
27
|
+
logging.info("--- INIT ---")
|
|
28
|
+
logging.info("Logger initialized")
|
|
29
|
+
|
|
30
|
+
# init database
|
|
31
|
+
from rcdl.core.db import DB # noqa: E402
|
|
32
|
+
|
|
33
|
+
db = DB()
|
|
34
|
+
db.init_database()
|
|
35
|
+
db.close()
|
|
36
|
+
|
|
37
|
+
from rcdl.interface.cli import cli # noqa: E402, F401
|
|
File without changes
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# core/adapters.py
|
|
2
|
+
|
|
3
|
+
"""Convert [Any] into proper Models from models.py"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from dataclasses import fields
|
|
8
|
+
|
|
9
|
+
from rcdl.core import parser
|
|
10
|
+
from rcdl.interface.ui import UI
|
|
11
|
+
from rcdl.core.models import Post, Media, Status, FusedMedia
|
|
12
|
+
from rcdl.utils import get_date_now, get_json_hash
|
|
13
|
+
|
|
14
|
+
VALID_POST_KEYS = set(
|
|
15
|
+
[
|
|
16
|
+
"id",
|
|
17
|
+
"user",
|
|
18
|
+
"service",
|
|
19
|
+
"title",
|
|
20
|
+
"substring",
|
|
21
|
+
"published",
|
|
22
|
+
"file",
|
|
23
|
+
"attachments",
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _compute_json_metadata(raw: dict) -> tuple[str, str, str]:
|
|
29
|
+
"""From a json dict, return:
|
|
30
|
+
- raw_json: str
|
|
31
|
+
- json_hash: str
|
|
32
|
+
- fetched_at str (datetime)
|
|
33
|
+
"""
|
|
34
|
+
raw_json, json_hash = get_json_hash(raw)
|
|
35
|
+
fetched_at = get_date_now()
|
|
36
|
+
return raw_json, json_hash, fetched_at
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def json_posts_to_posts(posts: list[dict]) -> list[Post]:
|
|
40
|
+
"""Convert a list of json post (dict) into a list of Post model
|
|
41
|
+
Ignore if conversion failed"""
|
|
42
|
+
formatted_posts = []
|
|
43
|
+
for post in posts:
|
|
44
|
+
p = json_post_to_post(post)
|
|
45
|
+
if p is not None:
|
|
46
|
+
formatted_posts.append(p)
|
|
47
|
+
return formatted_posts
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def json_post_to_post(post: dict) -> Post | None:
|
|
51
|
+
"""Convert a json post (dict) into a Post model
|
|
52
|
+
or return None if covnersion failed"""
|
|
53
|
+
post_keys = set(post)
|
|
54
|
+
if post_keys != VALID_POST_KEYS:
|
|
55
|
+
UI.error(
|
|
56
|
+
f"Post id {post.get('id')} of {post.get('user')} "
|
|
57
|
+
f"has invalid schema. "
|
|
58
|
+
f"Missing: {VALID_POST_KEYS - post_keys}, "
|
|
59
|
+
f"Extra: {post_keys - VALID_POST_KEYS}"
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
domain = parser.get_domain(post["service"])
|
|
65
|
+
raw_json, json_hash, fetched_at = _compute_json_metadata(post)
|
|
66
|
+
return Post(
|
|
67
|
+
**post,
|
|
68
|
+
domain=domain,
|
|
69
|
+
json_hash=json_hash,
|
|
70
|
+
raw_json=raw_json,
|
|
71
|
+
fetched_at=fetched_at,
|
|
72
|
+
)
|
|
73
|
+
except TypeError as e:
|
|
74
|
+
UI.error(
|
|
75
|
+
f"Post id {post.get('id')} from {post.get('user')} could not be parsed: {e}"
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def row_to_post(row: sqlite3.Row) -> Post | None:
|
|
81
|
+
"""Convert a sqlite3 row into a Post model.
|
|
82
|
+
Return None if conversion failed"""
|
|
83
|
+
try:
|
|
84
|
+
raw = json.loads(row["raw_json"])
|
|
85
|
+
return Post(
|
|
86
|
+
id=row["id"],
|
|
87
|
+
user=row["user"],
|
|
88
|
+
service=row["service"],
|
|
89
|
+
domain=row["domain"],
|
|
90
|
+
published=row["published"],
|
|
91
|
+
json_hash=row["json_hash"],
|
|
92
|
+
raw_json=row["raw_json"],
|
|
93
|
+
fetched_at=row["fetched_at"],
|
|
94
|
+
title=raw["title"],
|
|
95
|
+
substring=raw["substring"],
|
|
96
|
+
file=raw["file"],
|
|
97
|
+
attachments=raw["attachments"],
|
|
98
|
+
)
|
|
99
|
+
except KeyError as e:
|
|
100
|
+
UI.error(
|
|
101
|
+
f"KeyError: Failed to convert {row['id']} (row_id) into Post model due to: {e}"
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
104
|
+
except TypeError as e:
|
|
105
|
+
UI.error(
|
|
106
|
+
f"TypeError: Failed to convert {row['id']} (row_id) into Post model due to: {e}"
|
|
107
|
+
)
|
|
108
|
+
return None
|
|
109
|
+
except ValueError as e:
|
|
110
|
+
UI.error(
|
|
111
|
+
f"ValueError/JSONDecodeError: Failed to convert "
|
|
112
|
+
f"{row['id']} (row_id) into Post model due to: {e}"
|
|
113
|
+
)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def rows_to_posts(rows: list[sqlite3.Row]) -> list[Post]:
|
|
118
|
+
"""Convert a list of sqlite3 rows. Return a list of Post model.
|
|
119
|
+
Ignore the row if conversion fail"""
|
|
120
|
+
posts: list[Post] = []
|
|
121
|
+
for row in rows:
|
|
122
|
+
post = row_to_post(row)
|
|
123
|
+
if post is not None:
|
|
124
|
+
posts.append(post)
|
|
125
|
+
|
|
126
|
+
if len(posts) != len(rows):
|
|
127
|
+
UI.error(
|
|
128
|
+
f"From {len(rows)} rows, only converted {len(posts)}."
|
|
129
|
+
f" {len(rows) - len(posts)} error."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return posts
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def row_to_media(row: sqlite3.Row) -> Media | None:
|
|
136
|
+
"""Convert a sqlite3 row into a Media model.
|
|
137
|
+
Return None if conversion failed"""
|
|
138
|
+
try:
|
|
139
|
+
# create a dict to hold column of row that are present in Media.
|
|
140
|
+
# Ignore column (like default autoincrement ID) that are not a field in Media
|
|
141
|
+
media_data = {}
|
|
142
|
+
for field in fields(Media):
|
|
143
|
+
field_name = field.name
|
|
144
|
+
if field_name in row.keys():
|
|
145
|
+
value = row[field_name]
|
|
146
|
+
if field_name == "status" and value is not None:
|
|
147
|
+
value = Status(value)
|
|
148
|
+
media_data[field_name] = value
|
|
149
|
+
return Media(**media_data)
|
|
150
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
151
|
+
UI.error(
|
|
152
|
+
f"Key/Type/Value Error: Failed to convert row {row['id']} into Post model due to {e}"
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def rows_to_medias(rows: list[sqlite3.Row]) -> list[Media]:
|
|
158
|
+
"""Convert a list of sqlite3 rows. Return a list of Media model.
|
|
159
|
+
Ignore row if conversion failed"""
|
|
160
|
+
medias: list[Media] = []
|
|
161
|
+
for row in rows:
|
|
162
|
+
media = row_to_media(row)
|
|
163
|
+
if media is not None:
|
|
164
|
+
medias.append(media)
|
|
165
|
+
|
|
166
|
+
if len(medias) != len(rows):
|
|
167
|
+
UI.error(
|
|
168
|
+
f"From {len(rows)} rows, only converted {len(medias)}."
|
|
169
|
+
f" {len(rows) - len(medias)} error."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return medias
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def row_to_fused_media(row: sqlite3.Row) -> FusedMedia | None:
|
|
176
|
+
"""Convert a sqlite3 row into a FusedMedia model.
|
|
177
|
+
Return None if conversion fail"""
|
|
178
|
+
if row is None:
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
fuses_data = {}
|
|
182
|
+
for field in fields(FusedMedia):
|
|
183
|
+
field_name = field.name
|
|
184
|
+
if field_name in row.keys():
|
|
185
|
+
value = row[field_name]
|
|
186
|
+
if field_name == "status" and value is not None:
|
|
187
|
+
value = Status(value)
|
|
188
|
+
fuses_data[field_name] = value
|
|
189
|
+
return FusedMedia(**fuses_data)
|
|
190
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
191
|
+
UI.error(
|
|
192
|
+
f"Key/Type/Value Error: Failed to convert row "
|
|
193
|
+
f"{row['id']} into FusedMedia model due to {e}"
|
|
194
|
+
)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def rows_to_fuses(rows: list[sqlite3.Row]) -> list[FusedMedia]:
|
|
199
|
+
"""Convert a lsit of sqlite3 rows into a list of FusedMedia model
|
|
200
|
+
Ignore row if conversion failed"""
|
|
201
|
+
fuses: list[FusedMedia] = []
|
|
202
|
+
for row in rows:
|
|
203
|
+
fuse = row_to_fused_media(row)
|
|
204
|
+
if fuse is not None:
|
|
205
|
+
fuses.append(fuse)
|
|
206
|
+
|
|
207
|
+
if len(fuses) != len(rows):
|
|
208
|
+
UI.error(
|
|
209
|
+
f"From {len(rows)} rows, only converted {len(fuses)}."
|
|
210
|
+
f" {len(rows) - len(fuses)} error."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return fuses
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def post_to_videos(post: Post) -> list[Media]:
|
|
217
|
+
"""Extract a list of Media model from a Post model"""
|
|
218
|
+
json_post = json.loads(post.raw_json)
|
|
219
|
+
|
|
220
|
+
urls = parser.extract_video_urls(json_post)
|
|
221
|
+
sequence = 0
|
|
222
|
+
medias: list[Media] = []
|
|
223
|
+
for url in urls:
|
|
224
|
+
medias.append(
|
|
225
|
+
Media(
|
|
226
|
+
post_id=post.id,
|
|
227
|
+
service=post.service,
|
|
228
|
+
url=url,
|
|
229
|
+
duration=0.0,
|
|
230
|
+
sequence=sequence,
|
|
231
|
+
status=Status.PENDING,
|
|
232
|
+
checksum="",
|
|
233
|
+
file_path=parser.get_filename(json_post, url),
|
|
234
|
+
created_at="",
|
|
235
|
+
updated_at="",
|
|
236
|
+
file_size=0,
|
|
237
|
+
fail_count=0,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
sequence += 1
|
|
241
|
+
return medias
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# core/api.py
|
|
2
|
+
|
|
3
|
+
"""Build real URL for api request"""
|
|
4
|
+
|
|
5
|
+
from rcdl.core.models import Creator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class URL:
|
|
9
|
+
"""Build real URL for api request"""
|
|
10
|
+
|
|
11
|
+
DOMAINS_BASE_URL = {
|
|
12
|
+
"coomer": "https://coomer.st/api/v1/",
|
|
13
|
+
"kemono": "https://kemono.cr/api/v1/",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get_base_url(domain: str) -> str:
|
|
18
|
+
"""Return https://domain.com"""
|
|
19
|
+
if domain not in URL.DOMAINS_BASE_URL:
|
|
20
|
+
raise KeyError(f"{domain} not in known domains urls")
|
|
21
|
+
return URL.DOMAINS_BASE_URL[domain]
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def get_post_revision(creator: Creator, post_id) -> str:
|
|
25
|
+
"""Return post revision url"""
|
|
26
|
+
return (
|
|
27
|
+
f"{URL.get_base_url(creator.domain)}{creator.service}"
|
|
28
|
+
f"/user/{creator.id}/post/{post_id}/revisions"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def get_headers() -> dict:
|
|
33
|
+
"""Return necessary request header for successful request"""
|
|
34
|
+
return {
|
|
35
|
+
"User-Agent": (
|
|
36
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
37
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
38
|
+
"Chrome/117.0 Safari/537.36"
|
|
39
|
+
),
|
|
40
|
+
"Accept": "text/css",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def get_url_from_file(domain: str, path_url: str):
|
|
45
|
+
"""Add path_url to based domain url"""
|
|
46
|
+
if domain == "coomer":
|
|
47
|
+
return f"https://coomer.st{path_url}"
|
|
48
|
+
if domain == "kemono":
|
|
49
|
+
return f"https://kemono.cr{path_url}"
|
|
50
|
+
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Domain {domain} is not an accepted value/does not exist. "
|
|
53
|
+
f"Please check your creators.json file"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def add_params(url: str, params: dict):
|
|
58
|
+
"""Create all parameters string (key=params&key=...)"""
|
|
59
|
+
url += "?"
|
|
60
|
+
for key in params:
|
|
61
|
+
url += f"{key}={params[key]}&"
|
|
62
|
+
return url[:-1]
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_creator_post_wo_param(creator: Creator) -> str:
|
|
66
|
+
"""Get creator post without parameters"""
|
|
67
|
+
return (
|
|
68
|
+
f"{URL.get_base_url(creator.domain)}{creator.service}"
|
|
69
|
+
f"/user/{creator.id}/posts"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def get_posts_page_url_wo_param():
|
|
74
|
+
"""Get posts page without parameters -> use in tag search"""
|
|
75
|
+
domain = URL.DOMAINS_BASE_URL["coomer"]
|
|
76
|
+
return f"{domain}posts"
|