StreamingCommunity 2.7.0__py3-none-any.whl → 2.8.0__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/ddl.py +2 -2
- StreamingCommunity/Api/Player/maxstream.py +7 -13
- StreamingCommunity/Api/Player/supervideo.py +7 -33
- StreamingCommunity/Api/Player/vixcloud.py +8 -80
- StreamingCommunity/Api/Site/1337xx/__init__.py +8 -1
- StreamingCommunity/Api/Site/1337xx/site.py +10 -16
- StreamingCommunity/Api/Site/1337xx/title.py +4 -1
- StreamingCommunity/Api/Site/animeunity/__init__.py +9 -2
- StreamingCommunity/Api/Site/animeunity/film_serie.py +7 -1
- StreamingCommunity/Api/Site/animeunity/site.py +8 -10
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +1 -1
- StreamingCommunity/Api/Site/cb01new/__init__.py +8 -1
- StreamingCommunity/Api/Site/cb01new/film.py +7 -1
- StreamingCommunity/Api/Site/cb01new/site.py +16 -15
- StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +9 -2
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +7 -1
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +10 -15
- StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +1 -1
- StreamingCommunity/Api/Site/guardaserie/__init__.py +9 -2
- StreamingCommunity/Api/Site/guardaserie/series.py +9 -1
- StreamingCommunity/Api/Site/guardaserie/site.py +12 -17
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +1 -1
- StreamingCommunity/Api/Site/mostraguarda/__init__.py +6 -2
- StreamingCommunity/Api/Site/mostraguarda/film.py +7 -3
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +9 -2
- StreamingCommunity/Api/Site/streamingcommunity/film.py +8 -1
- StreamingCommunity/Api/Site/streamingcommunity/series.py +14 -5
- StreamingCommunity/Api/Site/streamingcommunity/site.py +10 -15
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +2 -2
- StreamingCommunity/Api/Template/Util/__init__.py +0 -1
- StreamingCommunity/Api/Template/Util/get_domain.py +24 -66
- StreamingCommunity/Api/Template/Util/manage_ep.py +10 -5
- StreamingCommunity/Api/Template/config_loader.py +8 -8
- StreamingCommunity/Api/Template/site.py +3 -6
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +10 -13
- StreamingCommunity/Lib/Downloader/HLS/segments.py +11 -31
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +12 -9
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +109 -101
- StreamingCommunity/Lib/FFmpeg/__init__.py +1 -1
- StreamingCommunity/Lib/FFmpeg/capture.py +10 -12
- StreamingCommunity/Lib/FFmpeg/command.py +15 -14
- StreamingCommunity/Lib/FFmpeg/util.py +9 -38
- StreamingCommunity/Lib/M3U8/decryptor.py +72 -146
- StreamingCommunity/Lib/M3U8/estimator.py +8 -16
- StreamingCommunity/Lib/M3U8/parser.py +1 -17
- StreamingCommunity/Lib/M3U8/url_fixer.py +1 -4
- StreamingCommunity/Lib/TMBD/__init__.py +2 -0
- StreamingCommunity/Lib/TMBD/obj_tmbd.py +3 -17
- StreamingCommunity/Lib/TMBD/tmdb.py +4 -9
- StreamingCommunity/TelegramHelp/telegram_bot.py +50 -50
- StreamingCommunity/Upload/update.py +3 -2
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/color.py +1 -1
- StreamingCommunity/Util/{_jsonConfig.py → config_json.py} +148 -54
- StreamingCommunity/Util/headers.py +2 -38
- StreamingCommunity/Util/logger.py +72 -42
- StreamingCommunity/Util/message.py +8 -3
- StreamingCommunity/Util/os.py +41 -93
- StreamingCommunity/Util/table.py +8 -17
- StreamingCommunity/run.py +26 -34
- {StreamingCommunity-2.7.0.dist-info → StreamingCommunity-2.8.0.dist-info}/METADATA +165 -92
- StreamingCommunity-2.8.0.dist-info/RECORD +75 -0
- StreamingCommunity/Api/Template/Util/recall_search.py +0 -37
- StreamingCommunity/Lib/Downloader/HLS/proxyes.py +0 -110
- StreamingCommunity/Util/call_stack.py +0 -42
- StreamingCommunity/Util/console.py +0 -12
- StreamingCommunity-2.7.0.dist-info/RECORD +0 -79
- {StreamingCommunity-2.7.0.dist-info → StreamingCommunity-2.8.0.dist-info}/LICENSE +0 -0
- {StreamingCommunity-2.7.0.dist-info → StreamingCommunity-2.8.0.dist-info}/WHEEL +0 -0
- {StreamingCommunity-2.7.0.dist-info → StreamingCommunity-2.8.0.dist-info}/entry_points.txt +0 -0
- {StreamingCommunity-2.7.0.dist-info → StreamingCommunity-2.8.0.dist-info}/top_level.txt +0 -0
|
@@ -9,47 +9,55 @@ import psutil
|
|
|
9
9
|
import logging
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
# External library
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
# Internal utilities
|
|
13
17
|
from StreamingCommunity.Util.color import Colors
|
|
14
18
|
from StreamingCommunity.Util.os import internet_manager
|
|
15
|
-
from StreamingCommunity.Util.
|
|
16
|
-
from StreamingCommunity.Util._jsonConfig import config_manager
|
|
19
|
+
from StreamingCommunity.Util.config_json import config_manager, get_use_large_bar
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
# External libraries
|
|
20
23
|
from tqdm import tqdm
|
|
21
|
-
|
|
24
|
+
import qbittorrentapi
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
# Tor config
|
|
25
|
-
HOST =
|
|
26
|
-
PORT =
|
|
27
|
-
USERNAME =
|
|
28
|
-
PASSWORD =
|
|
28
|
+
HOST = config_manager.get_dict('QBIT_CONFIG', 'host')
|
|
29
|
+
PORT = config_manager.get_dict('QBIT_CONFIG', 'port')
|
|
30
|
+
USERNAME = config_manager.get_dict('QBIT_CONFIG', 'user')
|
|
31
|
+
PASSWORD = config_manager.get_dict('QBIT_CONFIG', 'pass')
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
#
|
|
32
|
-
USE_LARGE_BAR = not ("android" in sys.platform or "ios" in sys.platform)
|
|
34
|
+
# Variable
|
|
33
35
|
REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
|
34
|
-
|
|
36
|
+
console = Console()
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class TOR_downloader:
|
|
38
40
|
def __init__(self):
|
|
39
41
|
"""
|
|
40
42
|
Initializes the TorrentManager instance.
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
Parameters:
|
|
43
45
|
- host (str): IP address or hostname of the qBittorrent Web UI.
|
|
44
|
-
- port (int): Port
|
|
45
|
-
- username (str): Username for
|
|
46
|
-
- password (str): Password for
|
|
46
|
+
- port (int): Port of the qBittorrent Web UI.
|
|
47
|
+
- username (str): Username for accessing qBittorrent.
|
|
48
|
+
- password (str): Password for accessing qBittorrent.
|
|
47
49
|
"""
|
|
48
50
|
try:
|
|
49
51
|
console.print(f"[cyan]Connect to: [green]{HOST}:{PORT}")
|
|
50
|
-
self.qb = Client(
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
self.qb = qbittorrentapi.Client(
|
|
53
|
+
host=HOST,
|
|
54
|
+
port=PORT,
|
|
55
|
+
username=USERNAME,
|
|
56
|
+
password=PASSWORD
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
except:
|
|
60
|
+
logging.error("Start qbittorrent first.")
|
|
53
61
|
sys.exit(0)
|
|
54
62
|
|
|
55
63
|
self.username = USERNAME
|
|
@@ -65,7 +73,7 @@ class TOR_downloader:
|
|
|
65
73
|
Logs into the qBittorrent Web UI.
|
|
66
74
|
"""
|
|
67
75
|
try:
|
|
68
|
-
self.qb.
|
|
76
|
+
self.qb.auth_log_in()
|
|
69
77
|
self.logged_in = True
|
|
70
78
|
logging.info("Successfully logged in to qBittorrent.")
|
|
71
79
|
|
|
@@ -74,96 +82,87 @@ class TOR_downloader:
|
|
|
74
82
|
self.logged_in = False
|
|
75
83
|
|
|
76
84
|
def delete_magnet(self, torrent_info):
|
|
77
|
-
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
"""
|
|
86
|
+
Deletes a torrent if it is not downloadable (no seeds/peers).
|
|
87
|
+
|
|
88
|
+
Parameters:
|
|
89
|
+
- torrent_info: Object containing torrent information obtained from the qBittorrent API.
|
|
90
|
+
"""
|
|
91
|
+
if (int(torrent_info.dlspeed) == 0 and
|
|
92
|
+
int(torrent_info.num_leechs) == 0 and
|
|
93
|
+
int(torrent_info.num_seeds) == 0):
|
|
84
94
|
|
|
95
|
+
console.print(f"[bold red]Torrent not downloadable. Removing...[/bold red]")
|
|
85
96
|
try:
|
|
86
|
-
|
|
87
|
-
self.qb.delete_permanently(torrent_info['hash'])
|
|
88
|
-
|
|
97
|
+
self.qb.torrents_delete(delete_files=True, torrent_hashes=torrent_info.hash)
|
|
89
98
|
except Exception as delete_error:
|
|
90
|
-
logging.error(f"
|
|
99
|
+
logging.error(f"Error while removing torrent: {delete_error}")
|
|
91
100
|
|
|
92
|
-
# Resetta l'ultimo hash
|
|
93
101
|
self.latest_torrent_hash = None
|
|
94
102
|
|
|
95
103
|
def add_magnet_link(self, magnet_link):
|
|
96
104
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
magnet_link (str): Magnet link
|
|
105
|
+
Adds a magnet link and retrieves detailed torrent information.
|
|
106
|
+
|
|
107
|
+
Arguments:
|
|
108
|
+
magnet_link (str): Magnet link to add.
|
|
101
109
|
|
|
102
110
|
Returns:
|
|
103
|
-
dict:
|
|
111
|
+
dict: Information about the added torrent, or None in case of error.
|
|
104
112
|
"""
|
|
105
|
-
|
|
106
|
-
# Estrai l'hash dal magnet link
|
|
107
113
|
magnet_hash_match = re.search(r'urn:btih:([0-9a-fA-F]+)', magnet_link)
|
|
108
|
-
|
|
109
114
|
if not magnet_hash_match:
|
|
110
|
-
raise ValueError("
|
|
115
|
+
raise ValueError("Magnet link hash not found")
|
|
111
116
|
|
|
112
117
|
magnet_hash = magnet_hash_match.group(1).lower()
|
|
113
118
|
|
|
114
|
-
#
|
|
119
|
+
# Extract the torrent name, if available
|
|
115
120
|
name_match = re.search(r'dn=([^&]+)', magnet_link)
|
|
116
|
-
torrent_name = name_match.group(1).replace('+', ' ') if name_match else "
|
|
121
|
+
torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Name not available"
|
|
117
122
|
|
|
118
|
-
#
|
|
123
|
+
# Save the timestamp before adding the torrent
|
|
119
124
|
before_add_time = time.time()
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.qb.download_from_link(magnet_link)
|
|
126
|
+
console.print(f"[cyan]Adding magnet link ...")
|
|
127
|
+
self.qb.torrents_add(urls=magnet_link)
|
|
124
128
|
|
|
125
|
-
# Aspetta un attimo per essere sicuri che il torrent sia stato aggiunto
|
|
126
129
|
time.sleep(1)
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
torrents = self.qb.torrents()
|
|
131
|
+
torrents = self.qb.torrents_info()
|
|
130
132
|
matching_torrents = [
|
|
131
133
|
t for t in torrents
|
|
132
|
-
if (t
|
|
134
|
+
if (t.hash.lower() == magnet_hash) or (getattr(t, 'added_on', 0) > before_add_time)
|
|
133
135
|
]
|
|
134
136
|
|
|
135
137
|
if not matching_torrents:
|
|
136
|
-
raise ValueError("
|
|
138
|
+
raise ValueError("No matching torrent found")
|
|
137
139
|
|
|
138
|
-
# Prendi il primo torrent corrispondente
|
|
139
140
|
torrent_info = matching_torrents[0]
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
console.print("
|
|
143
|
-
console.print(f"[yellow]
|
|
144
|
-
console.print(f"[yellow]Hash:[/yellow] {torrent_info['hash']}")
|
|
142
|
+
console.print("\n[bold green]Added Torrent Details:[/bold green]")
|
|
143
|
+
console.print(f"[yellow]Name:[/yellow] {torrent_info.name or torrent_name}")
|
|
144
|
+
console.print(f"[yellow]Hash:[/yellow] {torrent_info.hash}")
|
|
145
145
|
print()
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
self.
|
|
149
|
-
self.
|
|
150
|
-
self.file_name = torrent_info['name']
|
|
147
|
+
self.latest_torrent_hash = torrent_info.hash
|
|
148
|
+
self.output_file = torrent_info.content_path
|
|
149
|
+
self.file_name = torrent_info.name
|
|
151
150
|
|
|
152
|
-
#
|
|
151
|
+
# Wait and verify if the download is possible
|
|
153
152
|
time.sleep(5)
|
|
154
|
-
self.delete_magnet(self.qb.
|
|
153
|
+
self.delete_magnet(self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0])
|
|
155
154
|
|
|
156
155
|
return torrent_info
|
|
157
156
|
|
|
158
157
|
def start_download(self):
|
|
159
158
|
"""
|
|
160
|
-
Starts downloading the
|
|
161
|
-
"""
|
|
159
|
+
Starts downloading the added torrent and monitors its progress.
|
|
160
|
+
"""
|
|
162
161
|
if self.latest_torrent_hash is not None:
|
|
163
162
|
try:
|
|
164
|
-
|
|
165
|
-
# Custom bar for mobile and
|
|
166
|
-
if
|
|
163
|
+
|
|
164
|
+
# Custom progress bar for mobile and PC
|
|
165
|
+
if get_use_large_bar():
|
|
167
166
|
bar_format = (
|
|
168
167
|
f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
|
169
168
|
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
|
@@ -189,35 +188,42 @@ class TOR_downloader:
|
|
|
189
188
|
with progress_bar as pbar:
|
|
190
189
|
while True:
|
|
191
190
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
self.
|
|
195
|
-
self.torrent_name = torrent_info['name']
|
|
191
|
+
torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]
|
|
192
|
+
self.save_path = torrent_info.save_path
|
|
193
|
+
self.torrent_name = torrent_info.name
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
pieces_have = torrent_info['pieces_have']
|
|
199
|
-
pieces_num = torrent_info['pieces_num']
|
|
200
|
-
progress = (pieces_have / pieces_num) * 100 if pieces_num else 0
|
|
195
|
+
progress = torrent_info.progress * 100
|
|
201
196
|
pbar.n = progress
|
|
202
197
|
|
|
203
|
-
download_speed = torrent_info
|
|
204
|
-
total_size = torrent_info
|
|
205
|
-
downloaded_size = torrent_info
|
|
198
|
+
download_speed = torrent_info.dlspeed
|
|
199
|
+
total_size = torrent_info.size
|
|
200
|
+
downloaded_size = torrent_info.downloaded
|
|
206
201
|
|
|
207
|
-
# Format
|
|
202
|
+
# Format the downloaded size
|
|
208
203
|
downloaded_size_str = internet_manager.format_file_size(downloaded_size)
|
|
209
204
|
downloaded_size = downloaded_size_str.split(' ')[0]
|
|
210
205
|
|
|
206
|
+
# Safely format the total size
|
|
211
207
|
total_size_str = internet_manager.format_file_size(total_size)
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
total_size_parts = total_size_str.split(' ')
|
|
209
|
+
if len(total_size_parts) >= 2:
|
|
210
|
+
total_size = total_size_parts[0]
|
|
211
|
+
total_size_unit = total_size_parts[1]
|
|
212
|
+
else:
|
|
213
|
+
total_size = total_size_str
|
|
214
|
+
total_size_unit = ""
|
|
214
215
|
|
|
216
|
+
# Safely format the average download speed
|
|
215
217
|
average_internet_str = internet_manager.format_transfer_speed(download_speed)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
average_internet_parts = average_internet_str.split(' ')
|
|
219
|
+
if len(average_internet_parts) >= 2:
|
|
220
|
+
average_internet = average_internet_parts[0]
|
|
221
|
+
average_internet_unit = average_internet_parts[1]
|
|
222
|
+
else:
|
|
223
|
+
average_internet = average_internet_str
|
|
224
|
+
average_internet_unit = ""
|
|
218
225
|
|
|
219
|
-
|
|
220
|
-
if USE_LARGE_BAR:
|
|
226
|
+
if get_use_large_bar():
|
|
221
227
|
pbar.set_postfix_str(
|
|
222
228
|
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size} {Colors.WHITE}< {Colors.GREEN}{total_size} {Colors.RED}{total_size_unit} "
|
|
223
229
|
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
|
@@ -231,7 +237,6 @@ class TOR_downloader:
|
|
|
231
237
|
pbar.refresh()
|
|
232
238
|
time.sleep(0.2)
|
|
233
239
|
|
|
234
|
-
# Break at the end
|
|
235
240
|
if int(progress) == 100:
|
|
236
241
|
break
|
|
237
242
|
|
|
@@ -239,7 +244,15 @@ class TOR_downloader:
|
|
|
239
244
|
logging.info("Download process interrupted.")
|
|
240
245
|
|
|
241
246
|
def is_file_in_use(self, file_path: str) -> bool:
|
|
242
|
-
"""
|
|
247
|
+
"""
|
|
248
|
+
Checks if a file is being used by any process.
|
|
249
|
+
|
|
250
|
+
Parameters:
|
|
251
|
+
- file_path (str): The file path to check.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
- bool: True if the file is in use, False otherwise.
|
|
255
|
+
"""
|
|
243
256
|
for proc in psutil.process_iter(['open_files']):
|
|
244
257
|
try:
|
|
245
258
|
if any(file_path == f.path for f in proc.info['open_files'] or []):
|
|
@@ -251,21 +264,20 @@ class TOR_downloader:
|
|
|
251
264
|
|
|
252
265
|
def move_downloaded_files(self, destination: str):
|
|
253
266
|
"""
|
|
254
|
-
Moves downloaded files of the
|
|
255
|
-
|
|
267
|
+
Moves the downloaded files of the most recent torrent to a new location.
|
|
268
|
+
|
|
256
269
|
Parameters:
|
|
257
|
-
- destination (str): Destination
|
|
258
|
-
|
|
270
|
+
- destination (str): Destination folder.
|
|
271
|
+
|
|
259
272
|
Returns:
|
|
260
|
-
- bool: True if
|
|
273
|
+
- bool: True if the move was successful, False otherwise.
|
|
261
274
|
"""
|
|
262
275
|
console.print(f"[cyan]Destination folder: [red]{destination}")
|
|
263
276
|
|
|
264
277
|
try:
|
|
265
|
-
|
|
266
|
-
# Ensure the file is not in use
|
|
267
|
-
timeout = 3
|
|
278
|
+
timeout = 5
|
|
268
279
|
elapsed = 0
|
|
280
|
+
|
|
269
281
|
while self.is_file_in_use(self.output_file) and elapsed < timeout:
|
|
270
282
|
time.sleep(1)
|
|
271
283
|
elapsed += 1
|
|
@@ -273,24 +285,20 @@ class TOR_downloader:
|
|
|
273
285
|
if elapsed == timeout:
|
|
274
286
|
raise Exception(f"File '{self.output_file}' is in use and could not be moved.")
|
|
275
287
|
|
|
276
|
-
# Ensure destination directory exists
|
|
277
288
|
os.makedirs(destination, exist_ok=True)
|
|
278
289
|
|
|
279
|
-
# Perform the move operation
|
|
280
290
|
try:
|
|
281
291
|
shutil.move(self.output_file, destination)
|
|
282
|
-
|
|
283
292
|
except OSError as e:
|
|
284
|
-
if e.errno == 17: #
|
|
285
|
-
# Perform copy and delete manually
|
|
293
|
+
if e.errno == 17: # Error when moving between different disks
|
|
286
294
|
shutil.copy2(self.output_file, destination)
|
|
287
295
|
os.remove(self.output_file)
|
|
288
296
|
else:
|
|
289
297
|
raise
|
|
290
298
|
|
|
291
|
-
# Delete the torrent data
|
|
292
299
|
time.sleep(5)
|
|
293
|
-
self.qb.
|
|
300
|
+
last_torrent = self.qb.torrents_info()[-1]
|
|
301
|
+
self.qb.torrents_delete(delete_files=True, torrent_hashes=last_torrent.hash)
|
|
294
302
|
return True
|
|
295
303
|
|
|
296
304
|
except Exception as e:
|
|
@@ -6,12 +6,16 @@ import threading
|
|
|
6
6
|
import subprocess
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
# External library
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
# Internal utilities
|
|
10
|
-
from StreamingCommunity.Util.console import console
|
|
11
14
|
from StreamingCommunity.Util.os import internet_manager
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
# Variable
|
|
18
|
+
console = Console()
|
|
15
19
|
terminate_flag = threading.Event()
|
|
16
20
|
|
|
17
21
|
|
|
@@ -24,8 +28,6 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
24
28
|
- description (str): Description of the command being executed.
|
|
25
29
|
"""
|
|
26
30
|
try:
|
|
27
|
-
|
|
28
|
-
# Variable to store the length of the longest progress string
|
|
29
31
|
max_length = 0
|
|
30
32
|
|
|
31
33
|
for line in iter(process.stdout.readline, ''):
|
|
@@ -59,7 +61,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
# Construct the progress string with formatted output information
|
|
62
|
-
progress_string = (f"
|
|
64
|
+
progress_string = (f" {description}[white]: "
|
|
63
65
|
f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
|
|
64
66
|
f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white])")
|
|
65
67
|
max_length = max(max_length, len(progress_string))
|
|
@@ -94,10 +96,7 @@ def parse_output_line(line: str) -> dict:
|
|
|
94
96
|
dict: A dictionary containing parsed information.
|
|
95
97
|
"""
|
|
96
98
|
try:
|
|
97
|
-
|
|
98
99
|
data = {}
|
|
99
|
-
|
|
100
|
-
# Split the line by whitespace and extract key-value pairs
|
|
101
100
|
parts = line.replace(" ", "").replace("= ", "=").split()
|
|
102
101
|
|
|
103
102
|
for part in parts:
|
|
@@ -123,7 +122,7 @@ def terminate_process(process):
|
|
|
123
122
|
- process (subprocess.Popen): The subprocess to terminate.
|
|
124
123
|
"""
|
|
125
124
|
try:
|
|
126
|
-
if process.poll() is None:
|
|
125
|
+
if process.poll() is None:
|
|
127
126
|
process.kill()
|
|
128
127
|
except Exception as e:
|
|
129
128
|
logging.error(f"Failed to terminate process: {e}")
|
|
@@ -137,7 +136,6 @@ def capture_ffmpeg_real_time(ffmpeg_command: list, description: str) -> None:
|
|
|
137
136
|
- ffmpeg_command (list): The command to execute ffmpeg.
|
|
138
137
|
- description (str): Description of the command being executed.
|
|
139
138
|
"""
|
|
140
|
-
|
|
141
139
|
global terminate_flag
|
|
142
140
|
|
|
143
141
|
# Clear the terminate_flag before starting a new capture
|
|
@@ -163,8 +161,8 @@ def capture_ffmpeg_real_time(ffmpeg_command: list, description: str) -> None:
|
|
|
163
161
|
logging.error(f"Error in ffmpeg process: {e}")
|
|
164
162
|
|
|
165
163
|
finally:
|
|
166
|
-
terminate_flag.set()
|
|
167
|
-
output_thread.join()
|
|
164
|
+
terminate_flag.set()
|
|
165
|
+
output_thread.join()
|
|
168
166
|
|
|
169
167
|
except Exception as e:
|
|
170
|
-
logging.error(f"Failed to start ffmpeg process: {e}")
|
|
168
|
+
logging.error(f"Failed to start ffmpeg process: {e}")
|
|
@@ -6,10 +6,13 @@ import subprocess
|
|
|
6
6
|
from typing import List, Dict, Tuple, Optional
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
# External library
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
# Internal utilities
|
|
10
|
-
from StreamingCommunity.Util.
|
|
11
|
-
from StreamingCommunity.Util.os import os_manager,
|
|
12
|
-
from StreamingCommunity.Util.console import console
|
|
14
|
+
from StreamingCommunity.Util.config_json import config_manager, get_use_large_bar
|
|
15
|
+
from StreamingCommunity.Util.os import os_manager, suppress_output, get_ffmpeg_path
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
# Logic class
|
|
@@ -30,9 +33,7 @@ FFMPEG_DEFAULT_PRESET = config_manager.get("M3U8_CONVERSION", "default_preset")
|
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
# Variable
|
|
33
|
-
|
|
34
|
-
FFMPEG_PATH = os_summary.ffmpeg_path
|
|
35
|
-
|
|
36
|
+
console = Console()
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def check_subtitle_encoders() -> Tuple[Optional[bool], Optional[bool]]:
|
|
@@ -45,7 +46,7 @@ def check_subtitle_encoders() -> Tuple[Optional[bool], Optional[bool]]:
|
|
|
45
46
|
"""
|
|
46
47
|
try:
|
|
47
48
|
result = subprocess.run(
|
|
48
|
-
[
|
|
49
|
+
[get_ffmpeg_path(), '-encoders'],
|
|
49
50
|
capture_output=True,
|
|
50
51
|
text=True,
|
|
51
52
|
check=True
|
|
@@ -99,7 +100,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
99
100
|
- out_path (str): The path to save the output file.
|
|
100
101
|
- codec (M3U8_Codec): The video codec to use. Defaults to 'copy'.
|
|
101
102
|
"""
|
|
102
|
-
ffmpeg_cmd = [
|
|
103
|
+
ffmpeg_cmd = [get_ffmpeg_path()]
|
|
103
104
|
|
|
104
105
|
# Enabled the use of gpu
|
|
105
106
|
if USE_GPU:
|
|
@@ -156,7 +157,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
156
157
|
subprocess.run(ffmpeg_cmd, check=True)
|
|
157
158
|
else:
|
|
158
159
|
|
|
159
|
-
if
|
|
160
|
+
if get_use_large_bar():
|
|
160
161
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video")
|
|
161
162
|
print()
|
|
162
163
|
|
|
@@ -182,7 +183,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
182
183
|
video_audio_same_duration = check_duration_v_a(video_path, audio_tracks[0].get('path'))
|
|
183
184
|
|
|
184
185
|
# Start command with locate ffmpeg
|
|
185
|
-
ffmpeg_cmd = [
|
|
186
|
+
ffmpeg_cmd = [get_ffmpeg_path()]
|
|
186
187
|
|
|
187
188
|
# Enabled the use of gpu
|
|
188
189
|
if USE_GPU:
|
|
@@ -252,7 +253,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
252
253
|
subprocess.run(ffmpeg_cmd, check=True)
|
|
253
254
|
else:
|
|
254
255
|
|
|
255
|
-
if
|
|
256
|
+
if get_use_large_bar():
|
|
256
257
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio")
|
|
257
258
|
print()
|
|
258
259
|
|
|
@@ -275,7 +276,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
|
|
|
275
276
|
Each dictionary should contain the 'path' key with the path to the subtitle file and the 'name' key with the name of the subtitle.
|
|
276
277
|
- out_path (str): The path to save the output file.
|
|
277
278
|
"""
|
|
278
|
-
ffmpeg_cmd = [
|
|
279
|
+
ffmpeg_cmd = [get_ffmpeg_path(), "-i", video_path]
|
|
279
280
|
|
|
280
281
|
# Add subtitle input files first
|
|
281
282
|
for subtitle in subtitles_list:
|
|
@@ -305,9 +306,9 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
|
|
|
305
306
|
# Run join
|
|
306
307
|
if DEBUG_MODE:
|
|
307
308
|
subprocess.run(ffmpeg_cmd, check=True)
|
|
308
|
-
else:
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
else:
|
|
311
|
+
if get_use_large_bar():
|
|
311
312
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle")
|
|
312
313
|
print()
|
|
313
314
|
|
|
@@ -8,14 +8,16 @@ import logging
|
|
|
8
8
|
from typing import Tuple
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
# External library
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
# Internal utilities
|
|
12
|
-
from StreamingCommunity.Util.
|
|
13
|
-
from StreamingCommunity.Util.os import os_summary
|
|
16
|
+
from StreamingCommunity.Util.os import get_ffprobe_path
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
# Variable
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
console = Console()
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def has_audio_stream(video_path: str) -> bool:
|
|
@@ -29,7 +31,7 @@ def has_audio_stream(video_path: str) -> bool:
|
|
|
29
31
|
has_audio (bool): True if the input video has an audio stream, False otherwise.
|
|
30
32
|
"""
|
|
31
33
|
try:
|
|
32
|
-
ffprobe_cmd = [
|
|
34
|
+
ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-print_format', 'json', '-select_streams', 'a', '-show_streams', video_path]
|
|
33
35
|
logging.info(f"FFmpeg command: {ffprobe_cmd}")
|
|
34
36
|
|
|
35
37
|
with subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as proc:
|
|
@@ -57,7 +59,7 @@ def get_video_duration(file_path: str) -> float:
|
|
|
57
59
|
"""
|
|
58
60
|
|
|
59
61
|
try:
|
|
60
|
-
ffprobe_cmd = [
|
|
62
|
+
ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-show_format', '-print_format', 'json', file_path]
|
|
61
63
|
logging.info(f"FFmpeg command: {ffprobe_cmd}")
|
|
62
64
|
|
|
63
65
|
# Use a with statement to ensure the subprocess is cleaned up properly
|
|
@@ -83,37 +85,6 @@ def get_video_duration(file_path: str) -> float:
|
|
|
83
85
|
sys.exit(0)
|
|
84
86
|
|
|
85
87
|
|
|
86
|
-
def get_video_duration_s(filename):
|
|
87
|
-
"""
|
|
88
|
-
Get the duration of a video file using ffprobe.
|
|
89
|
-
|
|
90
|
-
Parameters:
|
|
91
|
-
- filename (str): Path to the video file (e.g., 'sim.mp4')
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
- duration (float): Duration of the video in seconds, or None if an error occurs.
|
|
95
|
-
"""
|
|
96
|
-
ffprobe_cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', filename]
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
|
|
100
|
-
# Run ffprobe command and capture output
|
|
101
|
-
result = subprocess.run(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True)
|
|
102
|
-
|
|
103
|
-
# Extract duration from the output
|
|
104
|
-
duration_str = result.stdout.strip()
|
|
105
|
-
duration = float(duration_str) # Convert duration to float
|
|
106
|
-
|
|
107
|
-
return int(duration)
|
|
108
|
-
|
|
109
|
-
except subprocess.CalledProcessError as e:
|
|
110
|
-
print(f"Error running ffprobe: {e}")
|
|
111
|
-
return None
|
|
112
|
-
except ValueError as e:
|
|
113
|
-
print(f"Error converting duration to float: {e}")
|
|
114
|
-
return None
|
|
115
|
-
|
|
116
|
-
|
|
117
88
|
def format_duration(seconds: float) -> Tuple[int, int, int]:
|
|
118
89
|
"""
|
|
119
90
|
Format duration in seconds into hours, minutes, and seconds.
|
|
@@ -172,7 +143,7 @@ def get_ffprobe_info(file_path):
|
|
|
172
143
|
"""
|
|
173
144
|
try:
|
|
174
145
|
result = subprocess.run(
|
|
175
|
-
[
|
|
146
|
+
[get_ffprobe_path(), '-v', 'error', '-show_format', '-show_streams', '-print_format', 'json', file_path],
|
|
176
147
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True
|
|
177
148
|
)
|
|
178
149
|
output = result.stdout
|