from flask import Flask, request, jsonify, render_template_string, send_file import requests import os import uuid import time import threading import m3u8 import logging import tempfile from datetime import datetime import io import json import shutil import subprocess from urllib.parse import urlparse, parse_qs, urlencode, urlunparse import re import zipfile app = Flask(__name__) # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Storage for active downloads using filesystem storage # Use a directory in /tmp which should be writable in most environments data_dir = os.path.join(tempfile.gettempdir(), 'm3u8_downloader_data') os.makedirs(data_dir, exist_ok=True) # Directory for extracted ZIP files zip_extract_dir = os.path.join(data_dir, 'extracted_zips') os.makedirs(zip_extract_dir, exist_ok=True) # In-memory reference to active downloads active_downloads = {} # Store information about extracted zip files extracted_zips = {} # Metadata file for persistence metadata_file = os.path.join(data_dir, 'downloads_metadata.json') zip_metadata_file = os.path.join(data_dir, 'zip_metadata.json') # Max chunk size for file uploads (2GB - 1 byte) MAX_CHUNK_SIZE = 2147483647 - 1024*1024 # Leave some buffer # Initialize downloads from metadata file def load_downloads_metadata(): if os.path.exists(metadata_file): try: with open(metadata_file, 'r') as f: return json.load(f) except Exception as e: logger.error(f"Error loading metadata: {e}") return {} # Save downloads metadata def save_downloads_metadata(): try: with open(metadata_file, 'w') as f: # Filter out file_data before saving metadata = {} for id, data in active_downloads.items(): metadata[id] = {k: v for k, v in data.items() if k != 'file_data'} json.dump(metadata, f) except Exception as e: logger.error(f"Error saving metadata: {e}") # Load zip metadata def load_zip_metadata(): if os.path.exists(zip_metadata_file): try: with open(zip_metadata_file, 'r') as f: return json.load(f) except Exception as e: logger.error(f"Error loading zip metadata: {e}") return {} # Save zip metadata def save_zip_metadata(): try: with open(zip_metadata_file, 'w') as f: json.dump(extracted_zips, f) except Exception as e: logger.error(f"Error saving zip metadata: {e}") # Initialize active downloads and extracted zips active_downloads = load_downloads_metadata() extracted_zips = load_zip_metadata() # Function to extract and process a ZIP file def process_zip_file(zip_url, zip_id): try: # Create directory for this zip extraction extract_dir = os.path.join(zip_extract_dir, zip_id) os.makedirs(extract_dir, exist_ok=True) # Download the ZIP file extracted_zips[zip_id]['status'] = 'downloading' save_zip_metadata() temp_zip_path = os.path.join(data_dir, f"{zip_id}.zip") try: response = requests.get(zip_url, stream=True) response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) extracted_zips[zip_id]['total_size'] = total_size bytes_downloaded = 0 with open(temp_zip_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) bytes_downloaded += len(chunk) if total_size > 0: progress = (bytes_downloaded / total_size) * 100 extracted_zips[zip_id]['progress'] = progress # Periodically save metadata if int(progress) % 5 == 0: save_zip_metadata() except Exception as e: logger.error(f"Error downloading ZIP file: {e}") extracted_zips[zip_id]['status'] = 'error' extracted_zips[zip_id]['error'] = str(e) save_zip_metadata() return # Extract the ZIP file extracted_zips[zip_id]['status'] = 'extracting' extracted_zips[zip_id]['progress'] = 0 save_zip_metadata() try: # Get the total number of files in the ZIP for progress tracking with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref: total_files = len(zip_ref.infolist()) extracted_zips[zip_id]['total_files'] = total_files extracted_files = [] # Extract each file and track progress for i, file_info in enumerate(zip_ref.infolist()): if not file_info.is_dir(): zip_ref.extract(file_info, extract_dir) extracted_files.append(os.path.join(extract_dir, file_info.filename)) progress = ((i + 1) / total_files) * 100 extracted_zips[zip_id]['progress'] = progress # Periodically save metadata if i % 10 == 0: save_zip_metadata() # Process the extracted files to create a list with file paths and types file_list = [] for root, dirs, files in os.walk(extract_dir): for file in files: full_path = os.path.join(root, file) rel_path = os.path.relpath(full_path, extract_dir) # Determine file type based on extension _, ext = os.path.splitext(file) ext = ext.lower() file_type = 'other' if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']: file_type = 'image' elif ext in ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.flv']: file_type = 'video' elif ext in ['.mp3', '.wav', '.ogg', '.m4a', '.flac']: file_type = 'audio' elif ext in ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.md']: file_type = 'document' elif ext in ['.html', '.htm']: file_type = 'html' file_list.append({ 'name': file, 'path': rel_path, 'full_path': full_path, 'type': file_type, 'size': os.path.getsize(full_path) }) # Save the file list extracted_zips[zip_id]['files'] = file_list extracted_zips[zip_id]['file_count'] = len(file_list) # Upload the ZIP file to jerrrycans extracted_zips[zip_id]['status'] = 'uploading' save_zip_metadata() # Get the original filename from the URL original_filename = os.path.basename(urlparse(zip_url).path) if not original_filename.endswith('.zip'): original_filename = f"archive_{zip_id}.zip" upload_url, error = upload_to_jerrrycans(temp_zip_path, original_filename) if upload_url: extracted_zips[zip_id]['upload_url'] = upload_url extracted_zips[zip_id]['status'] = 'completed' else: # Even if upload fails, we still have the extracted files extracted_zips[zip_id]['upload_error'] = error extracted_zips[zip_id]['status'] = 'completed' extracted_zips[zip_id]['progress'] = 100 save_zip_metadata() # Keep the temporary ZIP file for download extracted_zips[zip_id]['zip_path'] = temp_zip_path except Exception as e: logger.error(f"Error extracting ZIP file: {e}") extracted_zips[zip_id]['status'] = 'error' extracted_zips[zip_id]['error'] = str(e) save_zip_metadata() except Exception as e: logger.error(f"Overall error in process_zip_file: {e}") extracted_zips[zip_id]['status'] = 'error' extracted_zips[zip_id]['error'] = str(e) save_zip_metadata() # Home page with form @app.route('/') def index(): return render_template_string(INDEX_HTML, downloads=active_downloads, extracted_zips=extracted_zips) # Download file @app.route('/download/') def download_file(download_id): if download_id not in active_downloads: return jsonify({"error": "Download not found"}), 404 download = active_downloads[download_id] file_path = os.path.join(data_dir, f"{download_id}{download.get('extension', '.mp4')}") if not os.path.exists(file_path): return jsonify({"error": "File not found"}), 404 filename = download.get('filename', f"file_{download_id}{download.get('extension', '.mp4')}") return send_file( file_path, as_attachment=True, download_name=filename ) # Download ZIP file @app.route('/download_zip/') def download_zip_file(zip_id): if zip_id not in extracted_zips: return jsonify({"error": "ZIP not found"}), 404 zip_info = extracted_zips[zip_id] if 'zip_path' not in zip_info or not os.path.exists(zip_info['zip_path']): return jsonify({"error": "ZIP file not found"}), 404 # Get filename from the URL or use a default name original_url = zip_info.get('url', '') filename = os.path.basename(urlparse(original_url).path) if not filename.endswith('.zip'): filename = f"archive_{zip_id}.zip" return send_file( zip_info['zip_path'], as_attachment=True, download_name=filename ) # Serve extracted ZIP files @app.route('/zip_file//') def serve_zip_file(zip_id, file_path): if zip_id not in extracted_zips: return jsonify({"error": "ZIP extraction not found"}), 404 # Build the full file path full_path = os.path.join(zip_extract_dir, zip_id, file_path) if not os.path.exists(full_path): return jsonify({"error": "File not found"}), 404 # Get the filename for proper download filename = os.path.basename(file_path) return send_file( full_path, download_name=filename ) # Start download @app.route('/start_download', methods=['POST']) def start_download(): url = request.form.get('m3u8_url') if not url: return jsonify({"error": "No URL provided"}), 400 download_type = request.form.get('download_type', 'auto') # Default to auto-detection download_id = str(uuid.uuid4()) # Determine the type of URL url_lower = url.lower() # Check if it's an m3u8 url is_m3u8 = url_lower.endswith('.m3u8') or 'm3u8' in url_lower # Check if it's a ts file URL is_ts = url_lower.endswith('.ts') or re.search(r'seg-\d+-v\d+-a\d+\.ts', url_lower) # If explicit download type is specified, override the auto-detection if download_type == 'ts': is_ts = True is_m3u8 = False elif download_type == 'm3u8': is_ts = False is_m3u8 = True elif download_type == 'video': is_ts = False is_m3u8 = False # Create a new download task active_downloads[download_id] = { 'url': url, 'status': 'starting', 'progress': 0, 'start_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'paused': False, 'filename': None, 'extension': '.mp4', # Default extension 'type': 'ts' if is_ts else ('m3u8' if is_m3u8 else 'video'), 'segments_total': 0, 'segments_done': 0, 'upload_url': None } # Save metadata save_downloads_metadata() # Start download in background based on the type if is_ts: threading.Thread(target=download_ts_segments, args=(url, download_id)).start() elif is_m3u8: threading.Thread(target=download_m3u8, args=(url, download_id)).start() else: threading.Thread(target=download_video, args=(url, download_id)).start() return jsonify({ "message": "Download started", "download_id": download_id }) # Process a ZIP file URL @app.route('/process_zip', methods=['POST']) def process_zip(): zip_url = request.form.get('zip_url') if not zip_url: return jsonify({"error": "No ZIP URL provided"}), 400 zip_id = str(uuid.uuid4()) # Create a new ZIP extraction task extracted_zips[zip_id] = { 'url': zip_url, 'status': 'starting', 'progress': 0, 'start_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'error': None, 'files': [], 'file_count': 0, 'total_files': 0, 'total_size': 0, 'upload_url': None } # Save metadata save_zip_metadata() # Start processing in background threading.Thread(target=process_zip_file, args=(zip_url, zip_id)).start() return jsonify({ "message": "ZIP processing started", "zip_id": zip_id }) # Delete ZIP extraction @app.route('/delete_zip/', methods=['POST']) def delete_zip(zip_id): if zip_id not in extracted_zips: return jsonify({"error": "ZIP extraction not found"}), 404 # Remove the extraction directory if it exists extraction_dir = os.path.join(zip_extract_dir, zip_id) if os.path.exists(extraction_dir): try: shutil.rmtree(extraction_dir) except Exception as e: logger.error(f"Error deleting ZIP directory: {e}") # Remove the temporary ZIP file if it exists if 'zip_path' in extracted_zips[zip_id] and os.path.exists(extracted_zips[zip_id]['zip_path']): try: os.remove(extracted_zips[zip_id]['zip_path']) except Exception as e: logger.error(f"Error deleting ZIP file: {e}") # Remove from extracted zips del extracted_zips[zip_id] # Save metadata save_zip_metadata() return jsonify({"message": "ZIP extraction deleted"}) # Get ZIP extraction status @app.route('/zip_status/', methods=['GET']) def get_zip_status(zip_id): if zip_id not in extracted_zips: return jsonify({"error": "ZIP extraction not found"}), 404 return jsonify(extracted_zips[zip_id]) # Pause/Resume download @app.route('/toggle_pause/', methods=['POST']) def toggle_pause(download_id): if download_id not in active_downloads: return jsonify({"error": "Download not found"}), 404 active_downloads[download_id]['paused'] = not active_downloads[download_id]['paused'] status = "paused" if active_downloads[download_id]['paused'] else "resumed" # Save metadata save_downloads_metadata() return jsonify({"message": f"Download {status}", "status": status}) # Stop download @app.route('/stop_download/', methods=['POST']) def stop_download(download_id): if download_id not in active_downloads: return jsonify({"error": "Download not found"}), 404 active_downloads[download_id]['status'] = 'stopping' # Save metadata save_downloads_metadata() return jsonify({"message": "Download stopping"}) # Delete download @app.route('/delete_download/', methods=['POST']) def delete_download(download_id): if download_id not in active_downloads: return jsonify({"error": "Download not found"}), 404 # Get the file extension extension = active_downloads[download_id].get('extension', '.mp4') # Remove the file if it exists file_path = os.path.join(data_dir, f"{download_id}{extension}") if os.path.exists(file_path): try: os.remove(file_path) except Exception as e: logger.error(f"Error deleting file: {e}") # Remove from active downloads del active_downloads[download_id] # Save metadata save_downloads_metadata() return jsonify({"message": "Download deleted"}) # Get download status @app.route('/status/', methods=['GET']) def get_status(download_id): if download_id not in active_downloads: return jsonify({"error": "Download not found"}), 404 return jsonify(active_downloads[download_id]) # List all downloads @app.route('/downloads', methods=['GET']) def list_downloads(): return jsonify(active_downloads) # List all ZIP extractions @app.route('/zip_extractions', methods=['GET']) def list_zip_extractions(): return jsonify(extracted_zips) # Function to download and convert a direct video file def download_video(url, download_id): try: # Update status active_downloads[download_id]['status'] = 'downloading' save_downloads_metadata() # Get a unique filename parsed_url = urlparse(url) original_filename = os.path.basename(parsed_url.path) if not original_filename: original_filename = f"video_{int(time.time())}" # Get file extension, or use a generic one if not present file_ext = os.path.splitext(original_filename)[1] if not file_ext: file_ext = ".mp4" # Default extension # Generate unique filenames timestamp = int(time.time()) output_filename = f"{os.path.splitext(original_filename)[0]}_{timestamp}.mp4" # Temp file for download temp_file = os.path.join(tempfile.gettempdir(), f"temp_download_{download_id}{file_ext}") # Final output path final_output_path = os.path.join(data_dir, f"{download_id}.mp4") # Update filename in metadata active_downloads[download_id]['filename'] = output_filename save_downloads_metadata() # Download the file try: with requests.get(url, stream=True, timeout=60) as response: response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) active_downloads[download_id]['total_size'] = total_size bytes_downloaded = 0 with open(temp_file, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): # Check if download should be stopped if active_downloads[download_id]['status'] == 'stopping': active_downloads[download_id]['status'] = 'stopped' save_downloads_metadata() return # Check if download is paused while active_downloads[download_id]['paused']: time.sleep(1) if chunk: f.write(chunk) bytes_downloaded += len(chunk) if total_size > 0: progress = (bytes_downloaded / total_size) * 100 active_downloads[download_id]['progress'] = min(99, progress) # Periodically save metadata if int(progress) % 5 == 0: save_downloads_metadata() except Exception as e: logger.error(f"Error downloading video: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Download error: {str(e)}" save_downloads_metadata() return # Process the video active_downloads[download_id]['status'] = 'processing' save_downloads_metadata() try: # Convert to MP4 using FFmpeg ffmpeg_convert_cmd = [ 'ffmpeg', '-i', temp_file, '-c:v', 'libx264', '-preset', 'medium', # Balance between speed and quality '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-y', # Overwrite output file if it exists final_output_path ] subprocess.run(ffmpeg_convert_cmd, check=True) # Upload to jerrrycans active_downloads[download_id]['status'] = 'uploading' save_downloads_metadata() upload_url, error, chunk_urls = upload_to_jerrrycans(final_output_path, output_filename) if upload_url: active_downloads[download_id]['upload_url'] = upload_url if chunk_urls and len(chunk_urls) > 1: active_downloads[download_id]['chunk_urls'] = chunk_urls active_downloads[download_id]['status'] = 'completed' else: active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Upload failed: {error}" active_downloads[download_id]['progress'] = 100 save_downloads_metadata() # Clean up temp file if os.path.exists(temp_file): os.remove(temp_file) except Exception as e: logger.error(f"Error processing video: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Processing error: {str(e)}" save_downloads_metadata() except Exception as e: logger.error(f"Overall error in download_video: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = str(e) save_downloads_metadata() # Function to upload file to the hosting service - now with chunked uploading for large files def upload_to_jerrrycans(file_path, filename): try: # Get file size file_size = os.path.getsize(file_path) logger.info(f"Uploading file: {filename}, size: {file_size} bytes") # If file is smaller than the max chunk size, do a regular upload if file_size <= MAX_CHUNK_SIZE: upload_url, error = simple_upload_to_jerrrycans(file_path, filename) return upload_url, error, [upload_url] if upload_url else [] # For large files, do a chunked upload logger.info(f"File is large ({file_size} bytes), using chunked upload") return chunked_upload_to_jerrrycans(file_path, filename, file_size) except Exception as e: logger.error(f"Upload error: {e}") return None, str(e), [] # Simple upload for files under the max size def simple_upload_to_jerrrycans(file_path, filename): try: upload_url = "https://jerrrycans-file.hf.space/upload" # Ensure the file is properly read in binary mode with open(file_path, 'rb') as file_obj: file_content = file_obj.read() # Try to determine MIME type based on file extension mime_type = 'application/octet-stream' # Default MIME type file_ext = os.path.splitext(filename)[1].lower() # Set appropriate MIME type for common file types if file_ext in ['.mp4', '.m4v']: mime_type = 'video/mp4' elif file_ext == '.avi': mime_type = 'video/x-msvideo' elif file_ext == '.mkv': mime_type = 'video/x-matroska' elif file_ext == '.mov': mime_type = 'video/quicktime' elif file_ext == '.pdf': mime_type = 'application/pdf' elif file_ext in ['.jpg', '.jpeg']: mime_type = 'image/jpeg' elif file_ext == '.png': mime_type = 'image/png' elif file_ext == '.zip': mime_type = 'application/zip' # Prepare files for multipart form data files = { 'file': (filename, file_content, mime_type) } # Make the POST request with a timeout response = requests.post( upload_url, files=files, timeout=300 # 5 minutes timeout ) # Check response if response.status_code == 200: result = response.json() if 'url' in result: full_url = f"https://jerrrycans-file.hf.space{result['url']}" return full_url, None else: logger.error(f"Unexpected response format: {result}") return None, f"Unexpected response format: {result}" else: logger.error(f"Upload failed with status {response.status_code}: {response.text}") return None, f"Upload failed with status {response.status_code}: {response.text}" except requests.exceptions.Timeout: logger.error("Upload request timed out") return None, "Upload request timed out" except requests.exceptions.RequestException as e: logger.error(f"Request error: {e}") return None, f"Request error: {e}" except Exception as e: logger.error(f"Upload error: {e}") return None, str(e) # Improved chunked upload for large files - optimized for MP4 compatibility def chunked_upload_to_jerrrycans(file_path, filename, file_size): try: # Create a temporary directory for chunks temp_dir = os.path.join(tempfile.gettempdir(), f"chunked_upload_{str(uuid.uuid4())}") os.makedirs(temp_dir, exist_ok=True) logger.info(f"Created temp directory for chunks: {temp_dir}") # Determine number of chunks needed num_chunks = (file_size // MAX_CHUNK_SIZE) + (1 if file_size % MAX_CHUNK_SIZE > 0 else 0) logger.info(f"Splitting file into {num_chunks} chunks") # Get file extension file_ext = os.path.splitext(filename)[1].lower() is_mp4 = file_ext in ['.mp4', '.m4v'] # For MP4 files, we need to use ffmpeg to properly split the video # to ensure each chunk is a valid MP4 file chunk_paths = [] if is_mp4 and num_chunks > 1: # Use ffmpeg to split the video logger.info("Splitting MP4 file with ffmpeg for proper format compatibility") # Calculate segment duration based on file size and target chunk count # Get video duration with ffprobe duration_cmd = [ 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path ] try: duration_output = subprocess.check_output(duration_cmd, universal_newlines=True) duration = float(duration_output.strip()) logger.info(f"Video duration: {duration} seconds") # Calculate segment duration segment_duration = duration / num_chunks logger.info(f"Segment duration: {segment_duration} seconds") # Create segments with ffmpeg for i in range(num_chunks): chunk_path = os.path.join(temp_dir, f"chunk_{i}_{filename}") chunk_paths.append(chunk_path) start_time = i * segment_duration # Use a duration for all chunks except the last one if i < num_chunks - 1: segment_cmd = [ 'ffmpeg', '-i', file_path, '-ss', str(start_time), '-t', str(segment_duration), '-c:v', 'copy', # Copy video codec to avoid re-encoding '-c:a', 'copy', # Copy audio codec '-avoid_negative_ts', '1', # Adjust timestamps '-movflags', '+faststart', # Optimize for web '-y', # Overwrite output if exists chunk_path ] else: # For the last chunk, don't specify duration to get the rest of the video segment_cmd = [ 'ffmpeg', '-i', file_path, '-ss', str(start_time), '-c:v', 'copy', '-c:a', 'copy', '-avoid_negative_ts', '1', '-movflags', '+faststart', '-y', chunk_path ] logger.info(f"Creating chunk {i+1}/{num_chunks}") subprocess.run(segment_cmd, check=True) logger.info(f"Created chunk file: {chunk_path}") except (subprocess.SubprocessError, ValueError) as e: logger.error(f"Error splitting video: {e}") # Fall back to basic binary splitting logger.info("Falling back to binary splitting") chunk_paths = [] with open(file_path, 'rb') as source_file: for i in range(num_chunks): chunk_path = os.path.join(temp_dir, f"chunk_{i}_{filename}") chunk_paths.append(chunk_path) # Write chunk to disk with open(chunk_path, 'wb') as chunk_file: bytes_to_read = min(MAX_CHUNK_SIZE, file_size - (i * MAX_CHUNK_SIZE)) chunk_file.write(source_file.read(bytes_to_read)) logger.info(f"Created chunk {i+1}/{num_chunks}: {chunk_path}, size: {bytes_to_read}") else: # For non-MP4 files or smaller files, use standard binary splitting with open(file_path, 'rb') as source_file: for i in range(num_chunks): chunk_path = os.path.join(temp_dir, f"chunk_{i}_{filename}") chunk_paths.append(chunk_path) # Write chunk to disk with open(chunk_path, 'wb') as chunk_file: bytes_to_read = min(MAX_CHUNK_SIZE, file_size - (i * MAX_CHUNK_SIZE)) chunk_file.write(source_file.read(bytes_to_read)) logger.info(f"Created chunk {i+1}/{num_chunks}: {chunk_path}, size: {bytes_to_read}") # Upload each chunk and collect URLs chunk_urls = [] for i, chunk_path in enumerate(chunk_paths): logger.info(f"Uploading chunk {i+1}/{num_chunks}") # For MP4 files, use descriptive filenames for each chunk if is_mp4: base_name = os.path.splitext(filename)[0] chunk_name = f"{base_name}_part{i+1}of{num_chunks}.mp4" else: chunk_name = f"chunk_{i+1}_of_{num_chunks}_{filename}" url, error = simple_upload_to_jerrrycans(chunk_path, chunk_name) if url: chunk_urls.append(url) logger.info(f"Chunk {i+1} uploaded: {url}") else: # Clean up temp directory shutil.rmtree(temp_dir) return None, f"Failed to upload chunk {i+1}: {error}", [] # Clean up temp directory shutil.rmtree(temp_dir) # Return all chunk URLs with the main URL being the first chunk if chunk_urls: # Include all chunk URLs in the response chunk_info_html = "

File uploaded in multiple parts:

" # For display in the UI, we'll use the first chunk URL as the primary URL # but return all URLs in the data structure return chunk_urls[0], None, chunk_urls else: return None, "No chunks were successfully uploaded", [] except Exception as e: # Make sure to clean up any temporary files try: if os.path.exists(temp_dir): shutil.rmtree(temp_dir) except: pass logger.error(f"Chunked upload error: {e}") return None, f"Chunked upload error: {str(e)}", [] # Function to download M3U8 stream def download_m3u8(url, download_id): try: # Update status active_downloads[download_id]['status'] = 'downloading' save_downloads_metadata() # Create a temporary directory for segments with tempfile.TemporaryDirectory() as temp_folder: # Parse the M3U8 file m3u8_obj = m3u8.load(url) # Generate output filename timestamp = int(time.time()) output_filename = f"stream_{timestamp}.mp4" # Final output path final_output_path = os.path.join(data_dir, f"{download_id}.mp4") ts_concat_list_path = os.path.join(temp_folder, "concat_list.txt") temp_ts_output_path = os.path.join(temp_folder, "segments.ts") active_downloads[download_id]['filename'] = output_filename save_downloads_metadata() # For live streams, we'll need to keep checking for new segments segment_urls = [] downloaded_segments = set() # If it's a standard playlist with segments if m3u8_obj.segments: segment_urls = [segment.uri for segment in m3u8_obj.segments] active_downloads[download_id]['segments_total'] = len(segment_urls) # If it's a master playlist, select the first variant elif m3u8_obj.playlists: # Choose the first playlist (usually highest quality) child_url = m3u8_obj.playlists[0].uri # If the URI is relative, make it absolute if not child_url.startswith('http'): base_url = url.rsplit('/', 1)[0] child_url = f"{base_url}/{child_url}" # Load the child playlist child_m3u8 = m3u8.load(child_url) segment_urls = [segment.uri for segment in child_m3u8.segments] active_downloads[download_id]['segments_total'] = len(segment_urls) # Update the base URL for segments url = child_url # Make segment URLs absolute if needed base_url = url.rsplit('/', 1)[0] for i, segment_url in enumerate(segment_urls): if not segment_url.startswith('http'): segment_urls[i] = f"{base_url}/{segment_url}" # For live streams, we'll keep checking for new segments is_live = m3u8_obj.is_endlist is False # List to store downloaded segment paths for FFmpeg concat segment_paths = [] segment_index = 0 polling_interval = 2 # seconds to wait between checks for new segments while True: # Check if download should be stopped if active_downloads[download_id]['status'] == 'stopping': active_downloads[download_id]['status'] = 'stopped' save_downloads_metadata() break # Check if download is paused if active_downloads[download_id]['paused']: time.sleep(1) continue # If we've downloaded all segments and it's not a live stream, we're done if segment_index >= len(segment_urls) and not is_live: break # For live streams, reload the playlist to get new segments if is_live and segment_index >= len(segment_urls): time.sleep(polling_interval) try: updated_m3u8 = m3u8.load(url) new_segments = [segment.uri for segment in updated_m3u8.segments] # Make new segment URLs absolute if needed for i, segment_url in enumerate(new_segments): if not segment_url.startswith('http'): new_segments[i] = f"{base_url}/{segment_url}" # Check for new segments for segment_url in new_segments: if segment_url not in downloaded_segments: segment_urls.append(segment_url) active_downloads[download_id]['segments_total'] = len(segment_urls) save_downloads_metadata() except Exception as e: logger.error(f"Error reloading playlist: {e}") continue # Download the next segment if segment_index < len(segment_urls): segment_url = segment_urls[segment_index] segment_path = os.path.join(temp_folder, f"segment_{segment_index}.ts") try: response = requests.get(segment_url, stream=True) if response.status_code == 200: # Write segment to disk with open(segment_path, 'wb') as segment_file: for chunk in response.iter_content(chunk_size=1024*1024): # 1MB chunks if chunk: segment_file.write(chunk) segment_paths.append(segment_path) downloaded_segments.add(segment_url) segment_index += 1 active_downloads[download_id]['segments_done'] = segment_index if active_downloads[download_id]['segments_total'] > 0: progress = (segment_index / active_downloads[download_id]['segments_total']) * 100 active_downloads[download_id]['progress'] = min(99, progress) # Cap at 99% for live streams # Periodically save metadata if segment_index % 10 == 0: save_downloads_metadata() else: logger.error(f"Error downloading segment, status code: {response.status_code}") time.sleep(1) # Wait before retrying except Exception as e: logger.error(f"Error downloading segment {segment_url}: {e}") time.sleep(1) # Wait before retrying else: time.sleep(polling_interval) # Create a file for FFmpeg with the list of segments with open(ts_concat_list_path, 'w') as f: for segment_path in segment_paths: f.write(f"file '{segment_path}'\n") # First concat all TS segments into one TS file if segment_paths: try: active_downloads[download_id]['status'] = 'processing' save_downloads_metadata() # Use FFmpeg to concatenate all segments into a single TS file ffmpeg_concat_cmd = [ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', ts_concat_list_path, '-c', 'copy', temp_ts_output_path ] subprocess.run(ffmpeg_concat_cmd, check=True) # Then convert the TS file to MP4 ffmpeg_convert_cmd = [ 'ffmpeg', '-i', temp_ts_output_path, '-c:v', 'copy', '-c:a', 'aac', '-bsf:a', 'aac_adtstoasc', final_output_path ] subprocess.run(ffmpeg_convert_cmd, check=True) # Upload to jerrrycans active_downloads[download_id]['status'] = 'uploading' save_downloads_metadata() upload_url, error, chunk_urls = upload_to_jerrrycans(final_output_path, output_filename) if upload_url: active_downloads[download_id]['upload_url'] = upload_url if chunk_urls and len(chunk_urls) > 1: active_downloads[download_id]['chunk_urls'] = chunk_urls active_downloads[download_id]['status'] = 'completed' else: active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Upload failed: {error}" active_downloads[download_id]['progress'] = 100 save_downloads_metadata() except Exception as e: logger.error(f"Error processing video: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Error processing video: {e}" save_downloads_metadata() else: active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = "No segments were downloaded" save_downloads_metadata() except Exception as e: logger.error(f"Download error: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = str(e) save_downloads_metadata() # New function to download TS segments def download_ts_segments(url, download_id): try: # Update status active_downloads[download_id]['status'] = 'downloading' save_downloads_metadata() # Create a temporary directory for segments with tempfile.TemporaryDirectory() as temp_folder: active_downloads[download_id]['segments_dir'] = temp_folder # Extract information from the URL parsed_url = urlparse(url) path_parts = parsed_url.path.split('/') base_path = '/'.join(path_parts[:-1]) base_url = f"{parsed_url.scheme}://{parsed_url.netloc}{base_path}" # Try to find the segment pattern segment_pattern = re.search(r'seg-(\d+)-v\d+-a\d+\.ts', path_parts[-1]) if not segment_pattern: # If no specific pattern found, use the provided URL as the first segment logger.info("No specific segment pattern found, using the provided URL as starting point") # Extract initial segment number segment_number = 1 # Extract base segment name (for building future URLs) segment_filename = path_parts[-1] segment_base = re.sub(r'\d+', '{num}', segment_filename) if segment_base == segment_filename: # If no numbers found, we'll use a generic pattern if '.ts' in segment_filename: segment_base = segment_filename.replace('.ts', '-{num}.ts') else: segment_base = f"{segment_filename}-{num}.ts" else: # Extract starting segment number from the URL segment_number = int(segment_pattern.group(1)) segment_base = 'seg-{num}-v1-a1.ts' # Common pattern for TS segments # Get query parameters from the original URL query_params = parse_qs(parsed_url.query) query_string = urlencode(query_params, doseq=True) if query_params else "" # Set up headers to mimic a browser headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'en-US,en;q=0.5', 'Connection': 'keep-alive', 'Sec-Ch-Ua': '"Chromium";v="136", "Brave";v="136", "Not.A/Brand";v="99"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Gpc': '1', 'Upgrade-Insecure-Requests': '1' } # List to store downloaded segment paths segment_paths = [] # Generate output filename timestamp = int(time.time()) output_filename = f"ts_stream_{timestamp}.mp4" # Update filename in metadata active_downloads[download_id]['filename'] = output_filename save_downloads_metadata() # For the first segment, use the original URL first_segment_path = os.path.join(temp_folder, f"segment_{segment_number:03d}.ts") try: # Check if the first URL works directly response = requests.get(url, headers=headers, stream=True, timeout=30) if response.status_code == 200: with open(first_segment_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) # Check if file is valid (not too small) if os.path.getsize(first_segment_path) > 1000: segment_paths.append(first_segment_path) active_downloads[download_id]['segments_done'] = 1 active_downloads[download_id]['progress'] = 1 # Initial progress save_downloads_metadata() else: # First segment is invalid, may not be a TS file logger.error("First segment is too small or invalid") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = "First segment is too small or invalid. This may not be a valid TS stream URL." save_downloads_metadata() return else: # First URL didn't work logger.error(f"Could not download first segment, status code: {response.status_code}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Could not download first segment, status code: {response.status_code}" save_downloads_metadata() return except Exception as e: logger.error(f"Error downloading first segment: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Error downloading first segment: {e}" save_downloads_metadata() return # Start with the next segment current_segment = segment_number + 1 total_segments = 1 # We've got 1 segment so far consecutive_errors = 0 max_consecutive_errors = 5 # Stop after 5 consecutive failures # Continue downloading segments until we encounter too many errors while consecutive_errors < max_consecutive_errors: # Check if download should be stopped if active_downloads[download_id]['status'] == 'stopping': active_downloads[download_id]['status'] = 'stopped' save_downloads_metadata() break # Check if download is paused if active_downloads[download_id]['paused']: time.sleep(1) continue # Handle different segment naming patterns if 'seg-' in segment_base: # For "seg-X-vY-aZ.ts" pattern segment_filename = segment_base.replace('{num}', str(current_segment)) else: # For other patterns or simple numerical replacements segment_filename = segment_base.replace('{num}', str(current_segment)) # Construct the next segment URL with the original query parameters if query_string: next_segment_url = f"{base_url}/{segment_filename}?{query_string}" else: next_segment_url = f"{base_url}/{segment_filename}" # Create segment file path segment_path = os.path.join(temp_folder, f"segment_{current_segment:03d}.ts") try: logger.info(f"Downloading segment {current_segment} from {next_segment_url}") # Download the next segment response = requests.get(next_segment_url, headers=headers, stream=True, timeout=30) # Check if the request was successful if response.status_code == 200: # Get content length (might be missing) content_length = int(response.headers.get('Content-Length', 0)) # Write the segment to disk with open(segment_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) # Verify segment is valid (not too small) file_size = os.path.getsize(segment_path) if file_size > 1000 or (content_length > 0 and file_size >= content_length * 0.9): segment_paths.append(segment_path) current_segment += 1 total_segments += 1 consecutive_errors = 0 # Reset error count on success # Update download progress active_downloads[download_id]['segments_done'] = total_segments active_downloads[download_id]['segments_total'] = total_segments # We don't know the total active_downloads[download_id]['progress'] = min(99, (total_segments / 100) * 100) # Cap at 99% # Periodically save metadata if total_segments % 10 == 0: save_downloads_metadata() else: logger.info(f"Segment {current_segment} is too small ({file_size} bytes), might be invalid") os.remove(segment_path) # Remove invalid segment consecutive_errors += 1 else: logger.info(f"Failed to download segment {current_segment}, status code: {response.status_code}") consecutive_errors += 1 except Exception as e: logger.error(f"Error downloading segment {current_segment}: {e}") consecutive_errors += 1 # Slight delay between segment downloads time.sleep(0.5) # Final output path final_output_path = os.path.join(data_dir, f"{download_id}.mp4") ts_concat_list_path = os.path.join(temp_folder, "concat_list.txt") temp_ts_output_path = os.path.join(temp_folder, "segments_combined.ts") # Create a file for FFmpeg with the list of segments with open(ts_concat_list_path, 'w') as f: for path in segment_paths: f.write(f"file '{path}'\n") # Process segments if we have any if segment_paths: try: active_downloads[download_id]['status'] = 'processing' save_downloads_metadata() # First concatenate all TS segments ffmpeg_concat_cmd = [ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', ts_concat_list_path, '-c', 'copy', '-y', temp_ts_output_path ] subprocess.run(ffmpeg_concat_cmd, check=True) # Then convert to MP4 ffmpeg_convert_cmd = [ 'ffmpeg', '-i', temp_ts_output_path, '-c:v', 'copy', '-c:a', 'aac', '-bsf:a', 'aac_adtstoasc', '-y', final_output_path ] subprocess.run(ffmpeg_convert_cmd, check=True) # Upload to jerrrycans active_downloads[download_id]['status'] = 'uploading' save_downloads_metadata() upload_url, error, chunk_urls = upload_to_jerrrycans(final_output_path, output_filename) if upload_url: active_downloads[download_id]['upload_url'] = upload_url if chunk_urls and len(chunk_urls) > 1: active_downloads[download_id]['chunk_urls'] = chunk_urls active_downloads[download_id]['status'] = 'completed' else: active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Upload failed: {error}" active_downloads[download_id]['progress'] = 100 save_downloads_metadata() except Exception as e: logger.error(f"Error processing TS segments: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = f"Error processing TS segments: {e}" save_downloads_metadata() else: active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = "No valid segments were downloaded" save_downloads_metadata() except Exception as e: logger.error(f"TS download error: {e}") active_downloads[download_id]['status'] = 'error' active_downloads[download_id]['error'] = str(e) save_downloads_metadata() # Update the INDEX_HTML template to show the multiple chunk links INDEX_HTML = ''' Video & Downloader

Universal Downloader & Uploader

Downloader
ZIP Extractor

Start New Download

Active Downloads

No active downloads

Extract ZIP File

Extracted ZIP Files

No extracted ZIP files
''' if __name__ == '__main__': app.run(host='0.0.0.0', port=7860, debug=True)