Skip to content

Commit bd7a118

Browse files
committed
Implement downloading picker responses from cobalt, update add instance function for the cli
1 parent 41518d5 commit bd7a118

File tree

5 files changed

+94
-229
lines changed

5 files changed

+94
-229
lines changed

pybalt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .misc.tracker import tracker
1212
from .misc.tracker import get_tracker
1313

14-
VERSION = "2025.5.5"
14+
VERSION = "2025.5.6"
1515

1616
# Initialize tracker
1717
tracker = get_tracker()

pybalt/__main__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def create_parser():
9898
api_group.add_argument("--api-stop", action="store_true", help="Stop the running API server")
9999
api_group.add_argument("--api-status", action="store_true", help="Check if the API server is running")
100100
api_group.add_argument("--api-port", type=int, help="Set the port for the API server")
101+
102+
# Misc arguments
103+
misc_group = parser.add_argument_group('Miscellaneous')
104+
misc_group.add_argument("-y", "--yes", action="store_true", help="Automatically answer yes to prompts")
101105

102106
return parser
103107

@@ -269,6 +273,14 @@ async def handle_instance_management(args):
269273
url = args.add_instance[0]
270274
api_key = args.add_instance[1] if len(args.add_instance) > 1 else ""
271275

276+
clean_url = url.strip().replace("http://", "").replace("https://", "")
277+
if clean_url in [instance.get("url", "").strip().replace("http://", "").replace("https://", "") for instance in cfg.get_user_instances()]:
278+
# If --yes flag is provided, skip confirmation
279+
if not args.yes:
280+
response = input("There's already an instance with the same URL in the config, still add? Skip the confirmation with --yes (Y/n): ").lower()
281+
if response not in ["", "y"]: # Default to yes if empty
282+
return
283+
272284
if api_key and api_key.lower() == "none":
273285
api_key = ""
274286

pybalt/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class Config:
8585
"max_filename_length": "32",
8686
"progress_bar_width": "20",
8787
"max_visible_items": "4",
88-
"draw_interval": "0.4",
88+
"draw_interval": "0.3",
8989
"min_redraw_interval": "0.1",
9090
},
9191
}

pybalt/core/wrapper.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
import asyncio
2121
from ipaddress import ip_address
22+
import os
2223

2324

2425
logger = get_logger(__name__)
@@ -571,7 +572,7 @@ async def download_generator(
571572
min_file_size: int = 1024, # Default 1KB minimum size
572573
max_retries: int = None, # Prevent infinite retry loops
573574
**params: Unpack[CobaltRequestParams],
574-
) -> AsyncGenerator[Path | Tuple[str, Optional[Path], Optional[Exception]], None]:
575+
) -> AsyncGenerator[Path | List[Path] | Tuple[str, Optional[Path | List[Path]], Optional[Exception]], None]:
575576
"""
576577
Download multiple files from Cobalt, yielding results as they complete.
577578
@@ -588,7 +589,7 @@ async def download_generator(
588589
Yields:
589590
Tuples of (url, file_path, exception) where:
590591
- url is the original URL requested
591-
- file_path is the Path to the downloaded file (None if failed)
592+
- file_path is the Path to the downloaded file, or if the response was a picker, a list of Paths (None if failed)
592593
- exception is the exception that occurred (None if successful)
593594
"""
594595
# Check if bulk download is allowed in config
@@ -695,8 +696,83 @@ async def process_url(url):
695696
logger.debug(f"Error remuxing file {file_path}: {e}")
696697

697698
return url, file_path, None
699+
elif response.get("status", "") == "picker":
700+
logger.debug(f"Picker response detected for {url} with {len(response.get('picker', []))} items")
701+
702+
# Get the instance that responded
703+
responding_instance = response.get("instance_info", {}).get("url")
704+
705+
# Handle picker response - download all items
706+
picker_items = response.get("picker", [])
707+
downloaded_paths = []
708+
download_errors = []
709+
710+
# Helper function to truncate long filenames while preserving extension
711+
def safe_filename(url_str, max_length=200):
712+
# Extract original filename from URL
713+
basename = os.path.basename(url_str.split('?')[0])
714+
715+
# Split into name and extension
716+
name, ext = os.path.splitext(basename)
717+
718+
# If filename is too long, truncate the name part
719+
if len(basename) > max_length:
720+
# Make sure we leave enough room for the extension
721+
truncated_name = name[:max_length - len(ext) - 1]
722+
return f"{truncated_name}{ext}"
723+
return basename
724+
725+
# Download each picker item
726+
for idx, item in enumerate(picker_items):
727+
item_url = item.get("url")
728+
if not item_url:
729+
continue
730+
731+
# Create a descriptive filename that's not too long
732+
item_type = item.get("type", "media")
733+
# Generate a filename with an index to keep items in order
734+
raw_filename = safe_filename(item_url)
735+
item_filename = f"{url.split('/')[-1].split('?')[0][:30]}_{item_type}_{idx+1:02d}_{raw_filename[-40:]}"
736+
737+
try:
738+
# Create a download task for this item
739+
download_task = await self.client.detached_download(
740+
url=item_url,
741+
filename=item_filename,
742+
timeout=self.config.get("download_timeout", 60),
743+
progressive_timeout=True,
744+
)
745+
746+
file_path = await download_task
747+
748+
# Check if file is a "ghost file" (too small)
749+
if file_path and file_path.exists():
750+
file_size = file_path.stat().st_size
751+
if file_size < min_file_size:
752+
logger.warning(f"Ghost file detected for picker item {idx+1}: {file_path} ({file_size} bytes)")
753+
try:
754+
file_path.unlink()
755+
except Exception as e:
756+
logger.debug(f"Failed to delete ghost file {file_path}: {e}")
757+
continue
758+
759+
# Add to list of downloaded paths if successful
760+
if file_path:
761+
downloaded_paths.append(file_path)
762+
logger.debug(f"Downloaded picker item {idx+1}/{len(picker_items)}: {file_path}")
763+
except Exception as e:
764+
logger.debug(f"Failed to download picker item {idx+1}: {e}")
765+
download_errors.append(e)
766+
767+
# If we downloaded at least one file, consider it a success
768+
if downloaded_paths:
769+
return url, downloaded_paths, None
770+
else:
771+
# If all downloads failed, return an error
772+
error_msg = f"Failed to download any files from picker response ({len(download_errors)} errors)"
773+
return url, None, ValueError(error_msg)
698774
else:
699-
# Handle picker responses or other status types
775+
# Handle other status types
700776
error = ValueError(f"Unsupported response status: {response.get('status')}")
701777
return url, None, error
702778
except Exception as e:

0 commit comments

Comments
 (0)