Thaimozhi commited on
Commit ·
61ced71
1
Parent(s): 22d6ca1
feat: support Google Drive folder leeching by running gdown with --folder and matching files recursively
Browse files- plugins/file_rename.py +77 -47
plugins/file_rename.py
CHANGED
|
@@ -577,19 +577,22 @@ async def _download_url_with_progress(url: str, output_path: str, task: dict):
|
|
| 577 |
|
| 578 |
# ── Google Drive Intercept ──
|
| 579 |
if "drive.google.com" in url:
|
|
|
|
| 580 |
for attempt in range(1, DOWNLOAD_MAX_RETRIES + 1):
|
| 581 |
try:
|
| 582 |
task["retry_info"] = f"Initializing Google Drive (Attempt {attempt})" if attempt > 1 else None
|
| 583 |
-
|
|
|
|
| 584 |
|
| 585 |
# Use gdown in a temporary directory to auto-extract the real filename
|
| 586 |
tmp_gdrive_dir = output_path + "_gdrive_tmp"
|
| 587 |
os.makedirs(tmp_gdrive_dir, exist_ok=True)
|
| 588 |
|
| 589 |
-
gdown_cmd = [
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
|
|
|
| 593 |
proc = await asyncio.create_subprocess_exec(
|
| 594 |
*gdown_cmd, cwd=tmp_gdrive_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
| 595 |
)
|
|
@@ -600,17 +603,36 @@ async def _download_url_with_progress(url: str, output_path: str, task: dict):
|
|
| 600 |
raise asyncio.CancelledError()
|
| 601 |
|
| 602 |
if proc.returncode == 0:
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
shutil.rmtree(tmp_gdrive_dir, ignore_errors=True)
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
shutil.rmtree(tmp_gdrive_dir, ignore_errors=True)
|
| 613 |
-
|
| 614 |
err_text = stderr.decode('utf-8', errors='ignore')
|
| 615 |
raise RuntimeError(f"gdown failed with code {proc.returncode}: {err_text}")
|
| 616 |
|
|
@@ -992,14 +1014,17 @@ async def _run_leech_task(client, task: dict):
|
|
| 992 |
if source_type == "telegram_file":
|
| 993 |
await _download_telegram_file_with_progress(client, task["source_media_message"], temp_archive, task)
|
| 994 |
else:
|
| 995 |
-
await _download_url_with_progress(url, temp_archive, task)
|
|
|
|
|
|
|
| 996 |
if task.get("cancelled"):
|
| 997 |
raise asyncio.CancelledError()
|
| 998 |
|
| 999 |
# Re-read display_name AFTER download - Google Drive download updates task["display_name"]
|
| 1000 |
# with the real filename. Without this, the old URL-guessed name (e.g. "view") is used.
|
| 1001 |
new_display = task.get("display_name") or display_name
|
| 1002 |
-
if
|
|
|
|
| 1003 |
# Physical rename so the file's extension is correct on disk
|
| 1004 |
new_temp_archive = os.path.join(temp_dir, new_display)
|
| 1005 |
if os.path.exists(temp_archive) and temp_archive != new_temp_archive:
|
|
@@ -1008,38 +1033,46 @@ async def _run_leech_task(client, task: dict):
|
|
| 1008 |
temp_archive = new_temp_archive
|
| 1009 |
display_name = new_display
|
| 1010 |
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1018 |
|
| 1019 |
# Some releases ship a zip-inside-a-zip (or rar-inside-a-zip).
|
| 1020 |
# Recursively extract any nested archives we find (max 2 levels deep)
|
| 1021 |
# before scanning for media, so those aren't silently missed.
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
for
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
|
|
|
| 1039 |
|
| 1040 |
media_files = []
|
| 1041 |
all_extracted_files = []
|
| 1042 |
-
for root, _, files in os.walk(
|
| 1043 |
for name in files:
|
| 1044 |
path = os.path.join(root, name)
|
| 1045 |
all_extracted_files.append(path)
|
|
@@ -1047,12 +1080,12 @@ async def _run_leech_task(client, task: dict):
|
|
| 1047 |
media_files.append(path)
|
| 1048 |
if not media_files:
|
| 1049 |
logger.error(
|
| 1050 |
-
f"No media files matched.
|
| 1051 |
f"{[os.path.basename(p) for p in all_extracted_files[:30]]}"
|
| 1052 |
)
|
| 1053 |
raise RuntimeError(
|
| 1054 |
-
f"No supported media files found
|
| 1055 |
-
f"({len(all_extracted_files)} file(s)
|
| 1056 |
)
|
| 1057 |
auto_merge = await codeflixbots.get_auto_merge(task["user_id"])
|
| 1058 |
if auto_merge and len(media_files) > 1:
|
|
@@ -1067,10 +1100,7 @@ async def _run_leech_task(client, task: dict):
|
|
| 1067 |
task["user_id"],
|
| 1068 |
item,
|
| 1069 |
file_name,
|
| 1070 |
-
file_name,
|
| 1071 |
-
# display_name - previously every episode in a zip was
|
| 1072 |
-
# getting season/episode/quality extracted from the ZIP's
|
| 1073 |
-
# filename, so all episodes ended up with the same output name.
|
| 1074 |
task,
|
| 1075 |
task["chat_id"],
|
| 1076 |
task["status_message"],
|
|
|
|
| 577 |
|
| 578 |
# ── Google Drive Intercept ──
|
| 579 |
if "drive.google.com" in url:
|
| 580 |
+
is_folder = "/folders/" in url or "/drive/folders/" in url
|
| 581 |
for attempt in range(1, DOWNLOAD_MAX_RETRIES + 1):
|
| 582 |
try:
|
| 583 |
task["retry_info"] = f"Initializing Google Drive (Attempt {attempt})" if attempt > 1 else None
|
| 584 |
+
status_label = "Downloading (GDrive Folder)" if is_folder else "Downloading (GDrive)"
|
| 585 |
+
await _update_leech_status(task, status_label, force=True)
|
| 586 |
|
| 587 |
# Use gdown in a temporary directory to auto-extract the real filename
|
| 588 |
tmp_gdrive_dir = output_path + "_gdrive_tmp"
|
| 589 |
os.makedirs(tmp_gdrive_dir, exist_ok=True)
|
| 590 |
|
| 591 |
+
gdown_cmd = ["gdown"]
|
| 592 |
+
if is_folder:
|
| 593 |
+
gdown_cmd.append("--folder")
|
| 594 |
+
gdown_cmd.append(url)
|
| 595 |
+
|
| 596 |
proc = await asyncio.create_subprocess_exec(
|
| 597 |
*gdown_cmd, cwd=tmp_gdrive_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
| 598 |
)
|
|
|
|
| 603 |
raise asyncio.CancelledError()
|
| 604 |
|
| 605 |
if proc.returncode == 0:
|
| 606 |
+
if is_folder:
|
| 607 |
+
# Find all files recursively in tmp_gdrive_dir and move them to parent directory (temp_dir)
|
| 608 |
+
parent_dir = os.path.dirname(output_path)
|
| 609 |
+
moved_count = 0
|
| 610 |
+
for root, _, files_in_dir in os.walk(tmp_gdrive_dir):
|
| 611 |
+
for name in files_in_dir:
|
| 612 |
+
src_file = os.path.join(root, name)
|
| 613 |
+
dst_file = os.path.join(parent_dir, name)
|
| 614 |
+
# Avoid collision if the file exists
|
| 615 |
+
base, ext = os.path.splitext(name)
|
| 616 |
+
counter = 1
|
| 617 |
+
while os.path.exists(dst_file):
|
| 618 |
+
dst_file = os.path.join(parent_dir, f"{base}_{counter}{ext}")
|
| 619 |
+
counter += 1
|
| 620 |
+
shutil.move(src_file, dst_file)
|
| 621 |
+
moved_count += 1
|
| 622 |
shutil.rmtree(tmp_gdrive_dir, ignore_errors=True)
|
| 623 |
+
if moved_count > 0:
|
| 624 |
+
return parent_dir
|
| 625 |
+
else:
|
| 626 |
+
downloaded_files = os.listdir(tmp_gdrive_dir)
|
| 627 |
+
if downloaded_files:
|
| 628 |
+
true_name = downloaded_files[0]
|
| 629 |
+
# Overwrite the placeholder "view" name with the real Google Drive filename
|
| 630 |
+
task["display_name"] = true_name
|
| 631 |
+
shutil.move(os.path.join(tmp_gdrive_dir, true_name), output_path)
|
| 632 |
+
shutil.rmtree(tmp_gdrive_dir, ignore_errors=True)
|
| 633 |
+
return output_path
|
| 634 |
|
| 635 |
shutil.rmtree(tmp_gdrive_dir, ignore_errors=True)
|
|
|
|
| 636 |
err_text = stderr.decode('utf-8', errors='ignore')
|
| 637 |
raise RuntimeError(f"gdown failed with code {proc.returncode}: {err_text}")
|
| 638 |
|
|
|
|
| 1014 |
if source_type == "telegram_file":
|
| 1015 |
await _download_telegram_file_with_progress(client, task["source_media_message"], temp_archive, task)
|
| 1016 |
else:
|
| 1017 |
+
download_result = await _download_url_with_progress(url, temp_archive, task)
|
| 1018 |
+
if download_result and os.path.isdir(download_result):
|
| 1019 |
+
temp_archive = download_result
|
| 1020 |
if task.get("cancelled"):
|
| 1021 |
raise asyncio.CancelledError()
|
| 1022 |
|
| 1023 |
# Re-read display_name AFTER download - Google Drive download updates task["display_name"]
|
| 1024 |
# with the real filename. Without this, the old URL-guessed name (e.g. "view") is used.
|
| 1025 |
new_display = task.get("display_name") or display_name
|
| 1026 |
+
# Bypass rename if temp_archive is a directory
|
| 1027 |
+
if not os.path.isdir(temp_archive) and new_display != display_name:
|
| 1028 |
# Physical rename so the file's extension is correct on disk
|
| 1029 |
new_temp_archive = os.path.join(temp_dir, new_display)
|
| 1030 |
if os.path.exists(temp_archive) and temp_archive != new_temp_archive:
|
|
|
|
| 1033 |
temp_archive = new_temp_archive
|
| 1034 |
display_name = new_display
|
| 1035 |
|
| 1036 |
+
is_archive_file = _is_archive(temp_archive) if os.path.isfile(temp_archive) else False
|
| 1037 |
+
is_directory_download = os.path.isdir(temp_archive)
|
| 1038 |
+
|
| 1039 |
+
if is_archive_file or is_directory_download:
|
| 1040 |
+
task["is_extracting"] = True if is_archive_file else False
|
| 1041 |
+
if is_archive_file:
|
| 1042 |
+
await _update_leech_status(task, "Extracting")
|
| 1043 |
+
extracted_dir = os.path.join(temp_dir, "extracted")
|
| 1044 |
+
os.makedirs(extracted_dir, exist_ok=True)
|
| 1045 |
+
task["temp_paths"].append(extracted_dir)
|
| 1046 |
+
await _extract_archive(temp_archive, extracted_dir, task)
|
| 1047 |
+
scan_dir = extracted_dir
|
| 1048 |
+
else:
|
| 1049 |
+
scan_dir = temp_archive
|
| 1050 |
|
| 1051 |
# Some releases ship a zip-inside-a-zip (or rar-inside-a-zip).
|
| 1052 |
# Recursively extract any nested archives we find (max 2 levels deep)
|
| 1053 |
# before scanning for media, so those aren't silently missed.
|
| 1054 |
+
if is_archive_file:
|
| 1055 |
+
for _pass in range(2):
|
| 1056 |
+
nested = []
|
| 1057 |
+
for root, _, files in os.walk(scan_dir):
|
| 1058 |
+
for name in files:
|
| 1059 |
+
path = os.path.join(root, name)
|
| 1060 |
+
if _is_archive(path):
|
| 1061 |
+
nested.append(path)
|
| 1062 |
+
if not nested:
|
| 1063 |
+
break
|
| 1064 |
+
for nested_path in nested:
|
| 1065 |
+
try:
|
| 1066 |
+
nested_out = nested_path + "_extracted"
|
| 1067 |
+
os.makedirs(nested_out, exist_ok=True)
|
| 1068 |
+
await _extract_archive(nested_path, nested_out, task)
|
| 1069 |
+
os.remove(nested_path)
|
| 1070 |
+
except Exception as e:
|
| 1071 |
+
logger.warning(f"Could not extract nested archive {nested_path}: {e}")
|
| 1072 |
|
| 1073 |
media_files = []
|
| 1074 |
all_extracted_files = []
|
| 1075 |
+
for root, _, files in os.walk(scan_dir):
|
| 1076 |
for name in files:
|
| 1077 |
path = os.path.join(root, name)
|
| 1078 |
all_extracted_files.append(path)
|
|
|
|
| 1080 |
media_files.append(path)
|
| 1081 |
if not media_files:
|
| 1082 |
logger.error(
|
| 1083 |
+
f"No media files matched. Directory contains {len(all_extracted_files)} file(s): "
|
| 1084 |
f"{[os.path.basename(p) for p in all_extracted_files[:30]]}"
|
| 1085 |
)
|
| 1086 |
raise RuntimeError(
|
| 1087 |
+
f"No supported media files found "
|
| 1088 |
+
f"({len(all_extracted_files)} file(s) found, none matched known media extensions)"
|
| 1089 |
)
|
| 1090 |
auto_merge = await codeflixbots.get_auto_merge(task["user_id"])
|
| 1091 |
if auto_merge and len(media_files) > 1:
|
|
|
|
| 1100 |
task["user_id"],
|
| 1101 |
item,
|
| 1102 |
file_name,
|
| 1103 |
+
file_name,
|
|
|
|
|
|
|
|
|
|
| 1104 |
task,
|
| 1105 |
task["chat_id"],
|
| 1106 |
task["status_message"],
|