194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
from movies import *
|
|
from series import *
|
|
import os
|
|
import re
|
|
import requests
|
|
import subprocess
|
|
import signal
|
|
import sys
|
|
import pyfiglet
|
|
from urllib.parse import urlparse
|
|
from collections import defaultdict
|
|
from colorama import Fore, Style
|
|
|
|
USER_AGENT = "VLC/3.0.9 LibVLC/3.0.9"
|
|
HEADERS = {'User-Agent': USER_AGENT}
|
|
SUPPORTED_EXTENSIONS = ('.mkv', '.mp4', '.avi')
|
|
max_connections = 1
|
|
|
|
def handle_exit(sig, frame):
|
|
print("\nScript exit with CTRL+C by user")
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGINT, handle_exit)
|
|
|
|
def sanitize_filename(name):
|
|
return re.sub(r'[\\/*?:\"<>|]', "", name).strip()
|
|
|
|
def aria2c_exists():
|
|
return os.path.exists("./aria2c") or os.path.exists("./aria2c.exe")
|
|
|
|
def get_remote_file_size(url):
|
|
try:
|
|
with requests.head(url, headers=HEADERS, timeout=10) as head:
|
|
head.raise_for_status()
|
|
content_length = head.headers.get('content-length')
|
|
return int(content_length) if content_length else 0
|
|
except Exception:
|
|
return 0
|
|
|
|
def is_series(title):
|
|
match = re.search(r'(.*?)\s+[Ss](\d{2})[Ee](\d{2})', title)
|
|
if match:
|
|
series_name = match.group(1).strip()
|
|
season = f"S{match.group(2)}"
|
|
episode = f"E{match.group(3)}"
|
|
return True, series_name, season, episode
|
|
return False, None, None, None
|
|
|
|
def parse_m3u(m3u_content):
|
|
entries = []
|
|
lines = m3u_content.splitlines()
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
if line.startswith("#EXTINF"):
|
|
info_line = line
|
|
i += 1
|
|
if i < len(lines):
|
|
url_line = lines[i]
|
|
entries.append((info_line, url_line))
|
|
i += 1
|
|
return entries
|
|
|
|
def extract_metadata(info_line):
|
|
tvg_name_match = re.search(r'tvg-name="([^"]+)"', info_line)
|
|
tvg_logo_match = re.search(r'tvg-logo="([^"]+)"', info_line)
|
|
group_title_match = re.search(r'group-title="([^"]+)"', info_line)
|
|
|
|
title = tvg_name_match.group(1) if tvg_name_match else "Unknown Title"
|
|
logo_url = tvg_logo_match.group(1) if tvg_logo_match else None
|
|
group_title = group_title_match.group(1) if group_title_match else "Unknown Group"
|
|
|
|
return title, logo_url, group_title
|
|
|
|
def input_range(prompt, max_val):
|
|
inp = input(prompt)
|
|
selected = set()
|
|
for part in inp.split(','):
|
|
if '-' in part:
|
|
start, end = map(int, part.split('-'))
|
|
selected.update(range(start, end + 1))
|
|
else:
|
|
selected.add(int(part))
|
|
return [i for i in selected if 1 <= i <= max_val]
|
|
|
|
def show_banner():
|
|
print("=" * 72)
|
|
banner = pyfiglet.figlet_format("XC Downloader", font="slant")
|
|
print(banner)
|
|
print(" by redhat@woi ")
|
|
print("=" * 72 + "\n\n")
|
|
|
|
def get_credentials():
|
|
print("Enter your server credentials below")
|
|
print("-----------------------------------\n")
|
|
server_url = input("Server URL & Port (e.g., http://serverip_or_url:port): ").strip().rstrip('/')
|
|
username = input("Username: ").strip()
|
|
password = input("Password: ").strip()
|
|
try:
|
|
conn_limit = int(input("Server connection limit (max concurrent downloads): ").strip())
|
|
if conn_limit < 1:
|
|
conn_limit = 1
|
|
except ValueError:
|
|
conn_limit = 1
|
|
return server_url, username, password, conn_limit
|
|
|
|
def main():
|
|
show_banner()
|
|
global max_connections
|
|
|
|
# Loop until valid credentials are provided
|
|
while True:
|
|
server_url, username, password, max_connections = get_credentials()
|
|
m3u_url = f"{server_url}/get.php?username={username}&password={password}&type=m3u_plus&output=mpegts"
|
|
|
|
try:
|
|
response = requests.get(m3u_url, headers=HEADERS, timeout=30)
|
|
response.raise_for_status()
|
|
m3u_content = response.text
|
|
break # ✅ Exit loop if successful
|
|
except Exception:
|
|
print(f"{Fore.RED}Error: Check your credentials, connection could not be established{Style.RESET_ALL}\n")
|
|
|
|
entries = parse_m3u(m3u_content)
|
|
movies = defaultdict(list)
|
|
series = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
|
|
|
for info_line, media_url in entries:
|
|
if not media_url.lower().endswith(SUPPORTED_EXTENSIONS):
|
|
continue
|
|
|
|
title, logo_url, group_title = extract_metadata(info_line)
|
|
is_series_flag, series_name, season, episode = is_series(title)
|
|
|
|
if is_series_flag:
|
|
series[group_title][series_name][season].append({
|
|
'title': title,
|
|
'logo_url': logo_url,
|
|
'media_url': media_url,
|
|
'season': season,
|
|
'episode': episode
|
|
})
|
|
else:
|
|
movies[group_title].append({
|
|
'title': title,
|
|
'logo_url': logo_url,
|
|
'media_url': media_url
|
|
})
|
|
|
|
while True:
|
|
print("\n" + "*" * 21)
|
|
print("* * Main Menu * *")
|
|
print("*" * 21 + "\n")
|
|
|
|
print("Results:")
|
|
print("-----------\n")
|
|
print(f"Movies found: {sum(len(v) for v in movies.values())}")
|
|
print(f"Movies Categories found: {len(movies)}")
|
|
print("-----------\n")
|
|
|
|
print("-----------")
|
|
print(f"Series found: {sum(len(series[g][s][se]) for g in series for s in series[g] for se in series[g][s])}")
|
|
print(f"Series Categories found: {len(series)}")
|
|
print("-----------\n")
|
|
|
|
print("1. Show Movies Categories")
|
|
print("2. Show Series Categories")
|
|
print("3. Download all Movies")
|
|
print("4. Download all Series")
|
|
print("5. Exit\n")
|
|
|
|
choice = input("Select: ").strip()
|
|
|
|
if choice == '1':
|
|
show_movie_categories(movies)
|
|
elif choice == '2':
|
|
show_series_categories(series)
|
|
elif choice == '3':
|
|
for category in movies:
|
|
for movie in movies[category]:
|
|
download_movie(category, movie)
|
|
elif choice == '4':
|
|
for category in series:
|
|
for sname in series[category]:
|
|
for season in series[category][sname]:
|
|
for ep in series[category][sname][season]:
|
|
download_series_episode(category, sname, season, ep)
|
|
elif choice == '5':
|
|
break
|
|
else:
|
|
print("Invalid choice.")
|
|
|
|
if __name__ == "__main__":
|
|
main() |