mpkgit 0.1.0__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.
- mpkgit-0.1.0/LICENSE +21 -0
- mpkgit-0.1.0/PKG-INFO +13 -0
- mpkgit-0.1.0/README.md +0 -0
- mpkgit-0.1.0/mpkg/__init__.py +1 -0
- mpkgit-0.1.0/mpkg/assets/byfms_pow_solver.js +148 -0
- mpkgit-0.1.0/mpkg/banner.py +14 -0
- mpkgit-0.1.0/mpkg/catalog.py +121 -0
- mpkgit-0.1.0/mpkg/cli.py +231 -0
- mpkgit-0.1.0/mpkg/config.py +36 -0
- mpkgit-0.1.0/mpkg/grab.py +323 -0
- mpkgit-0.1.0/mpkg/hosts/__init__.py +32 -0
- mpkgit-0.1.0/mpkg/hosts/byse.py +148 -0
- mpkgit-0.1.0/mpkg/hosts/mixdrop.py +42 -0
- mpkgit-0.1.0/mpkg/hosts/streamtape.py +40 -0
- mpkgit-0.1.0/mpkg/hosts/vidcloud.py +50 -0
- mpkgit-0.1.0/mpkg/hosts/vidmoly.py +45 -0
- mpkgit-0.1.0/mpkg/hosts/voe.py +51 -0
- mpkgit-0.1.0/mpkgit.egg-info/PKG-INFO +13 -0
- mpkgit-0.1.0/mpkgit.egg-info/SOURCES.txt +23 -0
- mpkgit-0.1.0/mpkgit.egg-info/dependency_links.txt +1 -0
- mpkgit-0.1.0/mpkgit.egg-info/entry_points.txt +2 -0
- mpkgit-0.1.0/mpkgit.egg-info/requires.txt +6 -0
- mpkgit-0.1.0/mpkgit.egg-info/top_level.txt +1 -0
- mpkgit-0.1.0/pyproject.toml +27 -0
- mpkgit-0.1.0/setup.cfg +4 -0
mpkgit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mpkg contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mpkgit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mpkgit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License: MIT
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: requests>=2.28
|
|
8
|
+
Requires-Dist: rich>=13
|
|
9
|
+
Requires-Dist: InquirerPy>=0.3
|
|
10
|
+
Requires-Dist: pycryptodome>=3.19
|
|
11
|
+
Requires-Dist: pyfiglet>=0.8
|
|
12
|
+
Requires-Dist: tqdm>=4.66
|
|
13
|
+
Dynamic: license-file
|
mpkgit-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
function api(url, opts = {}) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const u = new URL(url);
|
|
8
|
+
const body = opts.body || null;
|
|
9
|
+
const options = {
|
|
10
|
+
hostname: u.hostname,
|
|
11
|
+
port: 443,
|
|
12
|
+
path: u.pathname + u.search,
|
|
13
|
+
method: opts.method || 'GET',
|
|
14
|
+
headers: Object.assign({
|
|
15
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
16
|
+
'Accept': 'application/json, */*',
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
}, opts.headers || {}),
|
|
19
|
+
};
|
|
20
|
+
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
|
21
|
+
const req = https.request(options, res => {
|
|
22
|
+
let d = '';
|
|
23
|
+
res.on('data', c => d += c);
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(d), raw: d }); }
|
|
26
|
+
catch { resolve({ status: res.statusCode, body: null, raw: d }); }
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
req.on('error', reject);
|
|
30
|
+
if (body) req.write(body);
|
|
31
|
+
req.end();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const be = 512, lt = be - 1, dr = 2, lr = 2654435761, hr = 2246822519;
|
|
36
|
+
|
|
37
|
+
function re(t, e) { t >>>= 0; return ((t << e) | (t >>> (32 - e))) >>> 0; }
|
|
38
|
+
function ht(t, e) { return Math.imul(t, e) >>> 0; }
|
|
39
|
+
function ye(t) {
|
|
40
|
+
t[0] = (t[0] + t[1]) >>> 0; t[3] = re(t[3] ^ t[0], 16);
|
|
41
|
+
t[2] = (t[2] + t[3]) >>> 0; t[1] = re(t[1] ^ t[2], 12);
|
|
42
|
+
t[0] = (t[0] + t[1]) >>> 0; t[3] = re(t[3] ^ t[0], 8);
|
|
43
|
+
t[2] = (t[2] + t[3]) >>> 0; t[1] = re(t[1] ^ t[2], 7);
|
|
44
|
+
}
|
|
45
|
+
function yr(t) {
|
|
46
|
+
const e = new Uint8Array(t.length);
|
|
47
|
+
for (let r = 0; r < t.length; r++) e[r] = t.charCodeAt(r) & 255;
|
|
48
|
+
return e;
|
|
49
|
+
}
|
|
50
|
+
function gr(t) {
|
|
51
|
+
const e = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762]);
|
|
52
|
+
for (let i = 0; i < t.length; i++) { e[0] = (e[0] + t[i]) >>> 0; e[0] = re(e[0], 7); ye(e); }
|
|
53
|
+
for (let i = 0; i < 8; i++) ye(e);
|
|
54
|
+
const r = new Uint32Array(be);
|
|
55
|
+
for (let i = 0; i < be; i++) { ye(e); r[i] = (e[0] ^ e[2]) >>> 0; }
|
|
56
|
+
for (let i = 0; i < dr; i++)
|
|
57
|
+
for (let s = 0; s < be; s++) {
|
|
58
|
+
const a = r[s] & lt;
|
|
59
|
+
let c = (r[s] + r[a]) >>> 0;
|
|
60
|
+
c = re(c, 13);
|
|
61
|
+
c = (c ^ ht(r[(s + 1) & lt], lr)) >>> 0;
|
|
62
|
+
r[s] = c; e[0] = (e[0] ^ c) >>> 0; ye(e);
|
|
63
|
+
}
|
|
64
|
+
const n = new Uint32Array(8), o = be / 8;
|
|
65
|
+
for (let i = 0; i < 8; i++) {
|
|
66
|
+
ye(e); let s = e[0]; const a = i * o;
|
|
67
|
+
for (let c = 0; c < o; c++) {
|
|
68
|
+
const d = r[a + c];
|
|
69
|
+
s = (s + d) >>> 0; s = re(s, 5); s = (s ^ ht(d, hr)) >>> 0;
|
|
70
|
+
}
|
|
71
|
+
n[i] = (s ^ e[2]) >>> 0;
|
|
72
|
+
}
|
|
73
|
+
return n;
|
|
74
|
+
}
|
|
75
|
+
function wr(t) {
|
|
76
|
+
let e = 0;
|
|
77
|
+
for (let r = 0; r < t.length; r++) {
|
|
78
|
+
const n = t[r];
|
|
79
|
+
if (n === 0) { e += 32; continue; }
|
|
80
|
+
return e + Math.clz32(n);
|
|
81
|
+
}
|
|
82
|
+
return e;
|
|
83
|
+
}
|
|
84
|
+
async function Er(t, e, r = 120000) {
|
|
85
|
+
if (e <= 0) return '0';
|
|
86
|
+
const o = t + ':';
|
|
87
|
+
const i = Date.now();
|
|
88
|
+
let s = 0;
|
|
89
|
+
const a = 4096;
|
|
90
|
+
for (;;) {
|
|
91
|
+
for (let c = 0; c < a; c++) {
|
|
92
|
+
const d = gr(yr(o + s));
|
|
93
|
+
if (wr(d) >= e) return String(s);
|
|
94
|
+
s++;
|
|
95
|
+
}
|
|
96
|
+
if (Date.now() - i > r) return null;
|
|
97
|
+
await new Promise(res => setImmediate(res));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const [base, vid, origin] = process.argv.slice(2);
|
|
103
|
+
if (!base || !vid || !origin) {
|
|
104
|
+
console.log(JSON.stringify({ error: 'usage: node byfms_pow_solver.js <base_url> <video_id> <embed_origin>' }));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ch = await api(`${base}/api/videos/${vid}/embed/captcha`, { method: 'POST', body: '{}' });
|
|
109
|
+
if (!ch.body || !ch.body.pow_nonce) {
|
|
110
|
+
console.log(JSON.stringify({ error: 'captcha_failed', raw: (ch.raw || '').slice(0, 200) }));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { pow_nonce: nonce, pow_difficulty: difficulty, pow_token } = ch.body;
|
|
114
|
+
|
|
115
|
+
const solution = await Er(nonce, difficulty);
|
|
116
|
+
if (!solution) { console.log(JSON.stringify({ error: 'pow_timeout' })); return; }
|
|
117
|
+
|
|
118
|
+
const verif = await api(`${base}/api/videos/${vid}/embed/captcha/verify`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: JSON.stringify({ pow_token, solution }),
|
|
121
|
+
});
|
|
122
|
+
const access_token = verif.body && verif.body.status === 'ok' ? verif.body.token : null;
|
|
123
|
+
if (!access_token) {
|
|
124
|
+
console.log(JSON.stringify({ error: 'verify_failed', raw: (verif.raw || '').slice(0, 200) }));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const playRes = await api(`${base}/api/videos/${vid}/embed/playback`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'X-Captcha-Token': access_token,
|
|
132
|
+
'X-Embed-Origin': origin,
|
|
133
|
+
'X-Embed-Referer': origin + '/',
|
|
134
|
+
'X-Embed-Parent': origin + '/',
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({ fingerprint: {} }),
|
|
137
|
+
});
|
|
138
|
+
if (playRes.status !== 200 || !playRes.body) {
|
|
139
|
+
console.log(JSON.stringify({ error: 'playback_failed', status: playRes.status, raw: (playRes.raw || '').slice(0, 300) }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log(JSON.stringify(playRes.body));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main().catch(e => {
|
|
146
|
+
console.log(JSON.stringify({ error: e.message }));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import pyfiglet
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def b1() -> None:
|
|
5
|
+
try:
|
|
6
|
+
banner = pyfiglet.figlet_format("Mpkg", font="slant")
|
|
7
|
+
except Exception:
|
|
8
|
+
banner = "Mpkg\n"
|
|
9
|
+
print(banner)
|
|
10
|
+
print(" type Mpkg -h for help\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def b2() -> None:
|
|
14
|
+
print("\n[done] Grab complete. 🎬")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .hosts import h1, HostError
|
|
6
|
+
|
|
7
|
+
CATALOG_PATH = os.path.expanduser("~/.mpkg_catalog.json")
|
|
8
|
+
|
|
9
|
+
SEED = [
|
|
10
|
+
{"title": "Eddington (2025)", "host": "filemoon",
|
|
11
|
+
"url": "https://filemoon.to/e/ro2jnqsg89j0/2025_Eddington.mp4"},
|
|
12
|
+
{"title": "InkaSex - Patty Cherry New Year 2026", "host": "streamtape",
|
|
13
|
+
"url": "https://streamtape.com/v/p2bJ3gYOvZiXBG/x.mp4"},
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
HOST_LABEL = {
|
|
17
|
+
"filemoon": "Filemoon", "streamtape": "StreamTape", "voe": "VOE",
|
|
18
|
+
"vidmoly": "VidMoly", "mixdrop": "MixDrop", "vidcloud": "VidCloud",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def k1() -> list:
|
|
23
|
+
if not os.path.exists(CATALOG_PATH):
|
|
24
|
+
return []
|
|
25
|
+
try:
|
|
26
|
+
with open(CATALOG_PATH) as f:
|
|
27
|
+
data = json.load(f)
|
|
28
|
+
return [e for e in data if isinstance(e, dict) and e.get("url")]
|
|
29
|
+
except Exception:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def k2(entries: list) -> None:
|
|
34
|
+
with open(CATALOG_PATH, "w") as f:
|
|
35
|
+
json.dump(entries, f, indent=2)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def k3() -> list:
|
|
39
|
+
seen, out = set(), []
|
|
40
|
+
for e in SEED + k1():
|
|
41
|
+
if e["url"] in seen:
|
|
42
|
+
continue
|
|
43
|
+
seen.add(e["url"])
|
|
44
|
+
out.append(e)
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def k4(url: str, title: str = None, host: str = None) -> dict:
|
|
49
|
+
if host is None:
|
|
50
|
+
host = h1(url)
|
|
51
|
+
if title is None:
|
|
52
|
+
title = k7(url) or url
|
|
53
|
+
entries = k1()
|
|
54
|
+
for e in entries:
|
|
55
|
+
if e["url"] == url:
|
|
56
|
+
if title:
|
|
57
|
+
e["title"] = title
|
|
58
|
+
k2(entries)
|
|
59
|
+
return e
|
|
60
|
+
entry = {"title": title, "host": host, "url": url}
|
|
61
|
+
entries.append(entry)
|
|
62
|
+
k2(entries)
|
|
63
|
+
return entry
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def k5(text: str) -> int:
|
|
67
|
+
count = 0
|
|
68
|
+
for url in re.findall(r"https?://\S+", text):
|
|
69
|
+
url = url.rstrip(".,;)")
|
|
70
|
+
try:
|
|
71
|
+
k4(url)
|
|
72
|
+
count += 1
|
|
73
|
+
except HostError:
|
|
74
|
+
pass
|
|
75
|
+
return count
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def k6(url: str) -> bool:
|
|
79
|
+
entries = k1()
|
|
80
|
+
before = len(entries)
|
|
81
|
+
entries = [e for e in entries if e["url"] != url]
|
|
82
|
+
k2(entries)
|
|
83
|
+
return len(entries) != before
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def k7(url: str) -> str:
|
|
87
|
+
path = url.split("/", 3)[-1]
|
|
88
|
+
name = path.rsplit("/", 1)[-1]
|
|
89
|
+
name = re.sub(r"\.(mp4|mkv|webm|html?)$", "", name, flags=re.I)
|
|
90
|
+
name = re.sub(r"[\-_]+", " ", name).strip().title()
|
|
91
|
+
return name or None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def k8(query: str, index: list = None, limit: int = 20) -> list:
|
|
95
|
+
if index is None:
|
|
96
|
+
index = k3()
|
|
97
|
+
q = query.lower().strip()
|
|
98
|
+
q_words = set(q.split())
|
|
99
|
+
|
|
100
|
+
def score(e):
|
|
101
|
+
t = e["title"].lower()
|
|
102
|
+
if t.startswith(q):
|
|
103
|
+
return 3
|
|
104
|
+
if all(w in t for w in q_words):
|
|
105
|
+
return 2
|
|
106
|
+
if any(w in t for w in q_words):
|
|
107
|
+
return 1
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
results = [(score(e), e) for e in index if score(e) > 0]
|
|
111
|
+
results.sort(key=lambda x: (-x[0], x[1]["title"]))
|
|
112
|
+
return [e for _, e in results[:limit]]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def k9(index: list = None) -> dict:
|
|
116
|
+
if index is None:
|
|
117
|
+
index = k3()
|
|
118
|
+
out = {}
|
|
119
|
+
for e in index:
|
|
120
|
+
out.setdefault(HOST_LABEL.get(e["host"], e["host"]), []).append(e)
|
|
121
|
+
return out
|
mpkgit-0.1.0/mpkg/cli.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from InquirerPy import inquirer
|
|
11
|
+
from InquirerPy.base.control import Choice
|
|
12
|
+
|
|
13
|
+
from mpkg import __version__
|
|
14
|
+
from mpkg.banner import b1, b2
|
|
15
|
+
from mpkg.config import c1, c2, c3, CONFIG_PATH
|
|
16
|
+
from mpkg.catalog import k3, k4, k5, k8, k9, HOST_LABEL
|
|
17
|
+
from mpkg.hosts import h2, HostError
|
|
18
|
+
from mpkg.grab import g10, DEFAULT_THREADS, g2
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def l1(s: str) -> str:
|
|
24
|
+
s = re.sub(r'\.(mp4|mkv|webm|avi|m4v)$', '', s, flags=re.I)
|
|
25
|
+
return re.sub(r'[^\w\-. ]', '_', s).strip() or "video"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def l2() -> None:
|
|
29
|
+
missing = []
|
|
30
|
+
if not shutil.which("ffmpeg"):
|
|
31
|
+
missing.append("ffmpeg -> apt install ffmpeg / pkg install ffmpeg")
|
|
32
|
+
if not shutil.which("node"):
|
|
33
|
+
missing.append("node -> needed for Filemoon PoW; apt install nodejs")
|
|
34
|
+
if missing:
|
|
35
|
+
console.print("[bold red]Missing required tools:[/bold red]")
|
|
36
|
+
for m in missing:
|
|
37
|
+
console.print(f" [red]{m}[/red]")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def l3(catalog: list) -> None:
|
|
42
|
+
grouped = k9(catalog)
|
|
43
|
+
for host_label in sorted(grouped):
|
|
44
|
+
console.print(f"[bold cyan]{host_label}[/bold cyan]")
|
|
45
|
+
for e in grouped[host_label]:
|
|
46
|
+
console.print(f" {e['title']} [dim]{e['url'][:70]}[/dim]")
|
|
47
|
+
console.print()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def l4(url: str, title: str, dl_dir: str, threads: int, quality: str,
|
|
51
|
+
limit_minutes: int, retries: int = 1) -> bool:
|
|
52
|
+
out_path = os.path.join(dl_dir, l1(title) + ".mp4")
|
|
53
|
+
if os.path.exists(g2(out_path)):
|
|
54
|
+
console.print(f" [dim]Already done, skipping: {title}[/dim]")
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
console.print(f" [dim]Resolving {title} ({url[:70]}…)[/dim]")
|
|
58
|
+
for attempt in range(retries + 1):
|
|
59
|
+
try:
|
|
60
|
+
item = h2(url)
|
|
61
|
+
except HostError as e:
|
|
62
|
+
console.print(f" [yellow]resolve error: {e}[/yellow]")
|
|
63
|
+
break
|
|
64
|
+
except Exception as e:
|
|
65
|
+
console.print(f" [yellow]resolve error: {e}[/yellow]")
|
|
66
|
+
break
|
|
67
|
+
console.print(f" Media: {item['url'][:100]}")
|
|
68
|
+
ok = g10(item, out_path, threads=threads, quality=quality, limit_minutes=limit_minutes)
|
|
69
|
+
if ok:
|
|
70
|
+
return True
|
|
71
|
+
if attempt < retries:
|
|
72
|
+
console.print(f" [dim]Retrying ({attempt + 1}/{retries})…[/dim]")
|
|
73
|
+
time.sleep(2)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main() -> None:
|
|
78
|
+
b1()
|
|
79
|
+
l2()
|
|
80
|
+
cfg = c1()
|
|
81
|
+
|
|
82
|
+
parser = argparse.ArgumentParser(
|
|
83
|
+
prog="Mpkg",
|
|
84
|
+
description="Mpkg",
|
|
85
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument("-s", "--search", type=str, help="search")
|
|
88
|
+
parser.add_argument("-q", "--quality", type=str, default=None, help="quality")
|
|
89
|
+
parser.add_argument("-t", "--threads", type=int, default=None, help="threads")
|
|
90
|
+
parser.add_argument("-d", "--download-dir", type=str, default=None, help="dir")
|
|
91
|
+
parser.add_argument("--limit-minutes", type=int, default=0, help="preview")
|
|
92
|
+
parser.add_argument("--config", action="store_true", help="config")
|
|
93
|
+
parser.add_argument("--set-quality", type=str, metavar="Q", help="set quality")
|
|
94
|
+
parser.add_argument("--set-threads", type=int, metavar="N", help="set threads")
|
|
95
|
+
parser.add_argument("--set-download-dir", type=str, metavar="PATH", help="set dir")
|
|
96
|
+
parser.add_argument("--reset-config", action="store_true", help="reset config")
|
|
97
|
+
parser.add_argument("--version", action="store_true", help="version")
|
|
98
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
99
|
+
|
|
100
|
+
p_add = sub.add_parser("add", help="add url [title]")
|
|
101
|
+
p_add.add_argument("url")
|
|
102
|
+
p_add.add_argument("title", nargs="*")
|
|
103
|
+
|
|
104
|
+
p_import = sub.add_parser("import", help="import file")
|
|
105
|
+
p_import.add_argument("path")
|
|
106
|
+
|
|
107
|
+
p_list = sub.add_parser("list", help="list")
|
|
108
|
+
|
|
109
|
+
p_diag = sub.add_parser("diag", help="diag url")
|
|
110
|
+
p_diag.add_argument("url")
|
|
111
|
+
|
|
112
|
+
p_grab = sub.add_parser("grab", help="grab url")
|
|
113
|
+
p_grab.add_argument("url")
|
|
114
|
+
p_grab.add_argument("-q", "--quality", type=str, default=None, help="quality")
|
|
115
|
+
p_grab.add_argument("-t", "--threads", type=int, default=None, help="threads")
|
|
116
|
+
p_grab.add_argument("-d", "--download-dir", type=str, default=None, help="dir")
|
|
117
|
+
p_grab.add_argument("--limit-minutes", type=int, default=None, help="preview")
|
|
118
|
+
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
if args.version:
|
|
122
|
+
print(f"Mpkg {__version__}")
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
if args.config:
|
|
126
|
+
console.print(f"[bold cyan]Config[/bold cyan] ({CONFIG_PATH})")
|
|
127
|
+
for k, v in cfg.items():
|
|
128
|
+
console.print(f" {k}: [green]{v}[/green]")
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
|
|
131
|
+
if args.reset_config:
|
|
132
|
+
c3()
|
|
133
|
+
console.print("✓ Config reset to defaults.")
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
changed = False
|
|
137
|
+
if args.set_quality:
|
|
138
|
+
cfg["quality"], changed = args.set_quality, True
|
|
139
|
+
if args.set_threads:
|
|
140
|
+
cfg["threads"], changed = args.set_threads, True
|
|
141
|
+
if args.set_download_dir:
|
|
142
|
+
cfg["download_dir"], changed = os.path.normpath(args.set_download_dir), True
|
|
143
|
+
if changed:
|
|
144
|
+
c2(cfg)
|
|
145
|
+
console.print(f"✓ Config saved to {CONFIG_PATH}")
|
|
146
|
+
sys.exit(0)
|
|
147
|
+
|
|
148
|
+
quality = args.quality or cfg["quality"]
|
|
149
|
+
threads = args.threads or cfg.get("threads", DEFAULT_THREADS)
|
|
150
|
+
dl_dir = os.path.normpath(args.download_dir or cfg["download_dir"])
|
|
151
|
+
os.makedirs(dl_dir, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
if args.cmd == "add":
|
|
154
|
+
try:
|
|
155
|
+
entry = k4(args.url, title=" ".join(args.title) or None)
|
|
156
|
+
console.print(f"✓ Added: {entry['title']} ({HOST_LABEL.get(entry['host'], entry['host'])})")
|
|
157
|
+
except HostError as e:
|
|
158
|
+
console.print(f"[red]{e}[/red]")
|
|
159
|
+
sys.exit(0)
|
|
160
|
+
|
|
161
|
+
if args.cmd == "import":
|
|
162
|
+
if not os.path.exists(args.path):
|
|
163
|
+
console.print(f"[red]File not found: {args.path}[/red]")
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
n = k5(open(args.path).read())
|
|
166
|
+
console.print(f"✓ Imported {n} URL(s)")
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
if args.cmd == "diag":
|
|
170
|
+
console.print(f"Resolving {args.url}")
|
|
171
|
+
try:
|
|
172
|
+
item = h2(args.url)
|
|
173
|
+
console.print(f"[green]{item['kind']}[/green] {item['url']}")
|
|
174
|
+
except HostError as e:
|
|
175
|
+
console.print(f"[red]{e}[/red]")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
if args.cmd == "grab":
|
|
180
|
+
title = args.url.rsplit("/", 1)[-1][:60] or "video"
|
|
181
|
+
g_quality = getattr(args, "quality", None) or quality
|
|
182
|
+
g_threads = getattr(args, "threads", None) or threads
|
|
183
|
+
g_limit = getattr(args, "limit_minutes", None) or args.limit_minutes
|
|
184
|
+
g_dir = os.path.normpath(getattr(args, "download_dir", None) or dl_dir)
|
|
185
|
+
ok = l4(args.url, title, g_dir, g_threads, g_quality, g_limit)
|
|
186
|
+
sys.exit(0 if ok else 1)
|
|
187
|
+
|
|
188
|
+
if args.cmd == "list":
|
|
189
|
+
catalog = k3()
|
|
190
|
+
console.print(f"[bold]Catalog[/bold] — {len(catalog)} titles\n")
|
|
191
|
+
l3(catalog)
|
|
192
|
+
sys.exit(0)
|
|
193
|
+
|
|
194
|
+
catalog = k3()
|
|
195
|
+
if not catalog:
|
|
196
|
+
console.print("[yellow]Empty catalog — add URLs first:\n Mpkg add <embed-url> \"Title\"[/yellow]")
|
|
197
|
+
sys.exit(0)
|
|
198
|
+
|
|
199
|
+
if args.search:
|
|
200
|
+
results = k8(args.search, catalog)
|
|
201
|
+
if not results:
|
|
202
|
+
console.print(f"[bold yellow]No matches for '{args.search}'[/bold yellow]")
|
|
203
|
+
sys.exit(0)
|
|
204
|
+
else:
|
|
205
|
+
results = catalog
|
|
206
|
+
|
|
207
|
+
choices = [
|
|
208
|
+
Choice(value=e, name=f"[{HOST_LABEL.get(e['host'], e['host'])}] {e['title']}")
|
|
209
|
+
for e in results
|
|
210
|
+
]
|
|
211
|
+
selected = inquirer.fuzzy(
|
|
212
|
+
message="Pick a title:",
|
|
213
|
+
choices=choices,
|
|
214
|
+
instruction="(type to filter, Enter to select)",
|
|
215
|
+
).execute()
|
|
216
|
+
if not selected:
|
|
217
|
+
sys.exit(0)
|
|
218
|
+
|
|
219
|
+
console.print(f"\n[bold cyan]Title: {selected['title']}[/bold cyan]")
|
|
220
|
+
console.print(f" Lang/quality: {quality} | Threads: {threads} | Saving to: {dl_dir}")
|
|
221
|
+
|
|
222
|
+
ok = l4(selected["url"], selected["title"], dl_dir, threads, quality, args.limit_minutes)
|
|
223
|
+
if ok:
|
|
224
|
+
b2()
|
|
225
|
+
else:
|
|
226
|
+
console.print("[red]Grab failed — check the host / URL, or run Mpkg diag <url>[/red]")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
CONFIG_PATH = os.path.expanduser("~/.mpkg_config.json")
|
|
5
|
+
|
|
6
|
+
DEFAULT_CONFIG = {
|
|
7
|
+
"quality": "best",
|
|
8
|
+
"threads": 12,
|
|
9
|
+
"download_dir": os.path.join(os.path.expanduser("~"), "Movies"),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def c1() -> dict:
|
|
14
|
+
if not os.path.exists(CONFIG_PATH):
|
|
15
|
+
return DEFAULT_CONFIG.copy()
|
|
16
|
+
try:
|
|
17
|
+
with open(CONFIG_PATH) as f:
|
|
18
|
+
user = json.load(f)
|
|
19
|
+
cfg = DEFAULT_CONFIG.copy()
|
|
20
|
+
cfg.update({k: v for k, v in user.items() if k in DEFAULT_CONFIG})
|
|
21
|
+
return cfg
|
|
22
|
+
except Exception:
|
|
23
|
+
return DEFAULT_CONFIG.copy()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def c2(cfg: dict) -> None:
|
|
27
|
+
try:
|
|
28
|
+
with open(CONFIG_PATH, "w") as f:
|
|
29
|
+
json.dump(cfg, f, indent=2)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def c3() -> None:
|
|
35
|
+
if os.path.exists(CONFIG_PATH):
|
|
36
|
+
os.remove(CONFIG_PATH)
|