mirror of
https://github.com/Arrowar/StreamingCommunity.git
synced 2025-06-06 19:45:24 +00:00
v2.4.0
This commit is contained in:
parent
fd0add424d
commit
3b8af89c35
3
.gitignore
vendored
3
.gitignore
vendored
@ -47,5 +47,4 @@ venv.bak/
|
|||||||
Video
|
Video
|
||||||
note.txt
|
note.txt
|
||||||
list_proxy.txt
|
list_proxy.txt
|
||||||
cmd.txt
|
cmd.txt
|
||||||
downloaded_files
|
|
21
README.md
21
README.md
@ -9,9 +9,6 @@
|
|||||||
<a href="https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C">
|
<a href="https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C">
|
||||||
<img src="https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge" alt="Donate"/>
|
<img src="https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge" alt="Donate"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Lovi-0/StreamingCommunity/blob/main/LICENSE">
|
|
||||||
<img src="https://img.shields.io/badge/License-GPL_3.0-blue.svg?style=for-the-badge" alt="License"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Lovi-0/StreamingCommunity/commits">
|
<a href="https://github.com/Lovi-0/StreamingCommunity/commits">
|
||||||
<img src="https://img.shields.io/github/commit-activity/m/Lovi-0/StreamingCommunity?label=commits&style=for-the-badge" alt="Commits"/>
|
<img src="https://img.shields.io/github/commit-activity/m/Lovi-0/StreamingCommunity?label=commits&style=for-the-badge" alt="Commits"/>
|
||||||
</a>
|
</a>
|
||||||
@ -21,17 +18,11 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="https://github.com/Lovi-0/StreamingCommunity/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/License-GPL_3.0-blue.svg?style=for-the-badge" alt="License"/>
|
||||||
|
</a>
|
||||||
<a href="https://pypi.org/project/streamingcommunity">
|
<a href="https://pypi.org/project/streamingcommunity">
|
||||||
<img src="https://img.shields.io/pypi/dm/streamingcommunity?style=for-the-badge" alt="PyPI Downloads"/>
|
<img src="https://img.shields.io/pypi/dw/streamingcommunity?style=for-the-badge" alt="PyPI Downloads"/>
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Lovi-0/StreamingCommunity/network/members">
|
|
||||||
<img src="https://img.shields.io/github/forks/Lovi-0/StreamingCommunity?style=for-the-badge" alt="Forks"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Lovi-0/StreamingCommunity">
|
|
||||||
<img src="https://img.shields.io/github/languages/code-size/Lovi-0/StreamingCommunity?style=for-the-badge" alt="Code Size"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/Lovi-0/StreamingCommunity">
|
|
||||||
<img src="https://img.shields.io/github/repo-size/Lovi-0/StreamingCommunity?style=for-the-badge" alt="Repo Size"/>
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -477,10 +468,10 @@ The `run-container` command mounts also the `config.json` file, so any change to
|
|||||||
| Website | Status |
|
| Website | Status |
|
||||||
|:-------------------|:------:|
|
|:-------------------|:------:|
|
||||||
| [1337xx](https://1337xx.to/) | ✅ |
|
| [1337xx](https://1337xx.to/) | ✅ |
|
||||||
| [Altadefinizione](https://altadefinizione.florist/) | ✅ |
|
| [AltadefinizioneGratis](https://altadefinizionegratis.info/) | ✅ |
|
||||||
| [AnimeUnity](https://animeunity.so/) | ✅ |
|
| [AnimeUnity](https://animeunity.so/) | ✅ |
|
||||||
| [Ilcorsaronero](https://ilcorsaronero.link/) | ✅ |
|
| [Ilcorsaronero](https://ilcorsaronero.link/) | ✅ |
|
||||||
| [CB01New](https://cb01new.lol/) | ✅ |
|
| [CB01New](https://cb01new.video/) | ✅ |
|
||||||
| [DDLStreamItaly](https://ddlstreamitaly.co/) | ✅ |
|
| [DDLStreamItaly](https://ddlstreamitaly.co/) | ✅ |
|
||||||
| [GuardaSerie](https://guardaserie.academy/) | ✅ |
|
| [GuardaSerie](https://guardaserie.academy/) | ✅ |
|
||||||
| [MostraGuarda](https://mostraguarda.stream/) | ✅ |
|
| [MostraGuarda](https://mostraguarda.stream/) | ✅ |
|
||||||
|
@ -199,7 +199,7 @@ def display_episodes_list(scrape_serie) -> str:
|
|||||||
# Run the table and handle user input
|
# Run the table and handle user input
|
||||||
last_command = table_show_manager.run()
|
last_command = table_show_manager.run()
|
||||||
|
|
||||||
if last_command == "q":
|
if last_command == "q" or last_command == "quit":
|
||||||
console.print("\n[red]Quit [white]...")
|
console.print("\n[red]Quit [white]...")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# 10.12.23
|
# 10.12.23
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
@ -83,8 +82,12 @@ def get_version_and_domain():
|
|||||||
if not disable_searchDomain:
|
if not disable_searchDomain:
|
||||||
domain_to_use, base_url = search_domain(SITE_NAME, f"https://{SITE_NAME}.{DOMAIN_NOW}")
|
domain_to_use, base_url = search_domain(SITE_NAME, f"https://{SITE_NAME}.{DOMAIN_NOW}")
|
||||||
|
|
||||||
version = get_version(domain_to_use)
|
try:
|
||||||
|
version = get_version(domain_to_use)
|
||||||
|
except:
|
||||||
|
console.print("[green]Auto generate version ...")
|
||||||
|
version = secrets.token_hex(32 // 2)
|
||||||
|
|
||||||
return version, domain_to_use
|
return version, domain_to_use
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
import certifi
|
|
||||||
from urllib.parse import urlparse, unquote
|
from urllib.parse import urlparse, unquote
|
||||||
|
|
||||||
|
|
||||||
@ -20,14 +19,14 @@ from StreamingCommunity.Util._jsonConfig import config_manager
|
|||||||
base_headers = {
|
base_headers = {
|
||||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||||
'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7',
|
'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||||
'cache-control': 'max-age=0',
|
|
||||||
'dnt': '1',
|
'dnt': '1',
|
||||||
'priority': 'u=0, i',
|
'priority': 'u=0, i',
|
||||||
|
'referer': '',
|
||||||
'sec-ch-ua-mobile': '?0',
|
'sec-ch-ua-mobile': '?0',
|
||||||
'sec-ch-ua-platform': '"Windows"',
|
'sec-ch-ua-platform': '"Windows"',
|
||||||
'sec-fetch-dest': 'document',
|
'sec-fetch-dest': 'document',
|
||||||
'sec-fetch-mode': 'navigate',
|
'sec-fetch-mode': 'navigate',
|
||||||
'sec-fetch-site': 'none',
|
'sec-fetch-site': 'same-origin',
|
||||||
'sec-fetch-user': '?1',
|
'sec-fetch-user': '?1',
|
||||||
'upgrade-insecure-requests': '1',
|
'upgrade-insecure-requests': '1',
|
||||||
'user-agent': ''
|
'user-agent': ''
|
||||||
@ -84,6 +83,8 @@ def validate_url(url, base_url, max_timeout, max_retries=3, sleep=3):
|
|||||||
# Verify URL structure matches base_url structure
|
# Verify URL structure matches base_url structure
|
||||||
base_domain = get_base_domain(base_url)
|
base_domain = get_base_domain(base_url)
|
||||||
url_domain = get_base_domain(url)
|
url_domain = get_base_domain(url)
|
||||||
|
|
||||||
|
base_headers['referer'] = url
|
||||||
base_headers['user-agent'] = get_headers()
|
base_headers['user-agent'] = get_headers()
|
||||||
|
|
||||||
if base_domain != url_domain:
|
if base_domain != url_domain:
|
||||||
@ -98,7 +99,7 @@ def validate_url(url, base_url, max_timeout, max_retries=3, sleep=3):
|
|||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
client = httpx.Client(
|
client = httpx.Client(
|
||||||
verify=certifi.where(),
|
verify=False,
|
||||||
headers=base_headers,
|
headers=base_headers,
|
||||||
timeout=max_timeout
|
timeout=max_timeout
|
||||||
)
|
)
|
||||||
|
@ -74,7 +74,7 @@ def get_select_title(table_show_manager, media_search_manager):
|
|||||||
table_show_manager.clear()
|
table_show_manager.clear()
|
||||||
|
|
||||||
# Handle user's quit command
|
# Handle user's quit command
|
||||||
if last_command == "q":
|
if last_command == "q" or last_command == "quit":
|
||||||
console.print("\n[red]Quit [white]...")
|
console.print("\n[red]Quit [white]...")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ def get_select_title(table_show_manager, generic_obj):
|
|||||||
table_show_manager.clear()
|
table_show_manager.clear()
|
||||||
|
|
||||||
# Handle user's quit command
|
# Handle user's quit command
|
||||||
if last_command == "q":
|
if last_command == "q" or last_command == "quit":
|
||||||
Console.print("\n[red]Quit [white]...")
|
Console.print("\n[red]Quit [white]...")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
__title__ = 'StreamingCommunity'
|
__title__ = 'StreamingCommunity'
|
||||||
__version__ = '2.3.0'
|
__version__ = '2.4.0'
|
||||||
__author__ = 'Lovi-0'
|
__author__ = 'Lovi-0'
|
||||||
__description__ = 'A command-line program to download film'
|
__description__ = 'A command-line program to download film'
|
||||||
__copyright__ = 'Copyright 2024'
|
__copyright__ = 'Copyright 2024'
|
||||||
|
@ -19,6 +19,8 @@ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRe
|
|||||||
|
|
||||||
# Variable
|
# Variable
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
# https://github.com/eugeneware/ffmpeg-static/releases
|
||||||
FFMPEG_CONFIGURATION = {
|
FFMPEG_CONFIGURATION = {
|
||||||
'windows': {
|
'windows': {
|
||||||
'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary'),
|
'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary'),
|
||||||
@ -28,13 +30,13 @@ FFMPEG_CONFIGURATION = {
|
|||||||
},
|
},
|
||||||
'darwin': {
|
'darwin': {
|
||||||
'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'),
|
'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'),
|
||||||
'download_url': 'https://evermeet.cx/ffmpeg/ffmpeg-{version}.zip',
|
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/download/b{version}/ffmpeg-macOS-{arch}.zip',
|
||||||
'file_extension': '.zip',
|
'file_extension': '.zip',
|
||||||
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
||||||
},
|
},
|
||||||
'linux': {
|
'linux': {
|
||||||
'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'),
|
'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'),
|
||||||
'download_url': 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-{arch}-static.tar.xz',
|
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/download/b{version}/ffmpeg-linux-{arch}.tar.xz',
|
||||||
'file_extension': '.tar.xz',
|
'file_extension': '.tar.xz',
|
||||||
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
||||||
}
|
}
|
||||||
@ -150,19 +152,26 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
def _get_latest_version(self) -> Optional[str]:
|
def _get_latest_version(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Get the latest FFmpeg version from the official website.
|
Get the latest FFmpeg version from the GitHub releases page.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[str]: The latest version string, or None if retrieval fails
|
Optional[str]: The latest version string, or None if retrieval fails.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
requests.exceptions.RequestException: If there are network-related errors
|
requests.exceptions.RequestException: If there are network-related errors.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
version_url = 'https://www.gyan.dev/ffmpeg/builds/release-version'
|
# Use GitHub API to fetch the latest release
|
||||||
return requests.get(version_url).text.strip()
|
response = requests.get(
|
||||||
|
'https://api.github.com/repos/eugeneware/ffmpeg-static/releases/latest'
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
latest_release = response.json()
|
||||||
|
|
||||||
|
# Extract the tag name or version from the release
|
||||||
|
return latest_release.get('tag_name')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Unable to get version: {e}")
|
logging.error(f"Unable to get version from GitHub: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _download_file(self, url: str, destination: str) -> bool:
|
def _download_file(self, url: str, destination: str) -> bool:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# 26.03.24
|
# 26.03.24
|
||||||
|
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ class Logger:
|
|||||||
|
|
||||||
# Configure file logging if debug mode and logging to file are both enabled
|
# Configure file logging if debug mode and logging to file are both enabled
|
||||||
if self.log_to_file:
|
if self.log_to_file:
|
||||||
|
self.remove_existing_log_file()
|
||||||
self.configure_file_logging()
|
self.configure_file_logging()
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@ -51,3 +53,10 @@ class Logger:
|
|||||||
formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
|
formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
logging.getLogger('').addHandler(file_handler)
|
logging.getLogger('').addHandler(file_handler)
|
||||||
|
|
||||||
|
def remove_existing_log_file(self):
|
||||||
|
"""
|
||||||
|
Remove the log file if it already exists.
|
||||||
|
"""
|
||||||
|
if os.path.exists(self.log_file):
|
||||||
|
os.remove(self.log_file)
|
@ -8,16 +8,16 @@ import shutil
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import unidecode
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import contextlib
|
import contextlib
|
||||||
import pathvalidate
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
|
|
||||||
# External library
|
# External library
|
||||||
import httpx
|
import httpx
|
||||||
|
from unidecode import unidecode
|
||||||
|
from pathvalidate import sanitize_filename, sanitize_filepath
|
||||||
|
|
||||||
|
|
||||||
# Internal utilities
|
# Internal utilities
|
||||||
@ -25,172 +25,133 @@ from .ffmpeg_installer import check_ffmpeg
|
|||||||
from StreamingCommunity.Util.console import console, msg
|
from StreamingCommunity.Util.console import console, msg
|
||||||
|
|
||||||
|
|
||||||
# Variable
|
|
||||||
OS_CONFIGURATIONS = {
|
|
||||||
'windows': {
|
|
||||||
'max_length': 255,
|
|
||||||
'invalid_chars': '<>:"/\\|?*',
|
|
||||||
'reserved_names': [
|
|
||||||
"CON", "PRN", "AUX", "NUL",
|
|
||||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
||||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
|
||||||
],
|
|
||||||
'max_path': 255
|
|
||||||
},
|
|
||||||
'darwin': {
|
|
||||||
'max_length': 4096,
|
|
||||||
'invalid_chars': '/:',
|
|
||||||
'reserved_names': [],
|
|
||||||
'hidden_file_restriction': True
|
|
||||||
},
|
|
||||||
'linux': {
|
|
||||||
'max_length': 4096,
|
|
||||||
'invalid_chars': '/\0',
|
|
||||||
'reserved_names': []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OsManager:
|
class OsManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.system = self._detect_system()
|
self.system = self._detect_system()
|
||||||
self.config = OS_CONFIGURATIONS.get(self.system, {})
|
self.max_length = self._get_max_length()
|
||||||
|
|
||||||
def _detect_system(self) -> str:
|
def _detect_system(self) -> str:
|
||||||
"""Detect and normalize operating system name."""
|
"""Detect and normalize operating system name."""
|
||||||
system = platform.system().lower()
|
system = platform.system().lower()
|
||||||
|
if system not in ['windows', 'darwin', 'linux']:
|
||||||
|
raise ValueError(f"Unsupported operating system: {system}")
|
||||||
|
return system
|
||||||
|
|
||||||
if system in OS_CONFIGURATIONS:
|
def _get_max_length(self) -> int:
|
||||||
return system
|
"""Get max filename length based on OS."""
|
||||||
|
return 255 if self.system == 'windows' else 4096
|
||||||
raise ValueError(f"Unsupported operating system: {system}")
|
|
||||||
|
|
||||||
def _normalize_windows_path(self, path: str) -> str:
|
def _normalize_windows_path(self, path: str) -> str:
|
||||||
"""
|
"""Normalize Windows paths."""
|
||||||
Normalize Windows paths to handle drive letters correctly.
|
if not path or self.system != 'windows':
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): Original path that might contain a drive letter.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Properly normalized absolute path.
|
|
||||||
"""
|
|
||||||
if self.system != 'windows':
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# Check if path starts with a drive letter
|
# Preserve network paths (UNC and IP-based)
|
||||||
|
if path.startswith('\\\\') or path.startswith('//'):
|
||||||
|
return path.replace('/', '\\')
|
||||||
|
|
||||||
|
# Handle drive letters
|
||||||
if len(path) >= 2 and path[1] == ':':
|
if len(path) >= 2 and path[1] == ':':
|
||||||
drive = path[0:2]
|
drive = path[0:2]
|
||||||
rest = path[2:].lstrip(os.sep)
|
rest = path[2:].replace('/', '\\').lstrip('\\')
|
||||||
# Ensure proper absolute path format
|
return f"{drive}\\{rest}"
|
||||||
return os.path.join(drive + os.sep, rest)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def _process_filename(self, filename: str) -> str:
|
|
||||||
"""
|
|
||||||
Comprehensively process filename with cross-platform considerations.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename (str): Original filename.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Processed filename.
|
|
||||||
"""
|
|
||||||
name, ext = os.path.splitext(filename)
|
|
||||||
|
|
||||||
# Handle length restrictions
|
|
||||||
if len(name) > self.config['max_length']:
|
|
||||||
name = self._truncate_filename(name)
|
|
||||||
|
|
||||||
# Reconstruct filename
|
|
||||||
processed_filename = name + ext
|
|
||||||
|
|
||||||
return processed_filename
|
|
||||||
|
|
||||||
def _truncate_filename(self, name: str) -> str:
|
return path.replace('/', '\\')
|
||||||
"""
|
|
||||||
Truncate filename based on OS-specific rules.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): Original filename.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Truncated filename.
|
|
||||||
"""
|
|
||||||
logging.info("_truncate_filename: ", name)
|
|
||||||
|
|
||||||
if self.system == 'windows':
|
def _normalize_mac_path(self, path: str) -> str:
|
||||||
return name[:self.config['max_length'] - 3] + '___'
|
"""Normalize macOS paths."""
|
||||||
elif self.system == 'darwin':
|
if not path or self.system != 'darwin':
|
||||||
return name[:self.config['max_length']]
|
return path
|
||||||
elif self.system == 'linux':
|
|
||||||
return name[:self.config['max_length'] - 2] + '___'
|
# Convert Windows separators to Unix
|
||||||
|
normalized = path.replace('\\', '/')
|
||||||
|
|
||||||
|
# Ensure absolute paths start with /
|
||||||
|
if normalized.startswith('/'):
|
||||||
|
return os.path.normpath(normalized)
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
def get_sanitize_file(self, filename: str) -> str:
|
def get_sanitize_file(self, filename: str) -> str:
|
||||||
"""
|
"""Sanitize filename."""
|
||||||
Sanitize filename using pathvalidate with unidecode.
|
if not filename:
|
||||||
|
return filename
|
||||||
Args:
|
|
||||||
filename (str): Original filename.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Sanitized filename.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Decode unicode characters and sanitize
|
# Decode and sanitize
|
||||||
decoded_filename = unidecode.unidecode(filename)
|
decoded = unidecode(filename)
|
||||||
sanitized_filename = pathvalidate.sanitize_filename(decoded_filename)
|
sanitized = sanitize_filename(decoded)
|
||||||
|
|
||||||
# Truncate if necessary based on OS configuration
|
# Split name and extension
|
||||||
name, ext = os.path.splitext(sanitized_filename)
|
name, ext = os.path.splitext(sanitized)
|
||||||
if len(name) > self.config['max_length']:
|
|
||||||
name = self._truncate_filename(name)
|
|
||||||
|
|
||||||
result = name + ext
|
# Calculate available length for name considering the '...' and extension
|
||||||
return result
|
max_name_length = self.max_length - len('...') - len(ext)
|
||||||
|
|
||||||
|
# Truncate name if it exceeds the max name length
|
||||||
|
if len(name) > max_name_length:
|
||||||
|
name = name[:max_name_length] + '...'
|
||||||
|
|
||||||
|
# Ensure the final file name includes the extension
|
||||||
|
return name + ext
|
||||||
|
|
||||||
def get_sanitize_path(self, path: str) -> str:
|
def get_sanitize_path(self, path: str) -> str:
|
||||||
"""
|
"""Sanitize complete path."""
|
||||||
Sanitize folder path using pathvalidate with unidecode.
|
if not path:
|
||||||
|
return path
|
||||||
Args:
|
|
||||||
path (str): Original folder path.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Sanitized folder path.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Normalize path for Windows drive letters first
|
# Decode unicode characters
|
||||||
path = self._normalize_windows_path(path)
|
decoded = unidecode(path)
|
||||||
|
|
||||||
# Decode unicode characters and sanitize
|
|
||||||
decoded_path = unidecode.unidecode(path)
|
|
||||||
sanitized_path = pathvalidate.sanitize_filepath(decoded_path)
|
|
||||||
|
|
||||||
# Split path and process each component
|
# Basic path sanitization
|
||||||
path_components = os.path.normpath(sanitized_path).split(os.sep)
|
sanitized = sanitize_filepath(decoded)
|
||||||
|
|
||||||
# Handle Windows drive letter specially
|
|
||||||
if self.system == 'windows' and len(path_components[0]) == 2 and path_components[0][1] == ':':
|
|
||||||
drive = path_components.pop(0)
|
|
||||||
processed_components = [drive + os.sep]
|
|
||||||
|
|
||||||
|
if self.system == 'windows':
|
||||||
|
# Handle network paths (UNC or IP-based)
|
||||||
|
if path.startswith('\\\\') or path.startswith('//'):
|
||||||
|
parts = path.replace('/', '\\').split('\\')
|
||||||
|
# Keep server/IP and share name as is
|
||||||
|
sanitized_parts = parts[:4]
|
||||||
|
# Sanitize remaining parts
|
||||||
|
if len(parts) > 4:
|
||||||
|
sanitized_parts.extend([
|
||||||
|
self.get_sanitize_file(part)
|
||||||
|
for part in parts[4:]
|
||||||
|
if part
|
||||||
|
])
|
||||||
|
return '\\'.join(sanitized_parts)
|
||||||
|
|
||||||
|
# Handle drive letters
|
||||||
|
elif len(path) >= 2 and path[1] == ':':
|
||||||
|
drive = path[:2]
|
||||||
|
rest = path[2:].lstrip('\\').lstrip('/')
|
||||||
|
path_parts = [drive] + [
|
||||||
|
self.get_sanitize_file(part)
|
||||||
|
for part in rest.replace('/', '\\').split('\\')
|
||||||
|
if part
|
||||||
|
]
|
||||||
|
return '\\'.join(path_parts)
|
||||||
|
|
||||||
|
# Regular path
|
||||||
|
else:
|
||||||
|
parts = path.replace('/', '\\').split('\\')
|
||||||
|
return '\\'.join(p for p in parts if p)
|
||||||
else:
|
else:
|
||||||
processed_components = []
|
# Handle Unix-like paths (Linux and macOS)
|
||||||
|
is_absolute = path.startswith('/')
|
||||||
|
parts = path.replace('\\', '/').split('/')
|
||||||
|
sanitized_parts = [
|
||||||
|
self.get_sanitize_file(part)
|
||||||
|
for part in parts
|
||||||
|
if part
|
||||||
|
]
|
||||||
|
|
||||||
|
result = '/'.join(sanitized_parts)
|
||||||
|
if is_absolute:
|
||||||
|
result = '/' + result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
# Process remaining components
|
|
||||||
for component in path_components:
|
|
||||||
if component: # Skip empty components
|
|
||||||
if len(component) > self.config['max_length']:
|
|
||||||
component = self._truncate_filename(component)
|
|
||||||
|
|
||||||
processed_components.append(component)
|
|
||||||
|
|
||||||
# Join with proper separator and normalize
|
|
||||||
result = os.path.normpath(os.path.join(*processed_components))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def create_path(self, path: str, mode: int = 0o755) -> bool:
|
def create_path(self, path: str, mode: int = 0o755) -> bool:
|
||||||
"""
|
"""
|
||||||
Create directory path with specified permissions.
|
Create directory path with specified permissions.
|
||||||
@ -272,6 +233,7 @@ class OsManager:
|
|||||||
logging.error(f"An error occurred while checking file existence: {e}")
|
logging.error(f"An error occurred while checking file existence: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class InternManager():
|
class InternManager():
|
||||||
|
|
||||||
def format_file_size(self, size_bytes: float) -> str:
|
def format_file_size(self, size_bytes: float) -> str:
|
||||||
|
@ -174,12 +174,12 @@ class TVShowManager:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
choices = [str(i) for i in range(0, max_int_input)]
|
choices = [str(i) for i in range(0, max_int_input)]
|
||||||
choices.extend(["q", "", "back"])
|
choices.extend(["q", "quit", "b", "back"])
|
||||||
|
|
||||||
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
|
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
|
||||||
last_command = key
|
last_command = key
|
||||||
|
|
||||||
if key.lower() == "q":
|
if key.lower() == "q" or key.lower() == "quit":
|
||||||
break
|
break
|
||||||
|
|
||||||
elif key == "":
|
elif key == "":
|
||||||
@ -188,7 +188,7 @@ class TVShowManager:
|
|||||||
if self.slice_end > total_items:
|
if self.slice_end > total_items:
|
||||||
self.slice_end = total_items
|
self.slice_end = total_items
|
||||||
|
|
||||||
elif key.lower() == "back" and research_func:
|
elif (key.lower() == "b" or key.lower() == "back") and research_func:
|
||||||
self.run_back_command(research_func)
|
self.run_back_command(research_func)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -205,19 +205,19 @@ class TVShowManager:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
choices = [str(i) for i in range(0, max_int_input)]
|
choices = [str(i) for i in range(0, max_int_input)]
|
||||||
choices.extend(["q", "", "back"])
|
choices.extend(["q", "quit", "b", "back"])
|
||||||
|
|
||||||
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
|
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
|
||||||
last_command = key
|
last_command = key
|
||||||
|
|
||||||
if key.lower() == "q":
|
if key.lower() == "q" or key.lower() == "quit":
|
||||||
break
|
break
|
||||||
|
|
||||||
elif key == "":
|
elif key == "":
|
||||||
self.slice_start = 0
|
self.slice_start = 0
|
||||||
self.slice_end = self.step
|
self.slice_end = self.step
|
||||||
|
|
||||||
elif key.lower() == "back" and research_func:
|
elif (key.lower() == "b" or key.lower() == "back") and research_func:
|
||||||
self.run_back_command(research_func)
|
self.run_back_command(research_func)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -125,7 +125,6 @@ def initialize():
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
# Create logger
|
# Create logger
|
||||||
@ -136,9 +135,39 @@ def main():
|
|||||||
search_functions = load_search_functions()
|
search_functions = load_search_functions()
|
||||||
logging.info(f"Load module in: {time.time() - start} s")
|
logging.info(f"Load module in: {time.time() - start} s")
|
||||||
|
|
||||||
# Create dynamic argument parser
|
# Create argument parser
|
||||||
parser = argparse.ArgumentParser(description='Script to download film and series from the internet.')
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Script to download movies and series from the internet. Use these commands to configure the script and control its behavior.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add arguments for the main configuration parameters
|
||||||
|
parser.add_argument(
|
||||||
|
'--add_siteName', type=bool, help='Enable or disable adding the site name to the file name (e.g., true/false).'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--disable_searchDomain', type=bool, help='Enable or disable searching in configured domains (e.g., true/false).'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--not_close', type=bool, help='If set to true, the script will not close the console after execution (e.g., true/false).'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add arguments for M3U8 configuration
|
||||||
|
parser.add_argument(
|
||||||
|
'--default_video_worker', type=int, help='Number of workers for video during M3U8 download (default: 12).'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--default_audio_worker', type=int, help='Number of workers for audio during M3U8 download (default: 12).'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add options for audio and subtitles
|
||||||
|
parser.add_argument(
|
||||||
|
'--specific_list_audio', type=str, help='Comma-separated list of specific audio languages to download (e.g., ita,eng).'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--specific_list_subtitles', type=str, help='Comma-separated list of specific subtitle languages to download (e.g., eng,spa).'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add arguments for search functions
|
||||||
color_map = {
|
color_map = {
|
||||||
"anime": "red",
|
"anime": "red",
|
||||||
"film_serie": "yellow",
|
"film_serie": "yellow",
|
||||||
@ -153,10 +182,35 @@ def main():
|
|||||||
long_option = alias
|
long_option = alias
|
||||||
parser.add_argument(f'-{short_option}', f'--{long_option}', action='store_true', help=f'Search for {alias.split("_")[0]} on streaming platforms.')
|
parser.add_argument(f'-{short_option}', f'--{long_option}', action='store_true', help=f'Search for {alias.split("_")[0]} on streaming platforms.')
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command-line arguments
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Mapping command-line arguments to functions
|
# Map command-line arguments to the config values
|
||||||
|
config_updates = {}
|
||||||
|
|
||||||
|
if args.add_siteName is not None:
|
||||||
|
config_updates['DEFAULT.add_siteName'] = args.add_siteName
|
||||||
|
if args.disable_searchDomain is not None:
|
||||||
|
config_updates['DEFAULT.disable_searchDomain'] = args.disable_searchDomain
|
||||||
|
if args.not_close is not None:
|
||||||
|
config_updates['DEFAULT.not_close'] = args.not_close
|
||||||
|
if args.default_video_worker is not None:
|
||||||
|
config_updates['M3U8_DOWNLOAD.default_video_worker'] = args.default_video_worker
|
||||||
|
if args.default_audio_worker is not None:
|
||||||
|
config_updates['M3U8_DOWNLOAD.default_audio_worker'] = args.default_audio_worker
|
||||||
|
if args.specific_list_audio is not None:
|
||||||
|
config_updates['M3U8_DOWNLOAD.specific_list_audio'] = args.specific_list_audio.split(',')
|
||||||
|
if args.specific_list_subtitles is not None:
|
||||||
|
config_updates['M3U8_DOWNLOAD.specific_list_subtitles'] = args.specific_list_subtitles.split(',')
|
||||||
|
|
||||||
|
# Apply the updates to the config file
|
||||||
|
for key, value in config_updates.items():
|
||||||
|
section, option = key.split('.')
|
||||||
|
config_manager.set_key(section, option, value)
|
||||||
|
|
||||||
|
config_manager.write_config()
|
||||||
|
|
||||||
|
# Map command-line arguments to functions
|
||||||
arg_to_function = {alias: func for alias, (func, _) in search_functions.items()}
|
arg_to_function = {alias: func for alias, (func, _) in search_functions.items()}
|
||||||
|
|
||||||
# Check which argument is provided and run the corresponding function
|
# Check which argument is provided and run the corresponding function
|
||||||
@ -188,4 +242,4 @@ def main():
|
|||||||
run_function(input_to_function[category])
|
run_function(input_to_function[category])
|
||||||
else:
|
else:
|
||||||
console.print("[red]Invalid category.")
|
console.print("[red]Invalid category.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
146
Test/Util/oss.py
Normal file
146
Test/Util/oss.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# 22.01.25
|
||||||
|
|
||||||
|
# Fix import
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
|
sys.path.append(src_path)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Import
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from StreamingCommunity.Util.os import OsManager
|
||||||
|
|
||||||
|
class TestOsManager(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.test_paths = {
|
||||||
|
'windows': {
|
||||||
|
'network': [
|
||||||
|
(r'\\server\share\folder\file.txt', r'\\server\share\folder\file.txt'),
|
||||||
|
(r'\\192.168.1.100\share\folder\file.txt', r'\\192.168.1.100\share\folder\file.txt'),
|
||||||
|
(r'\\server\share', r'\\server\share'),
|
||||||
|
(r'\\server\share\\folder//subfolder\file.txt', r'\\server\share\folder\subfolder\file.txt')
|
||||||
|
],
|
||||||
|
'drive': [
|
||||||
|
('C:\\folder\\file.txt', 'C:\\folder\\file.txt'),
|
||||||
|
('C:/folder/file.txt', 'C:\\folder\\file.txt'),
|
||||||
|
('D:\\Test\\file.txt', 'D:\\Test\\file.txt'),
|
||||||
|
('D:/Test/file.txt', 'D:\\Test\\file.txt')
|
||||||
|
],
|
||||||
|
'relative': [
|
||||||
|
('folder\\file.txt', 'folder\\file.txt'),
|
||||||
|
('folder/file.txt', 'folder\\file.txt'),
|
||||||
|
('.\\folder\\file.txt', 'folder\\file.txt')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'darwin': {
|
||||||
|
'absolute': [
|
||||||
|
('/media/TV/show.mp4', '/media/TV/show.mp4'),
|
||||||
|
('/Users/name/Documents/file.txt', '/Users/name/Documents/file.txt'),
|
||||||
|
('/media/TV/show.mp4', '/media/TV/show.mp4')
|
||||||
|
],
|
||||||
|
'relative': [
|
||||||
|
('folder/file.txt', 'folder/file.txt'),
|
||||||
|
('folder/file.txt', 'folder/file.txt')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'linux': {
|
||||||
|
'absolute': [
|
||||||
|
('/home/user/file.txt', '/home/user/file.txt'),
|
||||||
|
('/mnt/data/file.txt', '/mnt/data/file.txt'),
|
||||||
|
('/home/user/file.txt', '/home/user/file.txt')
|
||||||
|
],
|
||||||
|
'relative': [
|
||||||
|
('folder/file.txt', 'folder/file.txt'),
|
||||||
|
('folder/file.txt', 'folder/file.txt')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_sanitize_file(self):
|
||||||
|
with patch('platform.system', return_value='Windows'):
|
||||||
|
manager = OsManager()
|
||||||
|
test_cases = [
|
||||||
|
('file.txt', 'file.txt'),
|
||||||
|
('filéš.txt', 'files.txt')
|
||||||
|
]
|
||||||
|
for input_name, expected in test_cases:
|
||||||
|
with self.subTest(input_name=input_name):
|
||||||
|
result = manager.get_sanitize_file(input_name)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_windows_paths(self):
|
||||||
|
with patch('platform.system', return_value='Windows'):
|
||||||
|
manager = OsManager()
|
||||||
|
|
||||||
|
# Test network paths (including IP)
|
||||||
|
for input_path, expected in self.test_paths['windows']['network']:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
# Test drive paths
|
||||||
|
for input_path, expected in self.test_paths['windows']['drive']:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_macos_paths(self):
|
||||||
|
with patch('platform.system', return_value='Darwin'):
|
||||||
|
manager = OsManager()
|
||||||
|
|
||||||
|
# Test absolute paths
|
||||||
|
for input_path, expected in self.test_paths['darwin']['absolute']:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
# Test relative paths
|
||||||
|
for input_path, expected in self.test_paths['darwin']['relative']:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_linux_paths(self):
|
||||||
|
with patch('platform.system', return_value='Linux'):
|
||||||
|
manager = OsManager()
|
||||||
|
|
||||||
|
for input_path, expected in self.test_paths['linux']['absolute']:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_special_characters(self):
|
||||||
|
with patch('platform.system', return_value='Windows'):
|
||||||
|
manager = OsManager()
|
||||||
|
special_cases = [
|
||||||
|
('\\\\server\\share\\àèìòù\\file.txt', '\\\\server\\share\\aeiou\\file.txt'),
|
||||||
|
('D:\\Test\\åäö\\file.txt', 'D:\\Test\\aao\\file.txt'),
|
||||||
|
('\\\\192.168.1.100\\share\\tést\\file.txt', '\\\\192.168.1.100\\share\\test\\file.txt')
|
||||||
|
]
|
||||||
|
|
||||||
|
for input_path, expected in special_cases:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_network_paths_with_ip(self):
|
||||||
|
with patch('platform.system', return_value='Windows'):
|
||||||
|
manager = OsManager()
|
||||||
|
ip_paths = [
|
||||||
|
('\\\\192.168.1.100\\share\\folder', '\\\\192.168.1.100\\share\\folder'),
|
||||||
|
('\\\\10.0.0.50\\public\\data.txt', '\\\\10.0.0.50\\public\\data.txt'),
|
||||||
|
('\\\\172.16.254.1\\backup\\test.txt', '\\\\172.16.254.1\\backup\\test.txt'),
|
||||||
|
('\\\\192.168.1.100\\share\\folder\\sub dir\\file.txt',
|
||||||
|
'\\\\192.168.1.100\\share\\folder\\sub dir\\file.txt'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for input_path, expected in ip_paths:
|
||||||
|
with self.subTest(input_path=input_path):
|
||||||
|
result = manager.get_sanitize_path(input_path)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@ -122,5 +122,5 @@ if __name__ == "__main__":
|
|||||||
domain_to_use, _ = search_domain(site_name=site_name, base_url=f"https://{site_name}.{original_domain}", get_first=True)
|
domain_to_use, _ = search_domain(site_name=site_name, base_url=f"https://{site_name}.{original_domain}", get_first=True)
|
||||||
|
|
||||||
update_readme(alias, domain_to_use)
|
update_readme(alias, domain_to_use)
|
||||||
print("------------------------------------")
|
print("\n------------------------------------")
|
||||||
time.sleep(2)
|
time.sleep(1)
|
@ -61,8 +61,8 @@
|
|||||||
"streamingcommunity": {
|
"streamingcommunity": {
|
||||||
"domain": "ooo"
|
"domain": "ooo"
|
||||||
},
|
},
|
||||||
"altadefinizione": {
|
"altadefinizionegratis": {
|
||||||
"domain": "florist"
|
"domain": "info"
|
||||||
},
|
},
|
||||||
"guardaserie": {
|
"guardaserie": {
|
||||||
"domain": "academy"
|
"domain": "academy"
|
||||||
@ -82,7 +82,7 @@
|
|||||||
"domain": "so"
|
"domain": "so"
|
||||||
},
|
},
|
||||||
"cb01new": {
|
"cb01new": {
|
||||||
"domain": "lol"
|
"domain": "video"
|
||||||
},
|
},
|
||||||
"1337xx": {
|
"1337xx": {
|
||||||
"domain": "to"
|
"domain": "to"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user