StreamingCommunity 1.9.8__py3-none-any.whl → 1.9.90__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of StreamingCommunity might be problematic. Click here for more details.
- StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py +143 -0
- StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +145 -0
- StreamingCommunity/Api/Player/ddl.py +89 -0
- StreamingCommunity/Api/Player/maxstream.py +151 -0
- StreamingCommunity/Api/Player/supervideo.py +194 -0
- StreamingCommunity/Api/Player/vixcloud.py +273 -0
- StreamingCommunity/Api/Site/1337xx/__init__.py +51 -0
- StreamingCommunity/Api/Site/1337xx/costant.py +15 -0
- StreamingCommunity/Api/Site/1337xx/site.py +86 -0
- StreamingCommunity/Api/Site/1337xx/title.py +66 -0
- StreamingCommunity/Api/Site/altadefinizione/__init__.py +51 -0
- StreamingCommunity/Api/Site/altadefinizione/costant.py +15 -0
- StreamingCommunity/Api/Site/altadefinizione/film.py +74 -0
- StreamingCommunity/Api/Site/altadefinizione/site.py +89 -0
- StreamingCommunity/Api/Site/animeunity/__init__.py +51 -0
- StreamingCommunity/Api/Site/animeunity/costant.py +15 -0
- StreamingCommunity/Api/Site/animeunity/film_serie.py +135 -0
- StreamingCommunity/Api/Site/animeunity/site.py +167 -0
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +97 -0
- StreamingCommunity/Api/Site/cb01new/__init__.py +52 -0
- StreamingCommunity/Api/Site/cb01new/costant.py +15 -0
- StreamingCommunity/Api/Site/cb01new/film.py +73 -0
- StreamingCommunity/Api/Site/cb01new/site.py +76 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +58 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/costant.py +16 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +146 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +95 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +85 -0
- StreamingCommunity/Api/Site/guardaserie/__init__.py +53 -0
- StreamingCommunity/Api/Site/guardaserie/costant.py +15 -0
- StreamingCommunity/Api/Site/guardaserie/series.py +199 -0
- StreamingCommunity/Api/Site/guardaserie/site.py +86 -0
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +110 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +52 -0
- StreamingCommunity/Api/Site/ilcorsaronero/costant.py +15 -0
- StreamingCommunity/Api/Site/ilcorsaronero/site.py +63 -0
- StreamingCommunity/Api/Site/ilcorsaronero/title.py +46 -0
- StreamingCommunity/Api/Site/ilcorsaronero/util/ilCorsarScraper.py +141 -0
- StreamingCommunity/Api/Site/mostraguarda/__init__.py +49 -0
- StreamingCommunity/Api/Site/mostraguarda/costant.py +15 -0
- StreamingCommunity/Api/Site/mostraguarda/film.py +99 -0
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +56 -0
- StreamingCommunity/Api/Site/streamingcommunity/costant.py +15 -0
- StreamingCommunity/Api/Site/streamingcommunity/film.py +75 -0
- StreamingCommunity/Api/Site/streamingcommunity/series.py +206 -0
- StreamingCommunity/Api/Site/streamingcommunity/site.py +137 -0
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +123 -0
- StreamingCommunity/Api/Template/Class/SearchType.py +101 -0
- StreamingCommunity/Api/Template/Util/__init__.py +5 -0
- StreamingCommunity/Api/Template/Util/get_domain.py +173 -0
- StreamingCommunity/Api/Template/Util/manage_ep.py +179 -0
- StreamingCommunity/Api/Template/Util/recall_search.py +37 -0
- StreamingCommunity/Api/Template/__init__.py +3 -0
- StreamingCommunity/Api/Template/site.py +87 -0
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +946 -0
- StreamingCommunity/Lib/Downloader/HLS/proxyes.py +110 -0
- StreamingCommunity/Lib/Downloader/HLS/segments.py +561 -0
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +155 -0
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +296 -0
- StreamingCommunity/Lib/Downloader/__init__.py +5 -0
- StreamingCommunity/Lib/FFmpeg/__init__.py +4 -0
- StreamingCommunity/Lib/FFmpeg/capture.py +170 -0
- StreamingCommunity/Lib/FFmpeg/command.py +296 -0
- StreamingCommunity/Lib/FFmpeg/util.py +249 -0
- StreamingCommunity/Lib/M3U8/__init__.py +6 -0
- StreamingCommunity/Lib/M3U8/decryptor.py +164 -0
- StreamingCommunity/Lib/M3U8/estimator.py +176 -0
- StreamingCommunity/Lib/M3U8/parser.py +666 -0
- StreamingCommunity/Lib/M3U8/url_fixer.py +52 -0
- StreamingCommunity/Lib/TMBD/__init__.py +2 -0
- StreamingCommunity/Lib/TMBD/obj_tmbd.py +39 -0
- StreamingCommunity/Lib/TMBD/tmdb.py +346 -0
- StreamingCommunity/Upload/update.py +68 -0
- StreamingCommunity/Upload/version.py +5 -0
- StreamingCommunity/Util/_jsonConfig.py +204 -0
- StreamingCommunity/Util/call_stack.py +42 -0
- StreamingCommunity/Util/color.py +20 -0
- StreamingCommunity/Util/console.py +12 -0
- StreamingCommunity/Util/ffmpeg_installer.py +311 -0
- StreamingCommunity/Util/headers.py +147 -0
- StreamingCommunity/Util/logger.py +53 -0
- StreamingCommunity/Util/message.py +64 -0
- StreamingCommunity/Util/os.py +554 -0
- StreamingCommunity/Util/table.py +229 -0
- StreamingCommunity/__init__.py +0 -0
- StreamingCommunity/run.py +2 -2
- {StreamingCommunity-1.9.8.dist-info → StreamingCommunity-1.9.90.dist-info}/METADATA +7 -10
- StreamingCommunity-1.9.90.dist-info/RECORD +92 -0
- {StreamingCommunity-1.9.8.dist-info → StreamingCommunity-1.9.90.dist-info}/WHEEL +1 -1
- {StreamingCommunity-1.9.8.dist-info → StreamingCommunity-1.9.90.dist-info}/entry_points.txt +0 -1
- StreamingCommunity-1.9.8.dist-info/RECORD +0 -7
- {StreamingCommunity-1.9.8.dist-info → StreamingCommunity-1.9.90.dist-info}/LICENSE +0 -0
- {StreamingCommunity-1.9.8.dist-info → StreamingCommunity-1.9.90.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# 09.06.24
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import ssl
|
|
6
|
+
import certifi
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# External libraries
|
|
11
|
+
import httpx
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Internal utilities
|
|
16
|
+
from StreamingCommunity.Util.headers import get_headers
|
|
17
|
+
from StreamingCommunity.Util.color import Colors
|
|
18
|
+
from StreamingCommunity.Util.console import console, Panel
|
|
19
|
+
from StreamingCommunity.Util._jsonConfig import config_manager
|
|
20
|
+
from StreamingCommunity.Util.os import internet_manager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Logic class
|
|
24
|
+
from ...FFmpeg import print_duration_table
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Suppress SSL warnings
|
|
28
|
+
import urllib3
|
|
29
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Config
|
|
33
|
+
GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
|
|
34
|
+
TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar')
|
|
35
|
+
REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = None):
|
|
40
|
+
"""
|
|
41
|
+
Downloads an MP4 video from a given URL with robust error handling and SSL bypass.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
- url (str): The URL of the MP4 video to download.
|
|
45
|
+
- path (str): The local path where the downloaded MP4 file will be saved.
|
|
46
|
+
- referer (str, optional): The referer header value.
|
|
47
|
+
- headers_ (dict, optional): Custom headers for the request.
|
|
48
|
+
"""
|
|
49
|
+
# Early return for link-only mode
|
|
50
|
+
if GET_ONLY_LINK:
|
|
51
|
+
return {'path': path, 'url': url}
|
|
52
|
+
|
|
53
|
+
# Validate URL
|
|
54
|
+
if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
|
|
55
|
+
logging.error(f"Invalid URL: {url}")
|
|
56
|
+
console.print(f"[bold red]Invalid URL: {url}[/bold red]")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Prepare headers
|
|
60
|
+
try:
|
|
61
|
+
headers = {}
|
|
62
|
+
if referer:
|
|
63
|
+
headers['Referer'] = referer
|
|
64
|
+
|
|
65
|
+
# Use custom headers if provided, otherwise use default user agent
|
|
66
|
+
if headers_:
|
|
67
|
+
headers.update(headers_)
|
|
68
|
+
else:
|
|
69
|
+
headers['User-Agent'] = get_headers()
|
|
70
|
+
|
|
71
|
+
except Exception as header_err:
|
|
72
|
+
logging.error(f"Error preparing headers: {header_err}")
|
|
73
|
+
console.print(f"[bold red]Error preparing headers: {header_err}[/bold red]")
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Create a custom transport that bypasses SSL verification
|
|
78
|
+
transport = httpx.HTTPTransport(
|
|
79
|
+
verify=False, # Disable SSL certificate verification
|
|
80
|
+
http2=True # Optional: enable HTTP/2 support
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Download with streaming and progress tracking
|
|
84
|
+
with httpx.Client(transport=transport, timeout=httpx.Timeout(60.0)) as client:
|
|
85
|
+
with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response:
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
|
|
88
|
+
# Get total file size
|
|
89
|
+
total = int(response.headers.get('content-length', 0))
|
|
90
|
+
|
|
91
|
+
# Handle empty streams
|
|
92
|
+
if total == 0:
|
|
93
|
+
console.print("[bold red]No video stream found.[/bold red]")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Create progress bar
|
|
97
|
+
progress_bar = tqdm(
|
|
98
|
+
total=total,
|
|
99
|
+
ascii='░▒█',
|
|
100
|
+
bar_format=f"{Colors.YELLOW}[MP4] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
|
101
|
+
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
|
102
|
+
f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] "
|
|
103
|
+
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}} {Colors.WHITE}| "
|
|
104
|
+
f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} {Colors.WHITE}]",
|
|
105
|
+
unit='iB',
|
|
106
|
+
unit_scale=True,
|
|
107
|
+
desc='Downloading',
|
|
108
|
+
mininterval=0.05
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Ensure directory exists
|
|
112
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Download file
|
|
115
|
+
with open(path, 'wb') as file, progress_bar as bar:
|
|
116
|
+
downloaded = 0
|
|
117
|
+
for chunk in response.iter_bytes(chunk_size=1024):
|
|
118
|
+
if chunk:
|
|
119
|
+
size = file.write(chunk)
|
|
120
|
+
downloaded += size
|
|
121
|
+
bar.update(size)
|
|
122
|
+
|
|
123
|
+
# Optional: Add a check to stop download if needed
|
|
124
|
+
# if downloaded > MAX_DOWNLOAD_SIZE:
|
|
125
|
+
# break
|
|
126
|
+
|
|
127
|
+
# Post-download processing
|
|
128
|
+
if os.path.exists(path) and os.path.getsize(path) > 0:
|
|
129
|
+
console.print(Panel(
|
|
130
|
+
f"[bold green]Download completed![/bold green]\n"
|
|
131
|
+
f"[cyan]File size: [bold red]{internet_manager.format_file_size(os.path.getsize(path))}[/bold red]\n"
|
|
132
|
+
f"[cyan]Duration: [bold]{print_duration_table(path, description=False, return_string=True)}[/bold]",
|
|
133
|
+
title=f"{os.path.basename(path.replace('.mp4', ''))}",
|
|
134
|
+
border_style="green"
|
|
135
|
+
))
|
|
136
|
+
return path
|
|
137
|
+
|
|
138
|
+
else:
|
|
139
|
+
console.print("[bold red]Download failed or file is empty.[/bold red]")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
except httpx.HTTPStatusError as http_err:
|
|
143
|
+
logging.error(f"HTTP error occurred: {http_err}")
|
|
144
|
+
console.print(f"[bold red]HTTP Error: {http_err}[/bold red]")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
except httpx.RequestError as req_err:
|
|
148
|
+
logging.error(f"Request error: {req_err}")
|
|
149
|
+
console.print(f"[bold red]Request Error: {req_err}[/bold red]")
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logging.error(f"Unexpected error during download: {e}")
|
|
154
|
+
console.print(f"[bold red]Unexpected Error: {e}[/bold red]")
|
|
155
|
+
return None
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# 23.06.24
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import shutil
|
|
8
|
+
import psutil
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Internal utilities
|
|
13
|
+
from StreamingCommunity.Util.color import Colors
|
|
14
|
+
from StreamingCommunity.Util.os import internet_manager
|
|
15
|
+
from StreamingCommunity.Util.console import console
|
|
16
|
+
from StreamingCommunity.Util._jsonConfig import config_manager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# External libraries
|
|
20
|
+
from tqdm import tqdm
|
|
21
|
+
from qbittorrent import Client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Tor config
|
|
25
|
+
HOST = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['host'])
|
|
26
|
+
PORT = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['port'])
|
|
27
|
+
USERNAME = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['user'])
|
|
28
|
+
PASSWORD = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['pass'])
|
|
29
|
+
|
|
30
|
+
# Config
|
|
31
|
+
TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar')
|
|
32
|
+
REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TOR_downloader:
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""
|
|
39
|
+
Initializes the TorrentManager instance.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
- host (str): IP address or hostname of the qBittorrent Web UI.
|
|
43
|
+
- port (int): Port number of the qBittorrent Web UI.
|
|
44
|
+
- username (str): Username for logging into qBittorrent.
|
|
45
|
+
- password (str): Password for logging into qBittorrent.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
console.print(f"[cyan]Connect to: [green]{HOST}:{PORT}")
|
|
49
|
+
self.qb = Client(f'http://{HOST}:{PORT}/')
|
|
50
|
+
except:
|
|
51
|
+
logging.error("Start qbitorrent first.")
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
self.username = USERNAME
|
|
55
|
+
self.password = PASSWORD
|
|
56
|
+
self.latest_torrent_hash = None
|
|
57
|
+
self.output_file = None
|
|
58
|
+
self.file_name = None
|
|
59
|
+
|
|
60
|
+
self.login()
|
|
61
|
+
|
|
62
|
+
def login(self):
|
|
63
|
+
"""
|
|
64
|
+
Logs into the qBittorrent Web UI.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
self.qb.login(self.username, self.password)
|
|
68
|
+
self.logged_in = True
|
|
69
|
+
logging.info("Successfully logged in to qBittorrent.")
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logging.error(f"Failed to log in: {str(e)}")
|
|
73
|
+
self.logged_in = False
|
|
74
|
+
|
|
75
|
+
def delete_magnet(self, torrent_info):
|
|
76
|
+
|
|
77
|
+
if (int(torrent_info.get('dl_speed')) == 0 and
|
|
78
|
+
int(torrent_info.get('peers')) == 0 and
|
|
79
|
+
int(torrent_info.get('seeds')) == 0):
|
|
80
|
+
|
|
81
|
+
# Elimina il torrent appena aggiunto
|
|
82
|
+
console.print(f"[bold red]⚠️ Torrent non scaricabile. Rimozione in corso...[/bold red]")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Rimuovi il torrent
|
|
86
|
+
self.qb.delete_permanently(torrent_info['hash'])
|
|
87
|
+
|
|
88
|
+
except Exception as delete_error:
|
|
89
|
+
logging.error(f"Errore durante la rimozione del torrent: {delete_error}")
|
|
90
|
+
|
|
91
|
+
# Resetta l'ultimo hash
|
|
92
|
+
self.latest_torrent_hash = None
|
|
93
|
+
|
|
94
|
+
def add_magnet_link(self, magnet_link):
|
|
95
|
+
"""
|
|
96
|
+
Aggiunge un magnet link e recupera le informazioni dettagliate.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
magnet_link (str): Magnet link da aggiungere
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
dict: Informazioni del torrent aggiunto, o None se fallisce
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Estrai l'hash dal magnet link
|
|
106
|
+
magnet_hash_match = re.search(r'urn:btih:([0-9a-fA-F]+)', magnet_link)
|
|
107
|
+
|
|
108
|
+
if not magnet_hash_match:
|
|
109
|
+
raise ValueError("Hash del magnet link non trovato")
|
|
110
|
+
|
|
111
|
+
magnet_hash = magnet_hash_match.group(1).lower()
|
|
112
|
+
|
|
113
|
+
# Estrai il nome del file dal magnet link (se presente)
|
|
114
|
+
name_match = re.search(r'dn=([^&]+)', magnet_link)
|
|
115
|
+
torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Nome non disponibile"
|
|
116
|
+
|
|
117
|
+
# Salva il timestamp prima di aggiungere il torrent
|
|
118
|
+
before_add_time = time.time()
|
|
119
|
+
|
|
120
|
+
# Aggiungi il magnet link
|
|
121
|
+
console.print(f"[cyan]Aggiunta magnet link[/cyan]: [red]{magnet_link}")
|
|
122
|
+
self.qb.download_from_link(magnet_link)
|
|
123
|
+
|
|
124
|
+
# Aspetta un attimo per essere sicuri che il torrent sia stato aggiunto
|
|
125
|
+
time.sleep(1)
|
|
126
|
+
|
|
127
|
+
# Cerca il torrent
|
|
128
|
+
torrents = self.qb.torrents()
|
|
129
|
+
matching_torrents = [
|
|
130
|
+
t for t in torrents
|
|
131
|
+
if (t['hash'].lower() == magnet_hash) or (t.get('added_on', 0) > before_add_time)
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
if not matching_torrents:
|
|
135
|
+
raise ValueError("Nessun torrent corrispondente trovato")
|
|
136
|
+
|
|
137
|
+
# Prendi il primo torrent corrispondente
|
|
138
|
+
torrent_info = matching_torrents[0]
|
|
139
|
+
|
|
140
|
+
# Formatta e stampa le informazioni
|
|
141
|
+
console.print("\n[bold green]🔗 Dettagli Torrent Aggiunto:[/bold green]")
|
|
142
|
+
console.print(f"[yellow]Nome:[/yellow] {torrent_info.get('name', torrent_name)}")
|
|
143
|
+
console.print(f"[yellow]Hash:[/yellow] {torrent_info['hash']}")
|
|
144
|
+
console.print(f"[yellow]Dimensione:[/yellow] {internet_manager.format_file_size(torrent_info.get('size'))}")
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
# Salva l'hash per usi successivi e il path
|
|
148
|
+
self.latest_torrent_hash = torrent_info['hash']
|
|
149
|
+
self.output_file = torrent_info['content_path']
|
|
150
|
+
self.file_name = torrent_info['name']
|
|
151
|
+
|
|
152
|
+
# Controlla che sia possibile il download
|
|
153
|
+
time.sleep(5)
|
|
154
|
+
self.delete_magnet(self.qb.get_torrent(self.latest_torrent_hash))
|
|
155
|
+
|
|
156
|
+
return torrent_info
|
|
157
|
+
|
|
158
|
+
def start_download(self):
|
|
159
|
+
"""
|
|
160
|
+
Starts downloading the latest added torrent and monitors progress.
|
|
161
|
+
"""
|
|
162
|
+
if self.latest_torrent_hash is not None:
|
|
163
|
+
try:
|
|
164
|
+
|
|
165
|
+
# Custom bar for mobile and pc
|
|
166
|
+
if TQDM_USE_LARGE_BAR:
|
|
167
|
+
bar_format = (
|
|
168
|
+
f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
|
169
|
+
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
|
170
|
+
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
bar_format = (
|
|
174
|
+
f"{Colors.YELLOW}Proc{Colors.WHITE}: "
|
|
175
|
+
f"{Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| "
|
|
176
|
+
f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
progress_bar = tqdm(
|
|
180
|
+
total=100,
|
|
181
|
+
ascii='░▒█',
|
|
182
|
+
bar_format=bar_format,
|
|
183
|
+
unit_scale=True,
|
|
184
|
+
unit_divisor=1024,
|
|
185
|
+
mininterval=0.05
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
with progress_bar as pbar:
|
|
189
|
+
while True:
|
|
190
|
+
|
|
191
|
+
# Get variable from qtorrent
|
|
192
|
+
torrent_info = self.qb.get_torrent(self.latest_torrent_hash)
|
|
193
|
+
self.save_path = torrent_info['save_path']
|
|
194
|
+
self.torrent_name = torrent_info['name']
|
|
195
|
+
|
|
196
|
+
# Fetch important variable
|
|
197
|
+
pieces_have = torrent_info['pieces_have']
|
|
198
|
+
pieces_num = torrent_info['pieces_num']
|
|
199
|
+
progress = (pieces_have / pieces_num) * 100 if pieces_num else 0
|
|
200
|
+
pbar.n = progress
|
|
201
|
+
|
|
202
|
+
download_speed = torrent_info['dl_speed']
|
|
203
|
+
total_size = torrent_info['total_size']
|
|
204
|
+
downloaded_size = torrent_info['total_downloaded']
|
|
205
|
+
|
|
206
|
+
# Format variable
|
|
207
|
+
downloaded_size_str = internet_manager.format_file_size(downloaded_size)
|
|
208
|
+
downloaded_size = downloaded_size_str.split(' ')[0]
|
|
209
|
+
|
|
210
|
+
total_size_str = internet_manager.format_file_size(total_size)
|
|
211
|
+
total_size = total_size_str.split(' ')[0]
|
|
212
|
+
total_size_unit = total_size_str.split(' ')[1]
|
|
213
|
+
|
|
214
|
+
average_internet_str = internet_manager.format_transfer_speed(download_speed)
|
|
215
|
+
average_internet = average_internet_str.split(' ')[0]
|
|
216
|
+
average_internet_unit = average_internet_str.split(' ')[1]
|
|
217
|
+
|
|
218
|
+
# Update the progress bar's postfix
|
|
219
|
+
if TQDM_USE_LARGE_BAR:
|
|
220
|
+
pbar.set_postfix_str(
|
|
221
|
+
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size} {Colors.WHITE}< {Colors.GREEN}{total_size} {Colors.RED}{total_size_unit} "
|
|
222
|
+
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
pbar.set_postfix_str(
|
|
226
|
+
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size}{Colors.RED} {total_size} "
|
|
227
|
+
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
pbar.refresh()
|
|
231
|
+
time.sleep(0.2)
|
|
232
|
+
|
|
233
|
+
# Break at the end
|
|
234
|
+
if int(progress) == 100:
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
except KeyboardInterrupt:
|
|
238
|
+
logging.info("Download process interrupted.")
|
|
239
|
+
|
|
240
|
+
def is_file_in_use(self, file_path: str) -> bool:
|
|
241
|
+
"""Check if a file is in use by any process."""
|
|
242
|
+
for proc in psutil.process_iter(['open_files']):
|
|
243
|
+
try:
|
|
244
|
+
if any(file_path == f.path for f in proc.info['open_files'] or []):
|
|
245
|
+
return True
|
|
246
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def move_downloaded_files(self, destination: str):
|
|
252
|
+
"""
|
|
253
|
+
Moves downloaded files of the latest torrent to another location.
|
|
254
|
+
|
|
255
|
+
Parameters:
|
|
256
|
+
- destination (str): Destination directory to move files.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
- bool: True if files are moved successfully, False otherwise.
|
|
260
|
+
"""
|
|
261
|
+
console.print(f"[cyan]Destination folder: [red]{destination}")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
|
|
265
|
+
# Ensure the file is not in use
|
|
266
|
+
timeout = 3
|
|
267
|
+
elapsed = 0
|
|
268
|
+
while self.is_file_in_use(self.output_file) and elapsed < timeout:
|
|
269
|
+
time.sleep(1)
|
|
270
|
+
elapsed += 1
|
|
271
|
+
|
|
272
|
+
if elapsed == timeout:
|
|
273
|
+
raise Exception(f"File '{self.output_file}' is in use and could not be moved.")
|
|
274
|
+
|
|
275
|
+
# Ensure destination directory exists
|
|
276
|
+
os.makedirs(destination, exist_ok=True)
|
|
277
|
+
|
|
278
|
+
# Perform the move operation
|
|
279
|
+
try:
|
|
280
|
+
shutil.move(self.output_file, destination)
|
|
281
|
+
|
|
282
|
+
except OSError as e:
|
|
283
|
+
if e.errno == 17: # Cross-disk move error
|
|
284
|
+
# Perform copy and delete manually
|
|
285
|
+
shutil.copy2(self.output_file, destination)
|
|
286
|
+
os.remove(self.output_file)
|
|
287
|
+
else:
|
|
288
|
+
raise
|
|
289
|
+
|
|
290
|
+
# Delete the torrent data
|
|
291
|
+
#self.qb.delete_permanently(self.qb.torrents()[-1]['hash'])
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
print(f"Error moving file: {e}")
|
|
296
|
+
return False
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# 16.04.24
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Internal utilities
|
|
10
|
+
from StreamingCommunity.Util.console import console
|
|
11
|
+
from StreamingCommunity.Util.os import internet_manager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Variable
|
|
15
|
+
terminate_flag = threading.Event()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Function to capture and print output from a subprocess.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
- process (subprocess.Popen): The subprocess whose output is captured.
|
|
24
|
+
- description (str): Description of the command being executed.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
|
|
28
|
+
# Variable to store the length of the longest progress string
|
|
29
|
+
max_length = 0
|
|
30
|
+
|
|
31
|
+
for line in iter(process.stdout.readline, ''):
|
|
32
|
+
try:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
logging.info(f"FFMPEG line: {line}")
|
|
38
|
+
|
|
39
|
+
# Capture only error
|
|
40
|
+
if "rror" in str(line):
|
|
41
|
+
console.log(f"[red]FFMPEG: {str(line).strip()}")
|
|
42
|
+
|
|
43
|
+
# Check if termination is requested
|
|
44
|
+
if terminate_flag.is_set():
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
if "size=" in line:
|
|
48
|
+
try:
|
|
49
|
+
|
|
50
|
+
# Parse the output line to extract relevant information
|
|
51
|
+
data = parse_output_line(line)
|
|
52
|
+
|
|
53
|
+
if 'q' in data:
|
|
54
|
+
is_end = (float(data.get('q', -1.0)) == -1.0)
|
|
55
|
+
size_key = 'Lsize' if is_end else 'size'
|
|
56
|
+
byte_size = int(re.findall(r'\d+', data.get(size_key, '0'))[0]) * 1000
|
|
57
|
+
else:
|
|
58
|
+
byte_size = int(re.findall(r'\d+', data.get('size', '0'))[0]) * 1000
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Construct the progress string with formatted output information
|
|
62
|
+
progress_string = (f"[yellow][FFmpeg] [white][{description}[white]]: "
|
|
63
|
+
f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
|
|
64
|
+
f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white])")
|
|
65
|
+
max_length = max(max_length, len(progress_string))
|
|
66
|
+
|
|
67
|
+
# Print the progress string to the console, overwriting the previous line
|
|
68
|
+
console.print(progress_string.ljust(max_length), end="\r")
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logging.error(f"Error parsing output line: {line} - {e}")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logging.error(f"Error processing line from subprocess: {e}")
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logging.error(f"Error in capture_output: {e}")
|
|
78
|
+
|
|
79
|
+
finally:
|
|
80
|
+
try:
|
|
81
|
+
terminate_process(process)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logging.error(f"Error terminating process: {e}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_output_line(line: str) -> dict:
|
|
87
|
+
"""
|
|
88
|
+
Function to parse the output line and extract relevant information.
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
- line (str): The output line to parse.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
dict: A dictionary containing parsed information.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
|
|
98
|
+
data = {}
|
|
99
|
+
|
|
100
|
+
# Split the line by whitespace and extract key-value pairs
|
|
101
|
+
parts = line.replace(" ", "").replace("= ", "=").split()
|
|
102
|
+
|
|
103
|
+
for part in parts:
|
|
104
|
+
key_value = part.split('=')
|
|
105
|
+
|
|
106
|
+
if len(key_value) == 2:
|
|
107
|
+
key = key_value[0]
|
|
108
|
+
value = key_value[1]
|
|
109
|
+
data[key] = value
|
|
110
|
+
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logging.error(f"Error parsing line: {line} - {e}")
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def terminate_process(process):
|
|
119
|
+
"""
|
|
120
|
+
Function to terminate a subprocess if it's still running.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
- process (subprocess.Popen): The subprocess to terminate.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
if process.poll() is None: # Check if the process is still running
|
|
127
|
+
process.kill()
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logging.error(f"Failed to terminate process: {e}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def capture_ffmpeg_real_time(ffmpeg_command: list, description: str) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Function to capture real-time output from ffmpeg process.
|
|
135
|
+
|
|
136
|
+
Parameters:
|
|
137
|
+
- ffmpeg_command (list): The command to execute ffmpeg.
|
|
138
|
+
- description (str): Description of the command being executed.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
global terminate_flag
|
|
142
|
+
|
|
143
|
+
# Clear the terminate_flag before starting a new capture
|
|
144
|
+
terminate_flag.clear()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
|
|
148
|
+
# Start the ffmpeg process with subprocess.Popen
|
|
149
|
+
process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
|
|
150
|
+
|
|
151
|
+
# Start a thread to capture and print output
|
|
152
|
+
output_thread = threading.Thread(target=capture_output, args=(process, description))
|
|
153
|
+
output_thread.start()
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Wait for ffmpeg process to complete
|
|
157
|
+
process.wait()
|
|
158
|
+
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
logging.error("Terminating ffmpeg process...")
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logging.error(f"Error in ffmpeg process: {e}")
|
|
164
|
+
|
|
165
|
+
finally:
|
|
166
|
+
terminate_flag.set() # Signal the output capture thread to terminate
|
|
167
|
+
output_thread.join() # Wait for the output capture thread to complete
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logging.error(f"Failed to start ffmpeg process: {e}")
|