modiodirect 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheRootExec
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.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
@@ -0,0 +1,731 @@
1
+ #!/usr/bin/env python3
2
+ # ModioDirect by TheRootExec
3
+ # Direct Downloader for mod.io (official API only)
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ import time
10
+ import subprocess
11
+ from urllib.parse import urlparse, unquote
12
+
13
+ # Optional tqdm
14
+ try:
15
+ from tqdm import tqdm # type: ignore
16
+ except Exception:
17
+ tqdm = None
18
+
19
+ # Required requests (may be installed on demand)
20
+ try:
21
+ import requests # type: ignore
22
+ except Exception:
23
+ requests = None
24
+
25
+
26
+ API_BASE = "https://api.mod.io/v1"
27
+ CONFIG_NAME = "config.json"
28
+ USER_AGENT = "ModioDirect/1.1 (TheRootExec)"
29
+ DOWNLOAD_DIR = "downloads"
30
+ URL_REGEX = re.compile(
31
+ r"^https?://(?:www\.)?mod\.io/g/([^/]+)/m/([^/?#]+)",
32
+ re.IGNORECASE,
33
+ )
34
+
35
+
36
+ def print_error(msg):
37
+ print(f"[Error] {msg}")
38
+
39
+
40
+ def print_info(msg):
41
+ print(f"[Info] {msg}")
42
+
43
+
44
+ def print_banner():
45
+ print(r" __ __ _ _ _____ _ _ ")
46
+ print(r"| \/ | ___ __| (_) ___ | __ \(_)_ __ ___ ___| |_ ")
47
+ print(r"| |\/| |/ _ \ / _` | |/ _ \| | | | | '__/ _ \/ __| __|")
48
+ print(r"| | | | (_) | (_| | | (_) | |__| | | | | __/ (__| |_ ")
49
+ print(r"|_| |_|\___/ \__,_|_|\___/|_____/|_|_| \___|\___|\__|")
50
+ print("\n ModioDirect Downloader Tool")
51
+ print(" by TheRootExec")
52
+ print("-------------------------------------------------------")
53
+
54
+
55
+ def try_auto_install_requests():
56
+ global requests
57
+ if requests is not None:
58
+ return True
59
+ print_error("The 'requests' library is required but not installed.")
60
+ choice = input("Install requirements now? (y/n): ").strip().lower()
61
+ if choice != "y":
62
+ return False
63
+ try:
64
+ cmd = [sys.executable, "-m", "pip", "install", "requests"]
65
+ subprocess.run(cmd, check=False)
66
+ except Exception as exc:
67
+ print_error(f"Failed to run pip: {exc}")
68
+ return False
69
+ # Re-try import
70
+ try:
71
+ import requests as _requests # type: ignore
72
+ requests = _requests
73
+ return True
74
+ except Exception:
75
+ print_error("Requests is still not available after install attempt.")
76
+ return False
77
+
78
+
79
+ def safe_json(resp):
80
+ try:
81
+ return resp.json()
82
+ except Exception:
83
+ return None
84
+
85
+
86
+ def safe_request(method, url, **kwargs):
87
+ if requests is None:
88
+ print_error("The 'requests' library is not installed. Install it with: pip install requests")
89
+ return None
90
+ try:
91
+ return requests.request(method, url, **kwargs)
92
+ except Exception as exc:
93
+ print_error(f"Network error: {exc}")
94
+ return None
95
+
96
+
97
+ def load_config(config_path):
98
+ if not os.path.isfile(config_path):
99
+ return {}
100
+ try:
101
+ with open(config_path, "r", encoding="utf-8") as f:
102
+ data = json.load(f)
103
+ if isinstance(data, dict):
104
+ return data
105
+ except Exception:
106
+ pass
107
+ return {}
108
+
109
+
110
+ def save_config(config_path, data):
111
+ try:
112
+ with open(config_path, "w", encoding="utf-8") as f:
113
+ json.dump(data, f, indent=2)
114
+ return True
115
+ except Exception as exc:
116
+ print_error(f"Failed to save config: {exc}")
117
+ return False
118
+
119
+
120
+ def validate_api_key(api_key):
121
+ # Test API key by listing 1 game
122
+ url = f"{API_BASE}/games"
123
+ params = {"api_key": api_key, "limit": 1}
124
+ headers = {"User-Agent": USER_AGENT}
125
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
126
+ if resp is None:
127
+ return False, "Network error or requests missing."
128
+ if resp.status_code == 401:
129
+ return False, "Invalid API key (401 Unauthorized)."
130
+ if resp.status_code == 429:
131
+ return False, "Rate limited (429). Try again later."
132
+ if resp.status_code >= 400:
133
+ return False, f"API error ({resp.status_code})."
134
+ data = safe_json(resp)
135
+ if not isinstance(data, dict):
136
+ return False, "Empty or invalid API response."
137
+ return True, "API key validated."
138
+
139
+
140
+ def prompt_api_key(config_path, use_config):
141
+ config = {}
142
+ api_key = ""
143
+ if use_config:
144
+ config = load_config(config_path)
145
+ if isinstance(config, dict):
146
+ api_key = str(config.get("api_key", "")).strip()
147
+
148
+ while True:
149
+ if not api_key:
150
+ api_key = input("Enter your mod.io API key: ").strip()
151
+ if not api_key:
152
+ print_error("API key cannot be empty.")
153
+ continue
154
+
155
+ ok, msg = validate_api_key(api_key)
156
+ if ok:
157
+ print_info(msg)
158
+ if use_config:
159
+ config["api_key"] = api_key
160
+ save_config(config_path, config)
161
+ return api_key
162
+ print_error(msg)
163
+ api_key = "" # force re-prompt
164
+
165
+
166
+ def prompt_mod_url():
167
+ while True:
168
+ url = input("Enter mod.io mod URL (or 'q' to exit): ").strip()
169
+ if url.lower() in ("q", "quit", "exit"):
170
+ return None, None
171
+ if not url:
172
+ print_error("URL cannot be empty.")
173
+ continue
174
+ match = URL_REGEX.search(url)
175
+ if not match:
176
+ print_error("Invalid mod.io URL. Expected: https://mod.io/g/<game_slug>/m/<mod_slug>")
177
+ continue
178
+ game_slug = match.group(1).strip()
179
+ mod_slug = match.group(2).strip()
180
+ if not game_slug or not mod_slug:
181
+ print_error("Could not parse game_slug or mod_slug from URL.")
182
+ continue
183
+ return game_slug, mod_slug
184
+
185
+
186
+ def resolve_game_id(api_key, game_slug):
187
+ url = f"{API_BASE}/games"
188
+ # Filtering uses direct field parameters, e.g. name_id=<slug>
189
+ params = {"api_key": api_key, "name_id": game_slug, "limit": 1}
190
+ headers = {"User-Agent": USER_AGENT}
191
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
192
+ if resp is None:
193
+ return None, "Network error while resolving game."
194
+ if resp.status_code == 401:
195
+ return None, "Invalid API key (401 Unauthorized)."
196
+ if resp.status_code == 429:
197
+ return None, "Rate limited (429) while resolving game."
198
+ if resp.status_code >= 400:
199
+ return None, f"API error ({resp.status_code}) while resolving game."
200
+ data = safe_json(resp)
201
+ if not isinstance(data, dict):
202
+ return None, "Empty or invalid API response while resolving game."
203
+ items = data.get("data")
204
+ if not isinstance(items, list) or len(items) == 0:
205
+ # Fallback: search and match by name_id/slug
206
+ fallback_id, fallback_err = fallback_search_game_id(api_key, game_slug)
207
+ if fallback_id is not None:
208
+ return fallback_id, None
209
+ return None, fallback_err or "Game not found for provided game_slug."
210
+ # Never index without length check:
211
+ game = items[0] if len(items) > 0 else None
212
+ if not isinstance(game, dict):
213
+ return None, "Unexpected game data format."
214
+ game_id = game.get("id")
215
+ if not isinstance(game_id, int):
216
+ return None, "Missing game_id in API response."
217
+ return game_id, None
218
+
219
+
220
+ def match_slug(item, slug):
221
+ if not isinstance(item, dict):
222
+ return False
223
+ name_id = item.get("name_id")
224
+ if isinstance(name_id, str) and name_id.lower() == slug.lower():
225
+ return True
226
+ alt_slug = item.get("slug")
227
+ if isinstance(alt_slug, str) and alt_slug.lower() == slug.lower():
228
+ return True
229
+ return False
230
+
231
+
232
+ def fallback_search_game_id(api_key, game_slug):
233
+ url = f"{API_BASE}/games"
234
+ # _q is the documented full-text search parameter
235
+ params = {"api_key": api_key, "_q": game_slug, "limit": 100}
236
+ headers = {"User-Agent": USER_AGENT}
237
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
238
+ if resp is None:
239
+ return None, "Network error while searching game."
240
+ if resp.status_code == 401:
241
+ return None, "Invalid API key (401 Unauthorized)."
242
+ if resp.status_code == 429:
243
+ return None, "Rate limited (429) while searching game."
244
+ if resp.status_code >= 400:
245
+ return None, f"API error ({resp.status_code}) while searching game."
246
+ data = safe_json(resp)
247
+ if not isinstance(data, dict):
248
+ return None, "Empty or invalid API response while searching game."
249
+ items = data.get("data")
250
+ if not isinstance(items, list) or len(items) == 0:
251
+ return None, "Game not found for provided game_slug."
252
+ for item in items:
253
+ if match_slug(item, game_slug):
254
+ game_id = item.get("id") if isinstance(item, dict) else None
255
+ if isinstance(game_id, int):
256
+ return game_id, None
257
+ return None, "Game not found for provided game_slug."
258
+
259
+
260
+ def resolve_mod_id(api_key, game_id, mod_slug):
261
+ url = f"{API_BASE}/games/{game_id}/mods"
262
+ # Filtering uses direct field parameters, e.g. name_id=<slug>
263
+ params = {"api_key": api_key, "name_id": mod_slug, "limit": 1}
264
+ headers = {"User-Agent": USER_AGENT}
265
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
266
+ if resp is None:
267
+ return None, "Network error while resolving mod."
268
+ if resp.status_code == 401:
269
+ return None, "Invalid API key (401 Unauthorized)."
270
+ if resp.status_code == 429:
271
+ return None, "Rate limited (429) while resolving mod."
272
+ if resp.status_code == 404:
273
+ # Some API keys are restricted; try global mods endpoint as fallback
274
+ fallback_id, fallback_err = resolve_mod_id_global(api_key, game_id, mod_slug)
275
+ if fallback_id is not None:
276
+ return fallback_id, None
277
+ return None, fallback_err or "API returned 404 while resolving mod. The game or mod may be inaccessible with this API key."
278
+ if resp.status_code >= 400:
279
+ return None, f"API error ({resp.status_code}) while resolving mod."
280
+ data = safe_json(resp)
281
+ if not isinstance(data, dict):
282
+ return None, "Empty or invalid API response while resolving mod."
283
+ items = data.get("data")
284
+ if not isinstance(items, list) or len(items) == 0:
285
+ # Fallback: search and match by name_id/slug
286
+ fallback_id, fallback_err = fallback_search_mod_id(api_key, game_id, mod_slug)
287
+ if fallback_id is not None:
288
+ return fallback_id, None
289
+ return None, fallback_err or "Mod not found for provided mod_slug."
290
+ mod = items[0] if len(items) > 0 else None
291
+ if not isinstance(mod, dict):
292
+ return None, "Unexpected mod data format."
293
+ mod_id = mod.get("id")
294
+ if not isinstance(mod_id, int):
295
+ return None, "Missing mod_id in API response."
296
+ return mod_id, None
297
+
298
+
299
+ def resolve_mod_id_global(api_key, game_id, mod_slug):
300
+ url = f"{API_BASE}/mods"
301
+ params = {"api_key": api_key, "game_id": game_id, "name_id": mod_slug, "limit": 1}
302
+ headers = {"User-Agent": USER_AGENT}
303
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
304
+ if resp is None:
305
+ return None, "Network error while resolving mod (global)."
306
+ if resp.status_code == 401:
307
+ return None, "Invalid API key (401 Unauthorized)."
308
+ if resp.status_code == 429:
309
+ return None, "Rate limited (429) while resolving mod (global)."
310
+ if resp.status_code == 404:
311
+ # Try a global search by query and filter client-side
312
+ search_id, search_err = resolve_mod_id_global_search(api_key, game_id, mod_slug)
313
+ if search_id is not None:
314
+ return search_id, None
315
+ # If mod_slug is numeric, try direct mod lookup
316
+ numeric_id, numeric_err = resolve_mod_id_numeric(api_key, game_id, mod_slug)
317
+ if numeric_id is not None:
318
+ return numeric_id, None
319
+ return None, search_err or numeric_err or "API error (404) while resolving mod (global)."
320
+ if resp.status_code >= 400:
321
+ return None, f"API error ({resp.status_code}) while resolving mod (global)."
322
+ data = safe_json(resp)
323
+ if not isinstance(data, dict):
324
+ return None, "Empty or invalid API response while resolving mod (global)."
325
+ items = data.get("data")
326
+ if not isinstance(items, list) or len(items) == 0:
327
+ return None, "Mod not found for provided mod_slug."
328
+ mod = items[0] if len(items) > 0 else None
329
+ if not isinstance(mod, dict):
330
+ return None, "Unexpected mod data format."
331
+ mod_id = mod.get("id")
332
+ if not isinstance(mod_id, int):
333
+ return None, "Missing mod_id in API response."
334
+ return mod_id, None
335
+
336
+
337
+ def resolve_mod_id_global_search(api_key, game_id, mod_slug):
338
+ url = f"{API_BASE}/mods"
339
+ params = {"api_key": api_key, "_q": mod_slug, "limit": 100}
340
+ headers = {"User-Agent": USER_AGENT}
341
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
342
+ if resp is None:
343
+ return None, "Network error while searching mod (global)."
344
+ if resp.status_code == 401:
345
+ return None, "Invalid API key (401 Unauthorized)."
346
+ if resp.status_code == 429:
347
+ return None, "Rate limited (429) while searching mod (global)."
348
+ if resp.status_code >= 400:
349
+ return None, f"API error ({resp.status_code}) while searching mod (global)."
350
+ data = safe_json(resp)
351
+ if not isinstance(data, dict):
352
+ return None, "Empty or invalid API response while searching mod (global)."
353
+ items = data.get("data")
354
+ if not isinstance(items, list) or len(items) == 0:
355
+ return None, "Mod not found for provided mod_slug."
356
+ for item in items:
357
+ if not isinstance(item, dict):
358
+ continue
359
+ # Ensure game_id matches
360
+ item_game_id = item.get("game_id")
361
+ if isinstance(item_game_id, int) and item_game_id != game_id:
362
+ continue
363
+ if match_slug(item, mod_slug):
364
+ mod_id = item.get("id")
365
+ if isinstance(mod_id, int):
366
+ return mod_id, None
367
+ return None, "Mod not found for provided mod_slug."
368
+
369
+
370
+ def resolve_mod_id_numeric(api_key, game_id, mod_slug):
371
+ # If the slug is numeric, try direct mod ID
372
+ if not isinstance(mod_slug, str) or not mod_slug.isdigit():
373
+ return None, None
374
+ mod_id = int(mod_slug)
375
+ url = f"{API_BASE}/games/{game_id}/mods/{mod_id}"
376
+ params = {"api_key": api_key}
377
+ headers = {"User-Agent": USER_AGENT}
378
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
379
+ if resp is None:
380
+ return None, "Network error while resolving mod by numeric ID."
381
+ if resp.status_code == 401:
382
+ return None, "Invalid API key (401 Unauthorized)."
383
+ if resp.status_code == 429:
384
+ return None, "Rate limited (429) while resolving mod by numeric ID."
385
+ if resp.status_code >= 400:
386
+ return None, f"API error ({resp.status_code}) while resolving mod by numeric ID."
387
+ data = safe_json(resp)
388
+ if not isinstance(data, dict):
389
+ return None, "Empty or invalid API response while resolving mod by numeric ID."
390
+ mid = data.get("id")
391
+ if not isinstance(mid, int):
392
+ return None, "Missing mod_id in API response."
393
+ return mid, None
394
+
395
+
396
+ def fallback_search_mod_id(api_key, game_id, mod_slug):
397
+ url = f"{API_BASE}/games/{game_id}/mods"
398
+ # _q is the documented full-text search parameter
399
+ params = {"api_key": api_key, "_q": mod_slug, "limit": 100}
400
+ headers = {"User-Agent": USER_AGENT}
401
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
402
+ if resp is None:
403
+ return None, "Network error while searching mod."
404
+ if resp.status_code == 401:
405
+ return None, "Invalid API key (401 Unauthorized)."
406
+ if resp.status_code == 429:
407
+ return None, "Rate limited (429) while searching mod."
408
+ if resp.status_code >= 400:
409
+ return None, f"API error ({resp.status_code}) while searching mod."
410
+ data = safe_json(resp)
411
+ if not isinstance(data, dict):
412
+ return None, "Empty or invalid API response while searching mod."
413
+ items = data.get("data")
414
+ if not isinstance(items, list) or len(items) == 0:
415
+ return None, "Mod not found for provided mod_slug."
416
+ for item in items:
417
+ if match_slug(item, mod_slug):
418
+ mod_id = item.get("id") if isinstance(item, dict) else None
419
+ if isinstance(mod_id, int):
420
+ return mod_id, None
421
+ return None, "Mod not found for provided mod_slug."
422
+
423
+
424
+ def fetch_game_details(api_key, game_id):
425
+ url = f"{API_BASE}/games/{game_id}"
426
+ params = {"api_key": api_key}
427
+ headers = {"User-Agent": USER_AGENT}
428
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
429
+ if resp is None:
430
+ return None, "Network error while fetching game details."
431
+ if resp.status_code == 401:
432
+ return None, "Invalid API key (401 Unauthorized)."
433
+ if resp.status_code == 429:
434
+ return None, "Rate limited (429) while fetching game details."
435
+ if resp.status_code == 404:
436
+ return None, "Game not accessible (404). The game may be private, unpublished, or require OAuth access."
437
+ if resp.status_code >= 400:
438
+ return None, f"API error ({resp.status_code}) while fetching game details."
439
+ data = safe_json(resp)
440
+ if not isinstance(data, dict):
441
+ return None, "Empty or invalid API response while fetching game details."
442
+ return data, None
443
+
444
+
445
+ def fetch_mod_details(api_key, game_id, mod_id):
446
+ url = f"{API_BASE}/games/{game_id}/mods/{mod_id}"
447
+ params = {"api_key": api_key}
448
+ headers = {"User-Agent": USER_AGENT}
449
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=15)
450
+ if resp is None:
451
+ return None, "Network error while fetching mod details."
452
+ if resp.status_code == 401:
453
+ return None, "Invalid API key (401 Unauthorized)."
454
+ if resp.status_code == 429:
455
+ return None, "Rate limited (429) while fetching mod details."
456
+ if resp.status_code >= 400:
457
+ return None, f"API error ({resp.status_code}) while fetching mod details."
458
+ data = safe_json(resp)
459
+ if not isinstance(data, dict):
460
+ return None, "Empty or invalid API response while fetching mod details."
461
+ return data, None
462
+
463
+
464
+ def fetch_mod_files(api_key, game_id, mod_id):
465
+ url = f"{API_BASE}/games/{game_id}/mods/{mod_id}/files"
466
+ params = {"api_key": api_key, "limit": 100}
467
+ headers = {"User-Agent": USER_AGENT}
468
+ resp = safe_request("GET", url, params=params, headers=headers, timeout=20)
469
+ if resp is None:
470
+ return None, "Network error while fetching mod files."
471
+ if resp.status_code == 401:
472
+ return None, "Invalid API key (401 Unauthorized)."
473
+ if resp.status_code == 429:
474
+ return None, "Rate limited (429) while fetching mod files."
475
+ if resp.status_code >= 400:
476
+ return None, f"API error ({resp.status_code}) while fetching mod files."
477
+ data = safe_json(resp)
478
+ if not isinstance(data, dict):
479
+ return None, "Empty or invalid API response while fetching mod files."
480
+ items = data.get("data")
481
+ if not isinstance(items, list) or len(items) == 0:
482
+ return None, "No mod files found."
483
+ return items, None
484
+
485
+
486
+ def select_latest_file(files):
487
+ if not isinstance(files, list) or len(files) == 0:
488
+ return None
489
+ latest = None
490
+ latest_date = -1
491
+ for f in files:
492
+ if not isinstance(f, dict):
493
+ continue
494
+ date_added = f.get("date_added")
495
+ if isinstance(date_added, int) and date_added > latest_date:
496
+ latest_date = date_added
497
+ latest = f
498
+ return latest
499
+
500
+
501
+ def extract_download_info(file_obj):
502
+ if not isinstance(file_obj, dict):
503
+ return None, None
504
+ download = file_obj.get("download")
505
+ if not isinstance(download, dict):
506
+ return None, None
507
+ binary_url = download.get("binary_url")
508
+ if not isinstance(binary_url, str) or not binary_url.strip():
509
+ return None, None
510
+ # Fix escaped slashes
511
+ binary_url = binary_url.replace("\\/", "/")
512
+ # Determine filename
513
+ filename = file_obj.get("filename")
514
+ if not isinstance(filename, str) or not filename.strip():
515
+ # Try from URL
516
+ try:
517
+ parsed = urlparse(binary_url)
518
+ path = parsed.path or ""
519
+ basename = os.path.basename(path)
520
+ filename = unquote(basename) if basename else "modfile.bin"
521
+ except Exception:
522
+ filename = "modfile.bin"
523
+ return binary_url, filename
524
+
525
+
526
+ def download_file(url, filename):
527
+ headers = {"User-Agent": USER_AGENT}
528
+ # Attempt twice max
529
+ for attempt in range(1, 3):
530
+ resp = safe_request("GET", url, headers=headers, stream=True, timeout=30)
531
+ if resp is None:
532
+ print_error("Download failed due to network error.")
533
+ if attempt == 2:
534
+ return False
535
+ time.sleep(1)
536
+ continue
537
+ if resp.status_code == 429:
538
+ print_error("Rate limited (429) during download.")
539
+ if attempt == 2:
540
+ return False
541
+ time.sleep(2)
542
+ continue
543
+ if resp.status_code >= 400:
544
+ print_error(f"Download failed with status {resp.status_code}.")
545
+ if attempt == 2:
546
+ return False
547
+ time.sleep(1)
548
+ continue
549
+
550
+ total = resp.headers.get("Content-Length")
551
+ try:
552
+ total_bytes = int(total) if total is not None else None
553
+ except Exception:
554
+ total_bytes = None
555
+
556
+ try:
557
+ os.makedirs(DOWNLOAD_DIR, exist_ok=True)
558
+ filename = os.path.join(DOWNLOAD_DIR, filename)
559
+ with open(filename, "wb") as f:
560
+ if tqdm is not None and total_bytes is not None:
561
+ with tqdm(total=total_bytes, unit="B", unit_scale=True, desc="Downloading") as bar:
562
+ for chunk in resp.iter_content(chunk_size=1024 * 256):
563
+ if not chunk:
564
+ continue
565
+ f.write(chunk)
566
+ bar.update(len(chunk))
567
+ else:
568
+ downloaded = 0
569
+ last_print = 0
570
+ for chunk in resp.iter_content(chunk_size=1024 * 256):
571
+ if not chunk:
572
+ continue
573
+ f.write(chunk)
574
+ downloaded += len(chunk)
575
+ if total_bytes:
576
+ pct = int((downloaded / total_bytes) * 100)
577
+ if pct >= last_print + 5 or pct == 100:
578
+ print(f"Downloading... {pct}%")
579
+ last_print = pct
580
+ if total_bytes is None:
581
+ print_info("Download complete (size unknown).")
582
+ return True
583
+ except Exception as exc:
584
+ print_error(f"Failed to write file: {exc}")
585
+ if attempt == 2:
586
+ return False
587
+ time.sleep(1)
588
+ return False
589
+
590
+
591
+ def main():
592
+ print_banner()
593
+ if not try_auto_install_requests():
594
+ print_error("Cannot continue without 'requests'.")
595
+ return
596
+
597
+ config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_NAME)
598
+ use_config = "--no-config" not in sys.argv
599
+ api_key = prompt_api_key(config_path, use_config)
600
+
601
+ while True:
602
+ game_slug, mod_slug = prompt_mod_url()
603
+ if game_slug is None and mod_slug is None:
604
+ print_info("Thanks for using ModioDirect.")
605
+ return
606
+
607
+ game_id, err = resolve_game_id(api_key, game_slug)
608
+ if err:
609
+ print_error(err)
610
+ continue
611
+ game_details, gerr = fetch_game_details(api_key, game_id)
612
+ if gerr:
613
+ print_error(gerr)
614
+ continue
615
+ # Surface the resolved game name early
616
+ game_name = ""
617
+ if isinstance(game_details, dict):
618
+ name = game_details.get("name")
619
+ if isinstance(name, str):
620
+ game_name = name
621
+ if game_name:
622
+ print_info(f"Game : {game_name}")
623
+
624
+ mod_id, err = resolve_mod_id(api_key, game_id, mod_slug)
625
+ if err:
626
+ print_error(err)
627
+ continue
628
+ mod_details, merr = fetch_mod_details(api_key, game_id, mod_id)
629
+ if merr:
630
+ print_error(merr)
631
+ continue
632
+
633
+ mod_name = ""
634
+ if isinstance(mod_details, dict):
635
+ name = mod_details.get("name")
636
+ if isinstance(name, str):
637
+ mod_name = name
638
+
639
+ if mod_name:
640
+ print_info(f"Mod : {mod_name}")
641
+
642
+ files, err = fetch_mod_files(api_key, game_id, mod_id)
643
+ if err:
644
+ print_error(err)
645
+ continue
646
+
647
+ latest_file = select_latest_file(files)
648
+ if latest_file is None:
649
+ print_error("Could not determine latest mod file.")
650
+ continue
651
+
652
+ binary_url, filename = extract_download_info(latest_file)
653
+ if not binary_url:
654
+ print_error("No valid download URL found in mod file.")
655
+ continue
656
+
657
+ print_info(f"Latest file: {filename}")
658
+ ok = download_file(binary_url, filename)
659
+ if ok:
660
+ print_info(f"Saved as: {filename}")
661
+ # Optional JSON export
662
+ try:
663
+ os.makedirs(DOWNLOAD_DIR, exist_ok=True)
664
+ info_path = os.path.join(DOWNLOAD_DIR, "modinfo.json")
665
+ info = {
666
+ "game_name": game_name,
667
+ "mod_name": mod_name,
668
+ "mod_id": mod_id,
669
+ "file_id": latest_file.get("id") if isinstance(latest_file, dict) else None,
670
+ "date_downloaded": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
671
+ }
672
+ with open(info_path, "w", encoding="utf-8") as f:
673
+ json.dump(info, f, indent=2)
674
+ except Exception as exc:
675
+ print_error(f"Failed to write modinfo.json: {exc}")
676
+ else:
677
+ print_error("Download failed after retry.")
678
+
679
+ # Ask if user wants another
680
+ again = input("Download another mod? (y/n): ").strip().lower()
681
+ if again != "y":
682
+ print_info("Thanks for using ModioDirect.")
683
+ return
684
+
685
+
686
+ def maybe_pause_on_exit():
687
+ # Help Windows users who double-click the .py (console closes immediately).
688
+ if os.name != "nt":
689
+ return
690
+ if "--no-pause" in sys.argv:
691
+ return
692
+ # Only pause for direct script runs without extra args.
693
+ if len(sys.argv) > 1:
694
+ return
695
+ try:
696
+ input("Press Enter to exit...")
697
+ except Exception:
698
+ pass
699
+
700
+
701
+ if __name__ == "__main__":
702
+ try:
703
+ main()
704
+ except KeyboardInterrupt:
705
+ print_error("Interrupted by user.")
706
+ except Exception as exc:
707
+ print_error(f"Unexpected error: {exc}")
708
+ finally:
709
+ maybe_pause_on_exit()
710
+
711
+ """
712
+ Simulated full run (example):
713
+
714
+ ModioDirect - mod.io downloader
715
+ Enter your mod.io API key: 12345INVALID
716
+ [Error] Invalid API key (401 Unauthorized).
717
+ Enter your mod.io API key: 67890VALID
718
+ [Info] API key validated.
719
+ Enter mod.io mod URL: https://mod.io/g/spaceengineers/m/assault-weapons-pack1
720
+ [Info] Game : Space Engineers
721
+ [Info] Mod : Assault Weapons Pack
722
+ [Info] Latest file: assault_weapons_pack1.zip
723
+ Downloading... 5%
724
+ Downloading... 10%
725
+ ...
726
+ Downloading... 100%
727
+ [Info] Saved as: assault_weapons_pack1.zip
728
+ Download another mod? (y/n): n
729
+ [Info] Exiting.
730
+
731
+ """
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: modiodirect
3
+ Version: 1.0.0
4
+ Summary: Crash-proof direct downloader for mod.io (official API only)
5
+ Author: TheRootExec
6
+ License-Expression: MIT
7
+ Keywords: mod.io,mods,downloader,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests
21
+ Requires-Dist: tqdm
22
+ Dynamic: license-file
23
+
24
+ # ModioDirect v1.0.0 ![Python](https://img.shields.io/badge/python-3.9%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green)
25
+
26
+ **ModioDirect is a lightweight, single-file CLI that reliably downloads mods straight from mod.io via the official API—safe, fast, and free for use. It also allows manual downloads using an API key, bypassing the official game client.
27
+ Supports games like Space Engineers, SnowRunner, and more.
28
+
29
+ ![ModioDirectLogo1024x1024](https://github.com/user-attachments/assets/fc2687a6-61f1-42fb-bad2-57fa0df6fc73)
30
+
31
+
32
+
33
+
34
+ ## Features
35
+ - Official mod.io API only
36
+ - Validates API key
37
+ - Supports real mod.io URLs
38
+ - Fallback search when slugs fail
39
+ - Safe downloads with retries
40
+ - Progress bar (tqdm optional)
41
+ - Works on Windows & Linux
42
+ - Optional `--no-config` for shared machines
43
+
44
+ ## Requirements
45
+ - Python 3.9+
46
+ - `pip install requests tqdm`
47
+
48
+ ## How To Use It
49
+ ```bash
50
+ python modiodirect.py OR double click the modiodirect.py
51
+ ```
52
+ ## Add Your Mod.io API Key E.g
53
+ ```bash
54
+ 0923d9369664ba08bd91c67.........
55
+ ```
56
+ (optional) To avoid saving the API key to `config.json`:
57
+
58
+ ```bash
59
+ python modiodirect.py --no-config
60
+ ```
61
+
62
+ Paste a URL like:
63
+
64
+ ```
65
+ https://mod.io/g/GAME/m/example-mod
66
+ ```
67
+ ## :exclamation: Security Notice:
68
+ Your mod.io API key is private. Never share it or post it publicly.
69
+ ModioDirect stores the key locally and only uses it to communicate
70
+ with the official mod.io API.
71
+
72
+ # *SIMPLY WALKTHROUGH*:
73
+ <img width="1310" height="332" alt="Screenshot 2026-02-06 162920" src="https://github.com/user-attachments/assets/871142df-72c3-42b2-9655-f25d2b956488" />
74
+ <img width="1094" height="368" alt="Screenshot 2026-02-06 164436" src="https://github.com/user-attachments/assets/f351d3f7-8bc0-46b5-8c8b-1fe03af22332" />
75
+
76
+
77
+ ## Why this exists
78
+
79
+ Most mod.io download tools are broken, outdated, or unsafe—and frankly, frustrating to use. ModioDirect was made to fix that: simple, reliable, and safe. It gives you full control, letting you manually download mods with an API key and even bypass the official game client when needed. No clutter, no crashes, just the mods you want, when you want them.
80
+
81
+
82
+ ## Legal
83
+ This tool uses the official mod.io API. Users are responsible for complying with mod.io's Terms of Service.
84
+ This tool is not affiliated with, endorsed by, or officially supported by mod.io. Use at your own risk.
85
+
86
+ ## 🔴Access limitations (important)🔴
87
+
88
+ Some games/mods are private, unlisted, or require OAuth access. In those cases, the mod.io API returns 404 even if the URL exists. This is an access restriction, not a bug in ModioDirect.
89
+
90
+ If you see:
91
+
92
+ ```
93
+ [Error] Game not accessible (404). The game may be private, unpublished, or require OAuth access.
94
+ ```
95
+ <img width="1093" height="309" alt="Screenshot 2026-02-06 170047" src="https://github.com/user-attachments/assets/eb1148df-ef85-468f-a21b-d99cd26901db" />
96
+
97
+
98
+ API Key Limitations
99
+ Use a public game or mod to verify that your API key is working.
100
+ API keys can only access publicly available content.
101
+ Private or unlisted mods are not accessible using API keys alone, as they require OAuth-based authentication.
102
+ OAuth support is not currently implemented in ModioDirect. Future updates may add OAuth support if permitted by mod.io’s policies.
103
+
104
+ ## Upcoming Features Updates
105
+ ModioDirect is actively maintained. The following features are planned for future releases:
106
+ - Batch mod downloads
107
+ Download multiple mods in one run using a text file containing mod.io URLs.
108
+ - Optional auto-install to game mod folders
109
+ Detect common game mod directories (opt-in only, user confirmation required).
110
+ - Windows standalone executable (.exe)
111
+ A portable build for Windows users that does not require Python.
112
+ - PyPI package distribution
113
+ Install ModioDirect using pip install modiodirect and run it as a system command.
114
+
115
+
116
+ ## 🌟 Special Thanks
117
+
118
+ Thanks to [@Diversion-CTF](https://github.com/Diversion-CTF) For helping with the logo
119
+
120
+ ## 🤝 Contributions and feature requests are welcome
121
+ Please open an issue to discuss your ideas or suggestions.
122
+
@@ -0,0 +1,99 @@
1
+ # ModioDirect v1.0.0 ![Python](https://img.shields.io/badge/python-3.9%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green)
2
+
3
+ **ModioDirect is a lightweight, single-file CLI that reliably downloads mods straight from mod.io via the official API—safe, fast, and free for use. It also allows manual downloads using an API key, bypassing the official game client.
4
+ Supports games like Space Engineers, SnowRunner, and more.
5
+
6
+ ![ModioDirectLogo1024x1024](https://github.com/user-attachments/assets/fc2687a6-61f1-42fb-bad2-57fa0df6fc73)
7
+
8
+
9
+
10
+
11
+ ## Features
12
+ - Official mod.io API only
13
+ - Validates API key
14
+ - Supports real mod.io URLs
15
+ - Fallback search when slugs fail
16
+ - Safe downloads with retries
17
+ - Progress bar (tqdm optional)
18
+ - Works on Windows & Linux
19
+ - Optional `--no-config` for shared machines
20
+
21
+ ## Requirements
22
+ - Python 3.9+
23
+ - `pip install requests tqdm`
24
+
25
+ ## How To Use It
26
+ ```bash
27
+ python modiodirect.py OR double click the modiodirect.py
28
+ ```
29
+ ## Add Your Mod.io API Key E.g
30
+ ```bash
31
+ 0923d9369664ba08bd91c67.........
32
+ ```
33
+ (optional) To avoid saving the API key to `config.json`:
34
+
35
+ ```bash
36
+ python modiodirect.py --no-config
37
+ ```
38
+
39
+ Paste a URL like:
40
+
41
+ ```
42
+ https://mod.io/g/GAME/m/example-mod
43
+ ```
44
+ ## :exclamation: Security Notice:
45
+ Your mod.io API key is private. Never share it or post it publicly.
46
+ ModioDirect stores the key locally and only uses it to communicate
47
+ with the official mod.io API.
48
+
49
+ # *SIMPLY WALKTHROUGH*:
50
+ <img width="1310" height="332" alt="Screenshot 2026-02-06 162920" src="https://github.com/user-attachments/assets/871142df-72c3-42b2-9655-f25d2b956488" />
51
+ <img width="1094" height="368" alt="Screenshot 2026-02-06 164436" src="https://github.com/user-attachments/assets/f351d3f7-8bc0-46b5-8c8b-1fe03af22332" />
52
+
53
+
54
+ ## Why this exists
55
+
56
+ Most mod.io download tools are broken, outdated, or unsafe—and frankly, frustrating to use. ModioDirect was made to fix that: simple, reliable, and safe. It gives you full control, letting you manually download mods with an API key and even bypass the official game client when needed. No clutter, no crashes, just the mods you want, when you want them.
57
+
58
+
59
+ ## Legal
60
+ This tool uses the official mod.io API. Users are responsible for complying with mod.io's Terms of Service.
61
+ This tool is not affiliated with, endorsed by, or officially supported by mod.io. Use at your own risk.
62
+
63
+ ## 🔴Access limitations (important)🔴
64
+
65
+ Some games/mods are private, unlisted, or require OAuth access. In those cases, the mod.io API returns 404 even if the URL exists. This is an access restriction, not a bug in ModioDirect.
66
+
67
+ If you see:
68
+
69
+ ```
70
+ [Error] Game not accessible (404). The game may be private, unpublished, or require OAuth access.
71
+ ```
72
+ <img width="1093" height="309" alt="Screenshot 2026-02-06 170047" src="https://github.com/user-attachments/assets/eb1148df-ef85-468f-a21b-d99cd26901db" />
73
+
74
+
75
+ API Key Limitations
76
+ Use a public game or mod to verify that your API key is working.
77
+ API keys can only access publicly available content.
78
+ Private or unlisted mods are not accessible using API keys alone, as they require OAuth-based authentication.
79
+ OAuth support is not currently implemented in ModioDirect. Future updates may add OAuth support if permitted by mod.io’s policies.
80
+
81
+ ## Upcoming Features Updates
82
+ ModioDirect is actively maintained. The following features are planned for future releases:
83
+ - Batch mod downloads
84
+ Download multiple mods in one run using a text file containing mod.io URLs.
85
+ - Optional auto-install to game mod folders
86
+ Detect common game mod directories (opt-in only, user confirmation required).
87
+ - Windows standalone executable (.exe)
88
+ A portable build for Windows users that does not require Python.
89
+ - PyPI package distribution
90
+ Install ModioDirect using pip install modiodirect and run it as a system command.
91
+
92
+
93
+ ## 🌟 Special Thanks
94
+
95
+ Thanks to [@Diversion-CTF](https://github.com/Diversion-CTF) For helping with the logo
96
+
97
+ ## 🤝 Contributions and feature requests are welcome
98
+ Please open an issue to discuss your ideas or suggestions.
99
+
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: modiodirect
3
+ Version: 1.0.0
4
+ Summary: Crash-proof direct downloader for mod.io (official API only)
5
+ Author: TheRootExec
6
+ License-Expression: MIT
7
+ Keywords: mod.io,mods,downloader,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests
21
+ Requires-Dist: tqdm
22
+ Dynamic: license-file
23
+
24
+ # ModioDirect v1.0.0 ![Python](https://img.shields.io/badge/python-3.9%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green)
25
+
26
+ **ModioDirect is a lightweight, single-file CLI that reliably downloads mods straight from mod.io via the official API—safe, fast, and free for use. It also allows manual downloads using an API key, bypassing the official game client.
27
+ Supports games like Space Engineers, SnowRunner, and more.
28
+
29
+ ![ModioDirectLogo1024x1024](https://github.com/user-attachments/assets/fc2687a6-61f1-42fb-bad2-57fa0df6fc73)
30
+
31
+
32
+
33
+
34
+ ## Features
35
+ - Official mod.io API only
36
+ - Validates API key
37
+ - Supports real mod.io URLs
38
+ - Fallback search when slugs fail
39
+ - Safe downloads with retries
40
+ - Progress bar (tqdm optional)
41
+ - Works on Windows & Linux
42
+ - Optional `--no-config` for shared machines
43
+
44
+ ## Requirements
45
+ - Python 3.9+
46
+ - `pip install requests tqdm`
47
+
48
+ ## How To Use It
49
+ ```bash
50
+ python modiodirect.py OR double click the modiodirect.py
51
+ ```
52
+ ## Add Your Mod.io API Key E.g
53
+ ```bash
54
+ 0923d9369664ba08bd91c67.........
55
+ ```
56
+ (optional) To avoid saving the API key to `config.json`:
57
+
58
+ ```bash
59
+ python modiodirect.py --no-config
60
+ ```
61
+
62
+ Paste a URL like:
63
+
64
+ ```
65
+ https://mod.io/g/GAME/m/example-mod
66
+ ```
67
+ ## :exclamation: Security Notice:
68
+ Your mod.io API key is private. Never share it or post it publicly.
69
+ ModioDirect stores the key locally and only uses it to communicate
70
+ with the official mod.io API.
71
+
72
+ # *SIMPLY WALKTHROUGH*:
73
+ <img width="1310" height="332" alt="Screenshot 2026-02-06 162920" src="https://github.com/user-attachments/assets/871142df-72c3-42b2-9655-f25d2b956488" />
74
+ <img width="1094" height="368" alt="Screenshot 2026-02-06 164436" src="https://github.com/user-attachments/assets/f351d3f7-8bc0-46b5-8c8b-1fe03af22332" />
75
+
76
+
77
+ ## Why this exists
78
+
79
+ Most mod.io download tools are broken, outdated, or unsafe—and frankly, frustrating to use. ModioDirect was made to fix that: simple, reliable, and safe. It gives you full control, letting you manually download mods with an API key and even bypass the official game client when needed. No clutter, no crashes, just the mods you want, when you want them.
80
+
81
+
82
+ ## Legal
83
+ This tool uses the official mod.io API. Users are responsible for complying with mod.io's Terms of Service.
84
+ This tool is not affiliated with, endorsed by, or officially supported by mod.io. Use at your own risk.
85
+
86
+ ## 🔴Access limitations (important)🔴
87
+
88
+ Some games/mods are private, unlisted, or require OAuth access. In those cases, the mod.io API returns 404 even if the URL exists. This is an access restriction, not a bug in ModioDirect.
89
+
90
+ If you see:
91
+
92
+ ```
93
+ [Error] Game not accessible (404). The game may be private, unpublished, or require OAuth access.
94
+ ```
95
+ <img width="1093" height="309" alt="Screenshot 2026-02-06 170047" src="https://github.com/user-attachments/assets/eb1148df-ef85-468f-a21b-d99cd26901db" />
96
+
97
+
98
+ API Key Limitations
99
+ Use a public game or mod to verify that your API key is working.
100
+ API keys can only access publicly available content.
101
+ Private or unlisted mods are not accessible using API keys alone, as they require OAuth-based authentication.
102
+ OAuth support is not currently implemented in ModioDirect. Future updates may add OAuth support if permitted by mod.io’s policies.
103
+
104
+ ## Upcoming Features Updates
105
+ ModioDirect is actively maintained. The following features are planned for future releases:
106
+ - Batch mod downloads
107
+ Download multiple mods in one run using a text file containing mod.io URLs.
108
+ - Optional auto-install to game mod folders
109
+ Detect common game mod directories (opt-in only, user confirmation required).
110
+ - Windows standalone executable (.exe)
111
+ A portable build for Windows users that does not require Python.
112
+ - PyPI package distribution
113
+ Install ModioDirect using pip install modiodirect and run it as a system command.
114
+
115
+
116
+ ## 🌟 Special Thanks
117
+
118
+ Thanks to [@Diversion-CTF](https://github.com/Diversion-CTF) For helping with the logo
119
+
120
+ ## 🤝 Contributions and feature requests are welcome
121
+ Please open an issue to discuss your ideas or suggestions.
122
+
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ ModioDirect.py
4
+ README.md
5
+ pyproject.toml
6
+ modiodirect.egg-info/PKG-INFO
7
+ modiodirect.egg-info/SOURCES.txt
8
+ modiodirect.egg-info/dependency_links.txt
9
+ modiodirect.egg-info/entry_points.txt
10
+ modiodirect.egg-info/requires.txt
11
+ modiodirect.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ modiodirect = ModioDirect:main
@@ -0,0 +1,2 @@
1
+ requests
2
+ tqdm
@@ -0,0 +1 @@
1
+ ModioDirect
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "modiodirect"
7
+ version = "1.0.0"
8
+ description = "Crash-proof direct downloader for mod.io (official API only)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ {name = "TheRootExec"}
14
+ ]
15
+ keywords = ["mod.io", "mods", "downloader", "cli"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12"
26
+ ]
27
+ dependencies = [
28
+ "requests",
29
+ "tqdm"
30
+ ]
31
+
32
+ [project.scripts]
33
+ modiodirect = "ModioDirect:main"
34
+
35
+ [tool.setuptools]
36
+ py-modules = ["ModioDirect"]
37
+ include-package-data = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+