mfget 0.1.1__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.
- mediafire_dl/__init__.py +3 -0
- mediafire_dl/__main__.py +9 -0
- mediafire_dl/cli.py +112 -0
- mediafire_dl/downloader.py +61 -0
- mediafire_dl/errors.py +18 -0
- mediafire_dl/http.py +118 -0
- mediafire_dl/mediafire.py +159 -0
- mediafire_dl/models.py +30 -0
- mediafire_dl/output.py +172 -0
- mfget-0.1.1.dist-info/METADATA +235 -0
- mfget-0.1.1.dist-info/RECORD +15 -0
- mfget-0.1.1.dist-info/WHEEL +5 -0
- mfget-0.1.1.dist-info/entry_points.txt +2 -0
- mfget-0.1.1.dist-info/licenses/LICENSE +21 -0
- mfget-0.1.1.dist-info/top_level.txt +1 -0
mediafire_dl/__init__.py
ADDED
mediafire_dl/__main__.py
ADDED
mediafire_dl/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .downloader import Downloader
|
|
11
|
+
from .errors import MediafireDLError
|
|
12
|
+
from .http import HttpClient
|
|
13
|
+
from .mediafire import MediafireClient
|
|
14
|
+
from .output import RichUI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: list[str] | None = None) -> int:
|
|
18
|
+
args = parse_args(argv)
|
|
19
|
+
configure_logging(args.verbose)
|
|
20
|
+
|
|
21
|
+
ui = RichUI(Console())
|
|
22
|
+
link = args.link or ui.ask_link()
|
|
23
|
+
if not link.strip():
|
|
24
|
+
ui.error("Please paste a MediaFire file or folder link.")
|
|
25
|
+
return 1
|
|
26
|
+
destination = Path(args.output or ui.ask_save_dir(str(default_downloads_dir()))).expanduser()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
cookie_file = Path(args.cookies).expanduser() if args.cookies else None
|
|
30
|
+
http = HttpClient(cookie_file=cookie_file)
|
|
31
|
+
client = MediafireClient(http)
|
|
32
|
+
downloader = Downloader(http)
|
|
33
|
+
|
|
34
|
+
ui.status("Finding files...")
|
|
35
|
+
plan = client.build_plan(link)
|
|
36
|
+
if not plan.files:
|
|
37
|
+
ui.warning("No downloadable files were found.")
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
ui.show_plan(plan)
|
|
41
|
+
ui.status("Preparing downloads...")
|
|
42
|
+
|
|
43
|
+
if args.dry_run:
|
|
44
|
+
ui.warning("Dry run: nothing was downloaded.")
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
ui.status("Downloading...")
|
|
48
|
+
saved_to = downloader.download_plan(plan, destination, ui)
|
|
49
|
+
ui.finished(str(saved_to))
|
|
50
|
+
return 0
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
ui.error("Download cancelled.")
|
|
53
|
+
return 130
|
|
54
|
+
except MediafireDLError as exc:
|
|
55
|
+
if args.verbose:
|
|
56
|
+
logging.getLogger(__name__).exception("Mediafire-DL failed")
|
|
57
|
+
ui.error(str(exc))
|
|
58
|
+
return 1
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
if args.verbose:
|
|
61
|
+
logging.getLogger(__name__).exception("Unexpected failure")
|
|
62
|
+
ui.error(str(exc))
|
|
63
|
+
else:
|
|
64
|
+
ui.error("Something went wrong. Run again with --verbose for details.")
|
|
65
|
+
return 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
69
|
+
parser = argparse.ArgumentParser(
|
|
70
|
+
prog="mediafire-dl",
|
|
71
|
+
description="Download MediaFire files and folders with a clean progress UI.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument("link", nargs="?", help="MediaFire file or folder link")
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"-o",
|
|
76
|
+
"--output",
|
|
77
|
+
help="Folder to save downloads into. Defaults to your Downloads folder.",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--dry-run",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="List files without downloading them.",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--cookies",
|
|
86
|
+
help=(
|
|
87
|
+
"Path to a Netscape/Mozilla cookies.txt file to use for links your browser can access. "
|
|
88
|
+
"Cookies are read locally and are not saved by Mediafire-DL."
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--verbose",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="Show technical logs and tracebacks for troubleshooting.",
|
|
95
|
+
)
|
|
96
|
+
return parser.parse_args(argv)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def configure_logging(verbose: bool) -> None:
|
|
100
|
+
logging.basicConfig(
|
|
101
|
+
level=logging.DEBUG if verbose else logging.CRITICAL,
|
|
102
|
+
format="%(levelname)s: %(name)s: %(message)s",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def default_downloads_dir() -> Path:
|
|
107
|
+
downloads = Path.home() / "Downloads"
|
|
108
|
+
return downloads if downloads.exists() else Path.home()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .errors import DownloadLinkError, MediafireDLError
|
|
7
|
+
from .http import HttpClient
|
|
8
|
+
from .mediafire import safe_name
|
|
9
|
+
from .models import DownloadPlan, MediafireItem
|
|
10
|
+
from .output import UserInterface
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Downloader:
|
|
16
|
+
def __init__(self, http: HttpClient | None = None, chunk_size: int = 1024 * 128) -> None:
|
|
17
|
+
self.http = http or HttpClient()
|
|
18
|
+
self.chunk_size = chunk_size
|
|
19
|
+
|
|
20
|
+
def download_plan(self, plan: DownloadPlan, destination: Path, ui: UserInterface) -> Path:
|
|
21
|
+
root = destination / safe_name(plan.display_name) if plan.is_folder else destination
|
|
22
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
total = len(plan.files)
|
|
25
|
+
for index, item in enumerate(plan.files, start=1):
|
|
26
|
+
self._download_item(item, root, ui, index, total)
|
|
27
|
+
return root
|
|
28
|
+
|
|
29
|
+
def _download_item(
|
|
30
|
+
self,
|
|
31
|
+
item: MediafireItem,
|
|
32
|
+
root: Path,
|
|
33
|
+
ui: UserInterface,
|
|
34
|
+
index: int,
|
|
35
|
+
total: int,
|
|
36
|
+
) -> None:
|
|
37
|
+
target = root / Path(*item.relative_folder.parts) / safe_name(item.name)
|
|
38
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
partial = target.with_name(f"{target.name}.part")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
direct_url = self.http.find_public_download_url(item.page_url)
|
|
43
|
+
except MediafireDLError:
|
|
44
|
+
raise
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
LOGGER.debug("Unexpected error while finding download URL", exc_info=exc)
|
|
47
|
+
raise DownloadLinkError(f"Could not prepare {item.name} for download.") from exc
|
|
48
|
+
|
|
49
|
+
with self.http.open_download(direct_url) as response:
|
|
50
|
+
length = response.headers.get("Content-Length")
|
|
51
|
+
total_size = int(length) if length and length.isdigit() else item.size
|
|
52
|
+
progress = ui.start_file(index, total, item, total_size)
|
|
53
|
+
with partial.open("wb") as file:
|
|
54
|
+
while True:
|
|
55
|
+
chunk = response.read(self.chunk_size)
|
|
56
|
+
if not chunk:
|
|
57
|
+
break
|
|
58
|
+
file.write(chunk)
|
|
59
|
+
progress.update(len(chunk))
|
|
60
|
+
progress.finish()
|
|
61
|
+
partial.replace(target)
|
mediafire_dl/errors.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class MediafireDLError(Exception):
|
|
2
|
+
"""Base exception for user-facing downloader errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UnsupportedLinkError(MediafireDLError):
|
|
6
|
+
"""Raised when a link is not a supported MediaFire file or folder URL."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MediafireAPIError(MediafireDLError):
|
|
10
|
+
"""Raised when MediaFire's public API returns an error."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DownloadLinkError(MediafireDLError):
|
|
14
|
+
"""Raised when a public download URL cannot be extracted."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CookieFileError(MediafireDLError):
|
|
18
|
+
"""Raised when a user-provided cookie file cannot be loaded."""
|
mediafire_dl/http.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from http.cookiejar import LoadError, MozillaCookieJar
|
|
6
|
+
from html import unescape
|
|
7
|
+
from html.parser import HTMLParser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.error import HTTPError, URLError
|
|
11
|
+
from urllib.parse import quote, urlencode
|
|
12
|
+
from urllib.request import HTTPCookieProcessor, Request, build_opener
|
|
13
|
+
|
|
14
|
+
from .errors import CookieFileError, DownloadLinkError, MediafireAPIError
|
|
15
|
+
|
|
16
|
+
LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
USER_AGENT = "Mediafire-DL/0.1 (+https://github.com/)"
|
|
19
|
+
API_BASE = "https://www.mediafire.com/api/1.5"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DownloadButtonParser(HTMLParser):
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.download_url: str | None = None
|
|
26
|
+
self.title: str | None = None
|
|
27
|
+
self._in_title = False
|
|
28
|
+
|
|
29
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
30
|
+
attr_map = dict(attrs)
|
|
31
|
+
if tag == "a" and attr_map.get("id") == "downloadButton":
|
|
32
|
+
href = attr_map.get("href")
|
|
33
|
+
if href:
|
|
34
|
+
self.download_url = unescape(href)
|
|
35
|
+
if tag == "title":
|
|
36
|
+
self._in_title = True
|
|
37
|
+
|
|
38
|
+
def handle_endtag(self, tag: str) -> None:
|
|
39
|
+
if tag == "title":
|
|
40
|
+
self._in_title = False
|
|
41
|
+
|
|
42
|
+
def handle_data(self, data: str) -> None:
|
|
43
|
+
if self._in_title and data.strip():
|
|
44
|
+
self.title = data.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HttpClient:
|
|
48
|
+
def __init__(self, timeout: int = 30, cookie_file: Path | None = None) -> None:
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
self.opener = build_opener()
|
|
51
|
+
if cookie_file:
|
|
52
|
+
self._load_cookies(cookie_file)
|
|
53
|
+
|
|
54
|
+
def _load_cookies(self, cookie_file: Path) -> None:
|
|
55
|
+
path = cookie_file.expanduser()
|
|
56
|
+
if not path.exists():
|
|
57
|
+
raise CookieFileError(f"Cookie file was not found: {path}")
|
|
58
|
+
if not path.is_file():
|
|
59
|
+
raise CookieFileError(f"Cookie path is not a file: {path}")
|
|
60
|
+
|
|
61
|
+
jar = MozillaCookieJar(str(path))
|
|
62
|
+
try:
|
|
63
|
+
jar.load(ignore_discard=True, ignore_expires=True)
|
|
64
|
+
except (LoadError, OSError) as exc:
|
|
65
|
+
LOGGER.debug("Could not load cookie file %s", path, exc_info=exc)
|
|
66
|
+
raise CookieFileError(
|
|
67
|
+
"Could not read the cookie file. Use a Netscape/Mozilla cookies.txt export."
|
|
68
|
+
) from exc
|
|
69
|
+
self.opener = build_opener(HTTPCookieProcessor(jar))
|
|
70
|
+
|
|
71
|
+
def api_get(self, endpoint: str, params: dict[str, str]) -> dict[str, Any]:
|
|
72
|
+
params = {**params, "response_format": "json"}
|
|
73
|
+
url = f"{API_BASE}/{endpoint}?{urlencode(params)}"
|
|
74
|
+
data = self.fetch_bytes(url)
|
|
75
|
+
try:
|
|
76
|
+
payload = json.loads(data.decode("utf-8"))
|
|
77
|
+
except json.JSONDecodeError as exc:
|
|
78
|
+
LOGGER.debug("Invalid JSON returned by MediaFire API", exc_info=exc)
|
|
79
|
+
raise MediafireAPIError("MediaFire returned an unreadable response.") from exc
|
|
80
|
+
|
|
81
|
+
response = payload.get("response", {})
|
|
82
|
+
result = response.get("result")
|
|
83
|
+
if result and result != "Success":
|
|
84
|
+
message = response.get("message") or "MediaFire reported an error."
|
|
85
|
+
raise MediafireAPIError(message)
|
|
86
|
+
return response
|
|
87
|
+
|
|
88
|
+
def fetch_text(self, url: str) -> str:
|
|
89
|
+
return self.fetch_bytes(url).decode("utf-8", errors="replace")
|
|
90
|
+
|
|
91
|
+
def fetch_bytes(self, url: str) -> bytes:
|
|
92
|
+
request = Request(url, headers={"User-Agent": USER_AGENT})
|
|
93
|
+
try:
|
|
94
|
+
with self.opener.open(request, timeout=self.timeout) as response:
|
|
95
|
+
return response.read()
|
|
96
|
+
except HTTPError as exc:
|
|
97
|
+
LOGGER.debug("HTTP error while fetching %s", url, exc_info=exc)
|
|
98
|
+
raise MediafireAPIError("MediaFire could not be reached right now.") from exc
|
|
99
|
+
except URLError as exc:
|
|
100
|
+
LOGGER.debug("Network error while fetching %s", url, exc_info=exc)
|
|
101
|
+
raise MediafireAPIError("Network connection failed while contacting MediaFire.") from exc
|
|
102
|
+
|
|
103
|
+
def open_download(self, url: str):
|
|
104
|
+
request = Request(url, headers={"User-Agent": USER_AGENT})
|
|
105
|
+
return self.opener.open(request, timeout=self.timeout)
|
|
106
|
+
|
|
107
|
+
def find_public_download_url(self, page_url: str) -> str:
|
|
108
|
+
html = self.fetch_text(page_url)
|
|
109
|
+
parser = DownloadButtonParser()
|
|
110
|
+
parser.feed(html)
|
|
111
|
+
if parser.download_url:
|
|
112
|
+
return parser.download_url
|
|
113
|
+
LOGGER.debug("downloadButton link was not found on %s", page_url)
|
|
114
|
+
raise DownloadLinkError("Could not find the public download button for a file.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def encoded_path_segment(value: str) -> str:
|
|
118
|
+
return quote(value.replace(" ", "_"), safe="")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from urllib.parse import unquote, urlparse
|
|
7
|
+
|
|
8
|
+
from .errors import MediafireAPIError, UnsupportedLinkError
|
|
9
|
+
from .http import HttpClient, encoded_path_segment
|
|
10
|
+
from .models import DownloadPlan, MediafireItem
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
FOLDER_RE = re.compile(r"/folder/([^/?#]+)")
|
|
15
|
+
FILE_RE = re.compile(r"/file/([^/?#]+)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MediafireClient:
|
|
19
|
+
def __init__(self, http: HttpClient | None = None) -> None:
|
|
20
|
+
self.http = http or HttpClient()
|
|
21
|
+
|
|
22
|
+
def build_plan(self, link: str) -> DownloadPlan:
|
|
23
|
+
kind, key = parse_mediafire_link(link)
|
|
24
|
+
if kind == "folder":
|
|
25
|
+
return self._build_folder_plan(key)
|
|
26
|
+
return self._build_file_plan(key, link)
|
|
27
|
+
|
|
28
|
+
def _build_folder_plan(self, folder_key: str) -> DownloadPlan:
|
|
29
|
+
folder_name = self._folder_name(folder_key) or "MediaFire Folder"
|
|
30
|
+
files = tuple(self._walk_folder(folder_key, PurePosixPath()))
|
|
31
|
+
return DownloadPlan(display_name=folder_name, files=files, is_folder=True)
|
|
32
|
+
|
|
33
|
+
def _build_file_plan(self, quick_key: str, original_link: str) -> DownloadPlan:
|
|
34
|
+
info = self._file_info(quick_key)
|
|
35
|
+
filename = info.get("filename") or filename_from_url(original_link) or "MediaFire File"
|
|
36
|
+
page_url = info.get("links", {}).get("normal_download") or original_link
|
|
37
|
+
size = parse_int(info.get("size"))
|
|
38
|
+
item = MediafireItem(name=filename, page_url=page_url, size=size)
|
|
39
|
+
return DownloadPlan(display_name=filename, files=(item,), is_folder=False)
|
|
40
|
+
|
|
41
|
+
def _walk_folder(
|
|
42
|
+
self,
|
|
43
|
+
folder_key: str,
|
|
44
|
+
relative_folder: PurePosixPath,
|
|
45
|
+
) -> list[MediafireItem]:
|
|
46
|
+
items: list[MediafireItem] = []
|
|
47
|
+
|
|
48
|
+
chunk = 1
|
|
49
|
+
while True:
|
|
50
|
+
response = self.http.api_get(
|
|
51
|
+
"folder/get_content.php",
|
|
52
|
+
{
|
|
53
|
+
"folder_key": folder_key,
|
|
54
|
+
"content_type": "files",
|
|
55
|
+
"chunk_number": str(chunk),
|
|
56
|
+
"chunk_size": "100",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
content = response.get("folder_content", {})
|
|
60
|
+
for entry in content.get("files", []):
|
|
61
|
+
links = entry.get("links", {})
|
|
62
|
+
page_url = links.get("normal_download")
|
|
63
|
+
filename = entry.get("filename")
|
|
64
|
+
if not page_url or not filename:
|
|
65
|
+
LOGGER.debug("Skipping incomplete MediaFire file entry: %r", entry)
|
|
66
|
+
continue
|
|
67
|
+
items.append(
|
|
68
|
+
MediafireItem(
|
|
69
|
+
name=filename,
|
|
70
|
+
page_url=page_url,
|
|
71
|
+
size=parse_int(entry.get("size")),
|
|
72
|
+
relative_folder=relative_folder,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
if content.get("more_chunks") != "yes":
|
|
76
|
+
break
|
|
77
|
+
chunk += 1
|
|
78
|
+
|
|
79
|
+
chunk = 1
|
|
80
|
+
while True:
|
|
81
|
+
response = self.http.api_get(
|
|
82
|
+
"folder/get_content.php",
|
|
83
|
+
{
|
|
84
|
+
"folder_key": folder_key,
|
|
85
|
+
"content_type": "folders",
|
|
86
|
+
"chunk_number": str(chunk),
|
|
87
|
+
"chunk_size": "100",
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
content = response.get("folder_content", {})
|
|
91
|
+
for entry in content.get("folders", []):
|
|
92
|
+
child_key = entry.get("folderkey")
|
|
93
|
+
child_name = entry.get("name") or entry.get("foldername") or "Untitled Folder"
|
|
94
|
+
if not child_key:
|
|
95
|
+
LOGGER.debug("Skipping incomplete MediaFire folder entry: %r", entry)
|
|
96
|
+
continue
|
|
97
|
+
items.extend(self._walk_folder(child_key, relative_folder / safe_name(child_name)))
|
|
98
|
+
if content.get("more_chunks") != "yes":
|
|
99
|
+
break
|
|
100
|
+
chunk += 1
|
|
101
|
+
|
|
102
|
+
return items
|
|
103
|
+
|
|
104
|
+
def _folder_name(self, folder_key: str) -> str | None:
|
|
105
|
+
try:
|
|
106
|
+
response = self.http.api_get("folder/get_info.php", {"folder_key": folder_key})
|
|
107
|
+
except MediafireAPIError:
|
|
108
|
+
LOGGER.debug("Could not fetch folder name for %s", folder_key, exc_info=True)
|
|
109
|
+
return None
|
|
110
|
+
info = response.get("folder_info", {})
|
|
111
|
+
return info.get("name") or info.get("foldername")
|
|
112
|
+
|
|
113
|
+
def _file_info(self, quick_key: str) -> dict:
|
|
114
|
+
response = self.http.api_get("file/get_info.php", {"quick_key": quick_key})
|
|
115
|
+
return response.get("file_info", {})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_mediafire_link(link: str) -> tuple[str, str]:
|
|
119
|
+
parsed = urlparse(link.strip())
|
|
120
|
+
if not parsed.netloc or "mediafire.com" not in parsed.netloc.lower():
|
|
121
|
+
raise UnsupportedLinkError("Please provide a MediaFire file or folder link.")
|
|
122
|
+
|
|
123
|
+
path = unquote(parsed.path)
|
|
124
|
+
folder_match = FOLDER_RE.search(path)
|
|
125
|
+
if folder_match:
|
|
126
|
+
return "folder", folder_match.group(1)
|
|
127
|
+
|
|
128
|
+
file_match = FILE_RE.search(path)
|
|
129
|
+
if file_match:
|
|
130
|
+
return "file", file_match.group(1)
|
|
131
|
+
|
|
132
|
+
raise UnsupportedLinkError("Please provide a MediaFire file or folder link.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def filename_from_url(link: str) -> str | None:
|
|
136
|
+
parsed = urlparse(link)
|
|
137
|
+
pieces = [piece for piece in unquote(parsed.path).split("/") if piece]
|
|
138
|
+
if len(pieces) >= 3 and pieces[0] == "file":
|
|
139
|
+
return pieces[2].replace("_", " ")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def safe_name(name: str) -> str:
|
|
144
|
+
return name.replace("/", "-").replace("\\", "-").strip() or "Untitled"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def page_url_for_item(item: MediafireItem) -> str:
|
|
148
|
+
return item.page_url
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def api_file_page(quick_key: str, filename: str) -> str:
|
|
152
|
+
return f"https://www.mediafire.com/file/{quick_key}/{encoded_path_segment(filename)}/file"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_int(value: object) -> int | None:
|
|
156
|
+
try:
|
|
157
|
+
return int(str(value))
|
|
158
|
+
except (TypeError, ValueError):
|
|
159
|
+
return None
|
mediafire_dl/models.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class MediafireItem:
|
|
9
|
+
name: str
|
|
10
|
+
page_url: str
|
|
11
|
+
size: int | None = None
|
|
12
|
+
relative_folder: PurePosixPath = PurePosixPath()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def relative_path(self) -> PurePosixPath:
|
|
16
|
+
return self.relative_folder / self.name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class MediafireFolder:
|
|
21
|
+
name: str
|
|
22
|
+
key: str
|
|
23
|
+
files: tuple[MediafireItem, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class DownloadPlan:
|
|
28
|
+
display_name: str
|
|
29
|
+
files: tuple[MediafireItem, ...]
|
|
30
|
+
is_folder: bool
|
mediafire_dl/output.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import (
|
|
11
|
+
BarColumn,
|
|
12
|
+
DownloadColumn,
|
|
13
|
+
Progress,
|
|
14
|
+
SpinnerColumn,
|
|
15
|
+
TaskID,
|
|
16
|
+
TextColumn,
|
|
17
|
+
TimeRemainingColumn,
|
|
18
|
+
TransferSpeedColumn,
|
|
19
|
+
)
|
|
20
|
+
from rich.prompt import Prompt
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
from rich.tree import Tree
|
|
24
|
+
|
|
25
|
+
from .models import DownloadPlan, MediafireItem
|
|
26
|
+
|
|
27
|
+
MINT = "#7fffd4"
|
|
28
|
+
MINT_DIM = "#5fd6b5"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProgressHandle(ABC):
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def update(self, advance: int) -> None:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def finish(self) -> None:
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UserInterface(ABC):
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def ask_link(self) -> str:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def ask_save_dir(self, default: str) -> str:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def status(self, message: str) -> None:
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def show_plan(self, plan: DownloadPlan) -> None:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def start_file(self, index: int, total: int, item: MediafireItem, size: int | None) -> ProgressHandle:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def finished(self, path: str) -> None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def warning(self, message: str) -> None:
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def error(self, message: str) -> None:
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RichProgressHandle(ProgressHandle):
|
|
76
|
+
def __init__(self, progress: Progress, task_id: TaskID) -> None:
|
|
77
|
+
self.progress = progress
|
|
78
|
+
self.task_id = task_id
|
|
79
|
+
|
|
80
|
+
def update(self, advance: int) -> None:
|
|
81
|
+
self.progress.update(self.task_id, advance=advance)
|
|
82
|
+
|
|
83
|
+
def finish(self) -> None:
|
|
84
|
+
task = next(task for task in self.progress.tasks if task.id == self.task_id)
|
|
85
|
+
if task.total is not None:
|
|
86
|
+
self.progress.update(self.task_id, completed=task.total)
|
|
87
|
+
self.progress.stop_task(self.task_id)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class RichUI(UserInterface):
|
|
91
|
+
def __init__(self, console: Console | None = None) -> None:
|
|
92
|
+
self.console = console or Console()
|
|
93
|
+
self.progress = Progress(
|
|
94
|
+
SpinnerColumn(style=MINT),
|
|
95
|
+
TextColumn("[bold #7fffd4]{task.description}"),
|
|
96
|
+
BarColumn(bar_width=None, complete_style=MINT, finished_style=MINT, pulse_style=MINT_DIM),
|
|
97
|
+
DownloadColumn(),
|
|
98
|
+
TransferSpeedColumn(),
|
|
99
|
+
TimeRemainingColumn(),
|
|
100
|
+
console=self.console,
|
|
101
|
+
transient=False,
|
|
102
|
+
)
|
|
103
|
+
self._progress_started = False
|
|
104
|
+
|
|
105
|
+
def ask_link(self) -> str:
|
|
106
|
+
try:
|
|
107
|
+
return Prompt.ask("[#7fffd4]Paste a MediaFire file or folder link[/#7fffd4]").strip()
|
|
108
|
+
except EOFError:
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
def ask_save_dir(self, default: str) -> str:
|
|
112
|
+
try:
|
|
113
|
+
answer = Prompt.ask(
|
|
114
|
+
"[#7fffd4]Where should files be saved?[/#7fffd4]",
|
|
115
|
+
default=default,
|
|
116
|
+
).strip()
|
|
117
|
+
except EOFError:
|
|
118
|
+
return default
|
|
119
|
+
return answer or default
|
|
120
|
+
|
|
121
|
+
def status(self, message: str) -> None:
|
|
122
|
+
self.console.print(Text(message, style=MINT))
|
|
123
|
+
|
|
124
|
+
def show_plan(self, plan: DownloadPlan) -> None:
|
|
125
|
+
kind = "folder" if plan.is_folder else "file"
|
|
126
|
+
self.console.print(f"[#7fffd4]Found {kind}:[/#7fffd4] [bold]{plan.display_name}[/bold]")
|
|
127
|
+
count = len(plan.files)
|
|
128
|
+
label = "file" if count == 1 else "files"
|
|
129
|
+
self.console.print(f"[#7fffd4]Found {count} {label}:[/#7fffd4]")
|
|
130
|
+
self.console.print(build_file_tree(plan.files))
|
|
131
|
+
|
|
132
|
+
def start_file(self, index: int, total: int, item: MediafireItem, size: int | None) -> ProgressHandle:
|
|
133
|
+
if not self._progress_started:
|
|
134
|
+
self.progress.start()
|
|
135
|
+
self._progress_started = True
|
|
136
|
+
description = f"Downloading {index} of {total}: {item.name}"
|
|
137
|
+
task_id = self.progress.add_task(description, total=size)
|
|
138
|
+
return RichProgressHandle(self.progress, task_id)
|
|
139
|
+
|
|
140
|
+
def finished(self, path: str) -> None:
|
|
141
|
+
if self._progress_started:
|
|
142
|
+
self.progress.stop()
|
|
143
|
+
self._progress_started = False
|
|
144
|
+
table = Table.grid(padding=(0, 1))
|
|
145
|
+
table.add_column(style=MINT)
|
|
146
|
+
table.add_column()
|
|
147
|
+
table.add_row("Finished.", path)
|
|
148
|
+
self.console.print(Panel(table, border_style=MINT, box=box.ROUNDED))
|
|
149
|
+
|
|
150
|
+
def warning(self, message: str) -> None:
|
|
151
|
+
self.console.print(f"[yellow]{message}[/yellow]")
|
|
152
|
+
|
|
153
|
+
def error(self, message: str) -> None:
|
|
154
|
+
if self._progress_started:
|
|
155
|
+
self.progress.stop()
|
|
156
|
+
self._progress_started = False
|
|
157
|
+
self.console.print(f"[bold red]Error:[/bold red] {message}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_file_tree(files: Iterable[MediafireItem]) -> Tree:
|
|
161
|
+
root = Tree("[bold]Files[/bold]", guide_style=MINT_DIM)
|
|
162
|
+
folder_nodes: dict[PurePosixPath, Tree] = {PurePosixPath(): root}
|
|
163
|
+
for item in sorted(files, key=lambda file: str(file.relative_path).lower()):
|
|
164
|
+
current_path = PurePosixPath()
|
|
165
|
+
current_node = root
|
|
166
|
+
for part in item.relative_folder.parts:
|
|
167
|
+
current_path = current_path / part
|
|
168
|
+
if current_path not in folder_nodes:
|
|
169
|
+
folder_nodes[current_path] = current_node.add(f"[#7fffd4]{part}/[/#7fffd4]")
|
|
170
|
+
current_node = folder_nodes[current_path]
|
|
171
|
+
current_node.add(item.name)
|
|
172
|
+
return root
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mfget
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A clean MediaFire file and folder downloader with a Rich-powered CLI.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/worstgirlinamerica/mediafire-dl
|
|
7
|
+
Project-URL: Repository, https://github.com/worstgirlinamerica/mediafire-dl
|
|
8
|
+
Project-URL: Issues, https://github.com/worstgirlinamerica/mediafire-dl/issues
|
|
9
|
+
Keywords: mediafire,downloader,cli,rich
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: rich>=13.7.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Mediafire-DL
|
|
27
|
+
|
|
28
|
+
Mediafire-DL is a small command-line downloader for MediaFire links.
|
|
29
|
+
|
|
30
|
+
It handles single files and folders, keeps nested folder structure intact, and shows a clean progress display while it downloads. Public links work without any login details. Links that require an account can be tried with an optional local cookies file.
|
|
31
|
+
|
|
32
|
+
## What It Does
|
|
33
|
+
|
|
34
|
+
- Downloads public MediaFire file links
|
|
35
|
+
- Downloads public MediaFire folder links
|
|
36
|
+
- Walks nested folders automatically
|
|
37
|
+
- Saves folders using the same folder layout MediaFire reports
|
|
38
|
+
- Shows a file preview before downloading
|
|
39
|
+
- Supports a dry-run mode so you can check what would be saved first
|
|
40
|
+
- Defaults to your system `Downloads` folder
|
|
41
|
+
- Can read a local `cookies.txt` file for links your own browser account can access
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
You need Python 3.9 or newer.
|
|
46
|
+
|
|
47
|
+
### From GitHub
|
|
48
|
+
|
|
49
|
+
This works without cloning the repo first:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m pip install --user git+https://github.com/worstgirlinamerica/mediafire-dl.git
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mediafire-dl
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
On Windows, use `py` instead of `python3`:
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
py -m pip install --user git+https://github.com/worstgirlinamerica/mediafire-dl.git
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### From PyPI
|
|
68
|
+
|
|
69
|
+
Once Mediafire-DL is published to PyPI as `mfget`, it can be installed like this:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python3 -m pip install --user mfget
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
On Windows:
|
|
76
|
+
|
|
77
|
+
```powershell
|
|
78
|
+
py -m pip install --user mfget
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### macOS
|
|
82
|
+
|
|
83
|
+
If you installed with Python 3:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mediafire-dl
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If your terminal says `mediafire-dl` was not found, use the module form:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3 -m mediafire_dl
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If the command is installed but your shell cannot find it, add Python's user script folder to your PATH. The version number may be different on your machine:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
echo 'export PATH="$HOME/Library/Python/3.12/bin:$PATH"' >> ~/.zshrc
|
|
99
|
+
source ~/.zshrc
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Windows
|
|
103
|
+
|
|
104
|
+
Run:
|
|
105
|
+
|
|
106
|
+
```powershell
|
|
107
|
+
mediafire-dl
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If PowerShell says `mediafire-dl` was not found, use:
|
|
111
|
+
|
|
112
|
+
```powershell
|
|
113
|
+
py -m mediafire_dl
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you want the `mediafire-dl` command to work directly, add Python's user Scripts folder to PATH. It usually looks like this, with the Python version adjusted for your install:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
%APPDATA%\Python\Python312\Scripts
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Linux
|
|
123
|
+
|
|
124
|
+
Run:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
mediafire-dl
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If your shell says `mediafire-dl` was not found, use:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python3 -m mediafire_dl
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or add Python's user script folder to your PATH:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
|
140
|
+
source ~/.bashrc
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Local Development
|
|
144
|
+
|
|
145
|
+
If you cloned the repo and want to install from the project folder:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python3 -m pip install --user .
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Usage
|
|
152
|
+
|
|
153
|
+
Run the command and paste a MediaFire link when it asks:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
mediafire-dl
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or pass the link directly:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
mediafire-dl "https://www.mediafire.com/file/example/file.zip/file"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Choose a save folder:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
mediafire-dl "https://www.mediafire.com/folder/example/My+Folder" --output ~/Downloads/MediaFire
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Preview a folder without downloading anything:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
mediafire-dl --dry-run "https://www.mediafire.com/folder/example/My+Folder"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Show more technical error details:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
mediafire-dl --verbose "https://www.mediafire.com/file/example/file.zip/file"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Use a local cookies file for a link that your browser account can already access:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
mediafire-dl --cookies ~/Downloads/cookies.txt "https://www.mediafire.com/file/example/file.zip/file"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The cookies file must be in Netscape/Mozilla `cookies.txt` format. Mediafire-DL only reads the file from your computer for that run.
|
|
190
|
+
|
|
191
|
+
## Supported Links
|
|
192
|
+
|
|
193
|
+
Supported:
|
|
194
|
+
|
|
195
|
+
- Public MediaFire file links
|
|
196
|
+
- Public MediaFire folder links
|
|
197
|
+
- Public folders with nested subfolders
|
|
198
|
+
- MediaFire file or folder links that work with a user-provided `cookies.txt` file
|
|
199
|
+
|
|
200
|
+
Not supported:
|
|
201
|
+
|
|
202
|
+
- Asking for or storing MediaFire usernames and passwords
|
|
203
|
+
- Automatically reading cookies from your browser
|
|
204
|
+
- Password-protected downloads that require an extra password prompt
|
|
205
|
+
- Captcha-gated or blocked downloads
|
|
206
|
+
|
|
207
|
+
## Privacy And Safety
|
|
208
|
+
|
|
209
|
+
Mediafire-DL does not ask for MediaFire login details and does not read browser cookies automatically. If you use `--cookies`, the cookie file stays on your machine and is only read by the current command.
|
|
210
|
+
|
|
211
|
+
This repo already ignores common private and generated files in `.gitignore`, including `.env`, cookies, logs, caches, virtual environments, `.DS_Store`, partial downloads, and local media/download folders.
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
For an editable install while working on the code:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
python3 -m pip install --user -e .
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Run the CLI from source:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
python3 -m mediafire_dl
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The package entry point is:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
mediafire-dl
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
mediafire_dl/__init__.py,sha256=3nx79VFXPQQQ-og2g4JU6mSIn7SGb9N0Gj9q_RjTOcI,51
|
|
2
|
+
mediafire_dl/__main__.py,sha256=pb2bv7Lw6q-BWUue8v6YGW8OXQEZ9V_cSmC9G70FtjE,120
|
|
3
|
+
mediafire_dl/cli.py,sha256=gBIt0-gb0OInATwQmtjSofwjGUMTqdweBD1QxcDj2SI,3388
|
|
4
|
+
mediafire_dl/downloader.py,sha256=4YMnvQ0qhfdjT7UWe3Hrc3uidRSQQCF3hIYIKJtjsGQ,2255
|
|
5
|
+
mediafire_dl/errors.py,sha256=wXTd0p38m5_yi3nceSkKT5VTSvc5G6RhLCH70k5ZUeg,550
|
|
6
|
+
mediafire_dl/http.py,sha256=xscB1biVVYSp2CrrMiIZQ7aj3-6jiT4dOosIrGg4vIo,4645
|
|
7
|
+
mediafire_dl/mediafire.py,sha256=IjWUMYsLJ_wcOqCndFMEXK1rO_639furDxEq5eVlGew,5838
|
|
8
|
+
mediafire_dl/models.py,sha256=4UWDpxVCCW50RF0HEK26Y0P2qDTq4FblyhEhss_t63s,611
|
|
9
|
+
mediafire_dl/output.py,sha256=tltsoTdg8M0LhB0frsKohqS1deXqoeIhkWsw0cctZrI,5641
|
|
10
|
+
mfget-0.1.1.dist-info/licenses/LICENSE,sha256=aKwPDohMJF34kmg5u90KPsjDHylRM7jzJCNsUcqjF-M,1082
|
|
11
|
+
mfget-0.1.1.dist-info/METADATA,sha256=S-tnWYJt_udjmVxVzpvmz7Ud87Nge4DcKJMZ9Ca-xQo,5713
|
|
12
|
+
mfget-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
mfget-0.1.1.dist-info/entry_points.txt,sha256=9QYSF6dCxPxBd2eNF23UvsY_oYCHpvsEy_0dgTWZzeQ,55
|
|
14
|
+
mfget-0.1.1.dist-info/top_level.txt,sha256=F8JY_qmbnVM54_aaJL1sKHmjq8RAWq6_dqL3gOo5P-I,13
|
|
15
|
+
mfget-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mediafire-DL 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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mediafire_dl
|