StreamingCommunity 2.9.5__py3-none-any.whl → 2.9.6__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/sweetpixel.py +49 -0
- StreamingCommunity/Api/Site/1337xx/site.py +3 -3
- StreamingCommunity/Api/Site/1337xx/title.py +4 -6
- StreamingCommunity/Api/Site/altadefinizione/film.py +1 -1
- StreamingCommunity/Api/Site/altadefinizione/series.py +1 -2
- StreamingCommunity/Api/Site/altadefinizione/site.py +3 -3
- StreamingCommunity/Api/Site/animeunity/film_serie.py +3 -4
- StreamingCommunity/Api/Site/animeunity/site.py +4 -4
- StreamingCommunity/Api/Site/animeworld/__init__.py +71 -0
- StreamingCommunity/Api/Site/animeworld/serie.py +107 -0
- StreamingCommunity/Api/Site/animeworld/site.py +111 -0
- StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +79 -0
- StreamingCommunity/Api/Site/cb01new/film.py +1 -1
- StreamingCommunity/Api/Site/cb01new/site.py +3 -3
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +2 -2
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +3 -3
- StreamingCommunity/Api/Site/guardaserie/series.py +1 -1
- StreamingCommunity/Api/Site/guardaserie/site.py +3 -3
- StreamingCommunity/Api/Site/mostraguarda/film.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/film.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/series.py +1 -2
- StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -3
- StreamingCommunity/Lib/Downloader/HLS/segments.py +1 -3
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +397 -227
- StreamingCommunity/Lib/FFmpeg/util.py +12 -0
- StreamingCommunity/Lib/M3U8/estimator.py +5 -8
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/run.py +12 -2
- {streamingcommunity-2.9.5.dist-info → streamingcommunity-2.9.6.dist-info}/METADATA +4 -6
- {streamingcommunity-2.9.5.dist-info → streamingcommunity-2.9.6.dist-info}/RECORD +34 -29
- {streamingcommunity-2.9.5.dist-info → streamingcommunity-2.9.6.dist-info}/WHEEL +1 -1
- {streamingcommunity-2.9.5.dist-info → streamingcommunity-2.9.6.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-2.9.5.dist-info → streamingcommunity-2.9.6.dist-info/licenses}/LICENSE +0 -0
- {streamingcommunity-2.9.5.dist-info → streamingcommunity-2.9.6.dist-info}/top_level.txt +0 -0
|
@@ -4,13 +4,15 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
|
-
import shutil
|
|
8
7
|
import psutil
|
|
9
8
|
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
# External
|
|
12
|
+
# External libraries
|
|
13
13
|
from rich.console import Console
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
import qbittorrentapi
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
# Internal utilities
|
|
@@ -19,19 +21,12 @@ from StreamingCommunity.Util.os import internet_manager
|
|
|
19
21
|
from StreamingCommunity.Util.config_json import config_manager, get_use_large_bar
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Tor config
|
|
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')
|
|
32
|
-
|
|
24
|
+
# Configuration
|
|
25
|
+
HOST = config_manager.get('QBIT_CONFIG', 'host')
|
|
26
|
+
PORT = config_manager.get('QBIT_CONFIG', 'port')
|
|
27
|
+
USERNAME = config_manager.get('QBIT_CONFIG', 'user')
|
|
28
|
+
PASSWORD = config_manager.get('QBIT_CONFIG', 'pass')
|
|
33
29
|
|
|
34
|
-
# Variable
|
|
35
30
|
REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
|
36
31
|
console = Console()
|
|
37
32
|
|
|
@@ -39,268 +34,443 @@ console = Console()
|
|
|
39
34
|
class TOR_downloader:
|
|
40
35
|
def __init__(self):
|
|
41
36
|
"""
|
|
42
|
-
Initializes the
|
|
37
|
+
Initializes the TorrentDownloader instance and connects to qBittorrent.
|
|
38
|
+
"""
|
|
39
|
+
self.console = Console()
|
|
40
|
+
self.latest_torrent_hash = None
|
|
41
|
+
self.output_file = None
|
|
42
|
+
self.file_name = None
|
|
43
|
+
self.save_path = None
|
|
44
|
+
self.torrent_name = None
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
self._connect_to_client()
|
|
47
|
+
|
|
48
|
+
def _connect_to_client(self):
|
|
49
|
+
"""
|
|
50
|
+
Establishes connection to qBittorrent client using configuration parameters.
|
|
49
51
|
"""
|
|
52
|
+
self.console.print(f"[cyan]Connecting to qBittorrent: [green]{HOST}:{PORT}")
|
|
53
|
+
|
|
50
54
|
try:
|
|
51
|
-
|
|
55
|
+
# Create client with connection settings and timeouts
|
|
52
56
|
self.qb = qbittorrentapi.Client(
|
|
53
57
|
host=HOST,
|
|
54
58
|
port=PORT,
|
|
55
59
|
username=USERNAME,
|
|
56
|
-
password=PASSWORD
|
|
60
|
+
password=PASSWORD,
|
|
61
|
+
VERIFY_WEBUI_CERTIFICATE=False,
|
|
62
|
+
REQUESTS_ARGS={'timeout': REQUEST_TIMEOUT}
|
|
57
63
|
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
logging.error("Start qbittorrent first.")
|
|
61
|
-
sys.exit(0)
|
|
62
|
-
|
|
63
|
-
self.username = USERNAME
|
|
64
|
-
self.password = PASSWORD
|
|
65
|
-
self.latest_torrent_hash = None
|
|
66
|
-
self.output_file = None
|
|
67
|
-
self.file_name = None
|
|
68
|
-
|
|
69
|
-
self.login()
|
|
70
|
-
|
|
71
|
-
def login(self):
|
|
72
|
-
"""
|
|
73
|
-
Logs into the qBittorrent Web UI.
|
|
74
|
-
"""
|
|
75
|
-
try:
|
|
64
|
+
|
|
65
|
+
# Test connection and login
|
|
76
66
|
self.qb.auth_log_in()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
qb_version = self.qb.app.version
|
|
68
|
+
self.console.print(f"[green]Successfully connected to qBittorrent v{qb_version}")
|
|
69
|
+
|
|
80
70
|
except Exception as e:
|
|
81
|
-
logging.error(f"
|
|
82
|
-
self.
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
logging.error(f"Unexpected error: {str(e)}")
|
|
72
|
+
self.console.print(f"[bold red]Error initializing qBittorrent client: {str(e)}[/bold red]")
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
def add_magnet_link(self, magnet_link, save_path=None):
|
|
85
76
|
"""
|
|
86
|
-
|
|
77
|
+
Adds a magnet link to qBittorrent and retrieves torrent information.
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (int(torrent_info.dlspeed) == 0 and
|
|
92
|
-
int(torrent_info.num_leechs) == 0 and
|
|
93
|
-
int(torrent_info.num_seeds) == 0):
|
|
79
|
+
Args:
|
|
80
|
+
magnet_link (str): Magnet link to add to qBittorrent
|
|
81
|
+
save_path (str, optional): Directory where to save the downloaded files
|
|
94
82
|
|
|
95
|
-
console.print(f"[bold red]Torrent not downloadable. Removing...[/bold red]")
|
|
96
|
-
try:
|
|
97
|
-
self.qb.torrents_delete(delete_files=True, torrent_hashes=torrent_info.hash)
|
|
98
|
-
except Exception as delete_error:
|
|
99
|
-
logging.error(f"Error while removing torrent: {delete_error}")
|
|
100
|
-
|
|
101
|
-
self.latest_torrent_hash = None
|
|
102
|
-
|
|
103
|
-
def add_magnet_link(self, magnet_link):
|
|
104
|
-
"""
|
|
105
|
-
Adds a magnet link and retrieves detailed torrent information.
|
|
106
|
-
|
|
107
|
-
Arguments:
|
|
108
|
-
magnet_link (str): Magnet link to add.
|
|
109
|
-
|
|
110
83
|
Returns:
|
|
111
|
-
|
|
84
|
+
TorrentDictionary: Information about the added torrent
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If magnet link is invalid or torrent can't be added
|
|
112
88
|
"""
|
|
89
|
+
# Extract hash from magnet link
|
|
113
90
|
magnet_hash_match = re.search(r'urn:btih:([0-9a-fA-F]+)', magnet_link)
|
|
114
91
|
if not magnet_hash_match:
|
|
115
|
-
raise ValueError("
|
|
92
|
+
raise ValueError("Invalid magnet link: hash not found")
|
|
116
93
|
|
|
117
94
|
magnet_hash = magnet_hash_match.group(1).lower()
|
|
118
95
|
|
|
119
|
-
# Extract
|
|
96
|
+
# Extract torrent name from magnet link if available
|
|
120
97
|
name_match = re.search(r'dn=([^&]+)', magnet_link)
|
|
121
|
-
torrent_name = name_match.group(1).replace('+', ' ') if name_match else "
|
|
98
|
+
torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Unknown"
|
|
122
99
|
|
|
123
|
-
#
|
|
100
|
+
# Record timestamp before adding torrent for identification
|
|
124
101
|
before_add_time = time.time()
|
|
125
102
|
|
|
126
|
-
console.print(f"[cyan]Adding magnet link
|
|
127
|
-
self.qb.torrents_add(urls=magnet_link)
|
|
103
|
+
self.console.print(f"[cyan]Adding magnet link for: [yellow]{torrent_name}")
|
|
128
104
|
|
|
129
|
-
|
|
105
|
+
# Prepare save path
|
|
106
|
+
if save_path:
|
|
107
|
+
self.console.print(f"[cyan]Setting save location to: [green]{save_path}")
|
|
108
|
+
|
|
109
|
+
# Ensure save path exists
|
|
110
|
+
os.makedirs(save_path, exist_ok=True)
|
|
130
111
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
112
|
+
# Add the torrent with save options
|
|
113
|
+
add_options = {
|
|
114
|
+
"urls": magnet_link,
|
|
115
|
+
"use_auto_torrent_management": False, # Don't use automatic management
|
|
116
|
+
"is_paused": False, # Start download immediately
|
|
117
|
+
"tags": ["StreamingCommunity"] # Add tag for easy identification
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# If save_path is provided, add it to options
|
|
121
|
+
if save_path:
|
|
122
|
+
add_options["save_path"] = save_path
|
|
123
|
+
|
|
124
|
+
add_result = self.qb.torrents_add(**add_options)
|
|
125
|
+
|
|
126
|
+
if not add_result == "Ok.":
|
|
127
|
+
raise ValueError(f"Failed to add torrent: {add_result}")
|
|
128
|
+
|
|
129
|
+
# Wait for torrent to be recognized by the client
|
|
130
|
+
time.sleep(1.5)
|
|
131
|
+
|
|
132
|
+
# Find the newly added torrent
|
|
133
|
+
matching_torrents = self._find_torrent(magnet_hash, before_add_time)
|
|
136
134
|
|
|
137
135
|
if not matching_torrents:
|
|
138
|
-
raise ValueError("
|
|
136
|
+
raise ValueError("Torrent was added but couldn't be found in client")
|
|
139
137
|
|
|
140
138
|
torrent_info = matching_torrents[0]
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
console.print(f"[yellow]Name:[/yellow] {torrent_info.name or torrent_name}")
|
|
144
|
-
console.print(f"[yellow]Hash:[/yellow] {torrent_info.hash}")
|
|
145
|
-
print()
|
|
146
|
-
|
|
140
|
+
# Store relevant information
|
|
147
141
|
self.latest_torrent_hash = torrent_info.hash
|
|
148
142
|
self.output_file = torrent_info.content_path
|
|
149
143
|
self.file_name = torrent_info.name
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
self.
|
|
144
|
+
self.save_path = torrent_info.save_path
|
|
145
|
+
|
|
146
|
+
# Display torrent information
|
|
147
|
+
self._display_torrent_info(torrent_info)
|
|
148
|
+
|
|
149
|
+
# Check download viability after a short delay
|
|
150
|
+
time.sleep(3)
|
|
151
|
+
self._check_torrent_viability()
|
|
154
152
|
|
|
155
153
|
return torrent_info
|
|
156
|
-
|
|
157
|
-
def
|
|
158
|
-
"""
|
|
159
|
-
Starts downloading the added torrent and monitors its progress.
|
|
154
|
+
|
|
155
|
+
def _find_torrent(self, magnet_hash=None, timestamp=None):
|
|
160
156
|
"""
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
|
|
164
|
-
# Custom progress bar for mobile and PC
|
|
165
|
-
if get_use_large_bar():
|
|
166
|
-
bar_format = (
|
|
167
|
-
f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
|
168
|
-
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
|
169
|
-
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
|
170
|
-
)
|
|
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
|
-
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
|
|
194
|
-
|
|
195
|
-
progress = torrent_info.progress * 100
|
|
196
|
-
pbar.n = progress
|
|
197
|
-
|
|
198
|
-
download_speed = torrent_info.dlspeed
|
|
199
|
-
total_size = torrent_info.size
|
|
200
|
-
downloaded_size = torrent_info.downloaded
|
|
201
|
-
|
|
202
|
-
# Format the downloaded size
|
|
203
|
-
downloaded_size_str = internet_manager.format_file_size(downloaded_size)
|
|
204
|
-
downloaded_size = downloaded_size_str.split(' ')[0]
|
|
205
|
-
|
|
206
|
-
# Safely format the total size
|
|
207
|
-
total_size_str = internet_manager.format_file_size(total_size)
|
|
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 = ""
|
|
215
|
-
|
|
216
|
-
# Safely format the average download speed
|
|
217
|
-
average_internet_str = internet_manager.format_transfer_speed(download_speed)
|
|
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 = ""
|
|
225
|
-
|
|
226
|
-
if get_use_large_bar():
|
|
227
|
-
pbar.set_postfix_str(
|
|
228
|
-
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size} {Colors.WHITE}< {Colors.GREEN}{total_size} {Colors.RED}{total_size_unit} "
|
|
229
|
-
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
|
230
|
-
)
|
|
231
|
-
else:
|
|
232
|
-
pbar.set_postfix_str(
|
|
233
|
-
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size}{Colors.RED} {total_size} "
|
|
234
|
-
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
pbar.refresh()
|
|
238
|
-
time.sleep(0.2)
|
|
239
|
-
|
|
240
|
-
if int(progress) == 100:
|
|
241
|
-
break
|
|
242
|
-
|
|
243
|
-
except KeyboardInterrupt:
|
|
244
|
-
logging.info("Download process interrupted.")
|
|
245
|
-
|
|
246
|
-
def is_file_in_use(self, file_path: str) -> bool:
|
|
247
|
-
"""
|
|
248
|
-
Checks if a file is being used by any process.
|
|
157
|
+
Find a torrent by hash or added timestamp.
|
|
249
158
|
|
|
250
|
-
|
|
251
|
-
|
|
159
|
+
Args:
|
|
160
|
+
magnet_hash (str, optional): Hash of the torrent to find
|
|
161
|
+
timestamp (float, optional): Timestamp to compare against torrent added_on time
|
|
252
162
|
|
|
253
163
|
Returns:
|
|
254
|
-
|
|
164
|
+
list: List of matching torrent objects
|
|
255
165
|
"""
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
166
|
+
# Get list of all torrents with detailed information
|
|
167
|
+
torrents = self.qb.torrents_info()
|
|
168
|
+
|
|
169
|
+
if magnet_hash:
|
|
170
|
+
# First try to find by hash (most reliable)
|
|
171
|
+
hash_matches = [t for t in torrents if t.hash.lower() == magnet_hash]
|
|
172
|
+
if hash_matches:
|
|
173
|
+
return hash_matches
|
|
174
|
+
|
|
175
|
+
if timestamp:
|
|
176
|
+
# Fallback to finding by timestamp (least recently added torrent after timestamp)
|
|
177
|
+
time_matches = [t for t in torrents if getattr(t, 'added_on', 0) > timestamp]
|
|
178
|
+
if time_matches:
|
|
179
|
+
# Sort by added_on to get the most recently added
|
|
180
|
+
return sorted(time_matches, key=lambda t: getattr(t, 'added_on', 0), reverse=True)
|
|
181
|
+
|
|
182
|
+
# If we're just looking for the latest torrent
|
|
183
|
+
if not magnet_hash and not timestamp:
|
|
184
|
+
if torrents:
|
|
185
|
+
return [sorted(torrents, key=lambda t: getattr(t, 'added_on', 0), reverse=True)[0]]
|
|
186
|
+
|
|
187
|
+
return []
|
|
188
|
+
|
|
189
|
+
def _display_torrent_info(self, torrent_info):
|
|
190
|
+
"""
|
|
191
|
+
Display detailed information about a torrent.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
torrent_info: Torrent object from qBittorrent API
|
|
195
|
+
"""
|
|
196
|
+
self.console.print("\n[bold green]Torrent Details:[/bold green]")
|
|
197
|
+
self.console.print(f"[yellow]Name:[/yellow] {torrent_info.name}")
|
|
198
|
+
self.console.print(f"[yellow]Hash:[/yellow] {torrent_info.hash}")
|
|
199
|
+
#self.console.print(f"[yellow]Size:[/yellow] {internet_manager.format_file_size(torrent_info.size)}")
|
|
200
|
+
self.console.print(f"[yellow]Save Path:[/yellow] {torrent_info.save_path}")
|
|
201
|
+
|
|
202
|
+
# Show additional metadata if available
|
|
203
|
+
if hasattr(torrent_info, 'category') and torrent_info.category:
|
|
204
|
+
self.console.print(f"[yellow]Category:[/yellow] {torrent_info.category}")
|
|
205
|
+
|
|
206
|
+
if hasattr(torrent_info, 'tags') and torrent_info.tags:
|
|
207
|
+
self.console.print(f"[yellow]Tags:[/yellow] {torrent_info.tags}")
|
|
208
|
+
|
|
209
|
+
# Show connection info
|
|
210
|
+
self.console.print(f"[yellow]Seeds:[/yellow] {torrent_info.num_seeds} complete, {torrent_info.num_complete} connected")
|
|
211
|
+
self.console.print(f"[yellow]Peers:[/yellow] {torrent_info.num_leechs} incomplete, {torrent_info.num_incomplete} connected")
|
|
212
|
+
print()
|
|
213
|
+
|
|
214
|
+
def _check_torrent_viability(self):
|
|
266
215
|
"""
|
|
267
|
-
|
|
216
|
+
Check if the torrent is viable for downloading (has seeds/peers).
|
|
217
|
+
Removes the torrent if it doesn't appear to be downloadable.
|
|
218
|
+
"""
|
|
219
|
+
if not self.latest_torrent_hash:
|
|
220
|
+
return
|
|
268
221
|
|
|
269
|
-
|
|
270
|
-
|
|
222
|
+
try:
|
|
223
|
+
# Get updated torrent info
|
|
224
|
+
torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]
|
|
271
225
|
|
|
272
|
-
|
|
273
|
-
|
|
226
|
+
# Check if torrent has no activity and no source (seeders or peers)
|
|
227
|
+
if (torrent_info.dlspeed == 0 and
|
|
228
|
+
torrent_info.num_leechs == 0 and
|
|
229
|
+
torrent_info.num_seeds == 0 and
|
|
230
|
+
torrent_info.state in ('stalledDL', 'missingFiles', 'error')):
|
|
231
|
+
|
|
232
|
+
self.console.print(f"[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]")
|
|
233
|
+
self._remove_torrent(self.latest_torrent_hash)
|
|
234
|
+
self.latest_torrent_hash = None
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logging.error(f"Error checking torrent viability: {str(e)}")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def _remove_torrent(self, torrent_hash, delete_files=True):
|
|
274
244
|
"""
|
|
275
|
-
|
|
245
|
+
Remove a torrent from qBittorrent.
|
|
276
246
|
|
|
247
|
+
Args:
|
|
248
|
+
torrent_hash (str): Hash of the torrent to remove
|
|
249
|
+
delete_files (bool): Whether to delete associated files
|
|
250
|
+
"""
|
|
277
251
|
try:
|
|
278
|
-
|
|
279
|
-
|
|
252
|
+
self.qb.torrents_delete(delete_files=delete_files, torrent_hashes=torrent_hash)
|
|
253
|
+
self.console.print(f"[yellow]Torrent removed from client[/yellow]")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logging.error(f"Error removing torrent: {str(e)}")
|
|
256
|
+
|
|
257
|
+
def move_completed_torrent(self, destination):
|
|
258
|
+
"""
|
|
259
|
+
Move a completed torrent to a new destination using qBittorrent's API
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
destination (str): New destination path
|
|
280
263
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
264
|
+
Returns:
|
|
265
|
+
bool: True if successful, False otherwise
|
|
266
|
+
"""
|
|
267
|
+
if not self.latest_torrent_hash:
|
|
268
|
+
self.console.print("[yellow]No active torrent to move[/yellow]")
|
|
269
|
+
return False
|
|
284
270
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
271
|
+
try:
|
|
272
|
+
# Make sure destination exists
|
|
288
273
|
os.makedirs(destination, exist_ok=True)
|
|
274
|
+
|
|
275
|
+
# Get current state of the torrent
|
|
276
|
+
torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]
|
|
277
|
+
|
|
278
|
+
if torrent_info.progress < 1.0:
|
|
279
|
+
self.console.print("[yellow]Torrent not yet completed. Cannot move.[/yellow]")
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
self.console.print(f"[cyan]Moving torrent to: [green]{destination}")
|
|
283
|
+
|
|
284
|
+
# Use qBittorrent API to set location
|
|
285
|
+
self.qb.torrents_set_location(location=destination, torrent_hashes=self.latest_torrent_hash)
|
|
286
|
+
|
|
287
|
+
# Wait a bit for the move operation to complete
|
|
288
|
+
time.sleep(2)
|
|
289
|
+
|
|
290
|
+
# Verify move was successful
|
|
291
|
+
updated_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]
|
|
292
|
+
|
|
293
|
+
if Path(updated_info.save_path) == Path(destination):
|
|
294
|
+
self.console.print(f"[bold green]Successfully moved torrent to {destination}[/bold green]")
|
|
295
|
+
self.save_path = updated_info.save_path
|
|
296
|
+
self.output_file = updated_info.content_path
|
|
297
|
+
return True
|
|
298
|
+
else:
|
|
299
|
+
self.console.print(f"[bold red]Failed to move torrent. Current path: {updated_info.save_path}[/bold red]")
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logging.error(f"Error moving torrent: {str(e)}")
|
|
304
|
+
self.console.print(f"[bold red]Error moving torrent: {str(e)}[/bold red]")
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
def start_download(self):
|
|
308
|
+
"""
|
|
309
|
+
Start downloading the torrent and monitor its progress with a progress bar.
|
|
310
|
+
"""
|
|
311
|
+
if not self.latest_torrent_hash:
|
|
312
|
+
self.console.print("[yellow]No active torrent to download[/yellow]")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
# Ensure the torrent is started
|
|
317
|
+
self.qb.torrents_resume(torrent_hashes=self.latest_torrent_hash)
|
|
318
|
+
|
|
319
|
+
# Configure progress bar display format based on device
|
|
320
|
+
if get_use_large_bar():
|
|
321
|
+
bar_format = (
|
|
322
|
+
f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
|
323
|
+
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
|
324
|
+
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
bar_format = (
|
|
328
|
+
f"{Colors.YELLOW}Proc{Colors.WHITE}: "
|
|
329
|
+
f"{Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| "
|
|
330
|
+
f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
|
331
|
+
)
|
|
289
332
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
333
|
+
# Initialize progress bar
|
|
334
|
+
with tqdm(
|
|
335
|
+
total=100,
|
|
336
|
+
ascii='░▒█',
|
|
337
|
+
bar_format=bar_format,
|
|
338
|
+
unit_scale=True,
|
|
339
|
+
unit_divisor=1024,
|
|
340
|
+
mininterval=0.1
|
|
341
|
+
) as pbar:
|
|
342
|
+
|
|
343
|
+
was_downloading = True
|
|
344
|
+
stalled_count = 0
|
|
345
|
+
|
|
346
|
+
while True:
|
|
347
|
+
|
|
348
|
+
# Get updated torrent information
|
|
349
|
+
try:
|
|
350
|
+
torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]
|
|
351
|
+
except (IndexError, qbittorrentapi.exceptions.NotFound404Error):
|
|
352
|
+
self.console.print("[bold red]Torrent no longer exists in client[/bold red]")
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
# Store the latest path and name
|
|
356
|
+
self.save_path = torrent_info.save_path
|
|
357
|
+
self.torrent_name = torrent_info.name
|
|
358
|
+
self.output_file = torrent_info.content_path
|
|
359
|
+
|
|
360
|
+
# Update progress
|
|
361
|
+
progress = torrent_info.progress * 100
|
|
362
|
+
pbar.n = progress
|
|
363
|
+
|
|
364
|
+
# Get download statistics
|
|
365
|
+
download_speed = torrent_info.dlspeed
|
|
366
|
+
upload_speed = torrent_info.upspeed
|
|
367
|
+
total_size = torrent_info.size
|
|
368
|
+
downloaded_size = torrent_info.downloaded
|
|
369
|
+
eta = torrent_info.eta # eta in seconds
|
|
370
|
+
|
|
371
|
+
# Format sizes and speeds using the existing functions without modification
|
|
372
|
+
downloaded_size_str = internet_manager.format_file_size(downloaded_size)
|
|
373
|
+
total_size_str = internet_manager.format_file_size(total_size)
|
|
374
|
+
download_speed_str = internet_manager.format_transfer_speed(download_speed)
|
|
375
|
+
|
|
376
|
+
# Parse the formatted strings to extract numbers and units
|
|
377
|
+
# The format is "X.XX Unit" from the format_file_size and format_transfer_speed functions
|
|
378
|
+
dl_parts = downloaded_size_str.split(' ')
|
|
379
|
+
dl_size_num = dl_parts[0] if len(dl_parts) > 0 else "0"
|
|
380
|
+
dl_size_unit = dl_parts[1] if len(dl_parts) > 1 else "B"
|
|
381
|
+
|
|
382
|
+
total_parts = total_size_str.split(' ')
|
|
383
|
+
total_size_num = total_parts[0] if len(total_parts) > 0 else "0"
|
|
384
|
+
total_size_unit = total_parts[1] if len(total_parts) > 1 else "B"
|
|
385
|
+
|
|
386
|
+
speed_parts = download_speed_str.split(' ')
|
|
387
|
+
speed_num = speed_parts[0] if len(speed_parts) > 0 else "0"
|
|
388
|
+
speed_unit = ' '.join(speed_parts[1:]) if len(speed_parts) > 1 else "B/s"
|
|
389
|
+
|
|
390
|
+
# Check if download is active
|
|
391
|
+
currently_downloading = download_speed > 0
|
|
392
|
+
|
|
393
|
+
# Handle stalled downloads
|
|
394
|
+
if was_downloading and not currently_downloading and progress < 100:
|
|
395
|
+
stalled_count += 1
|
|
396
|
+
if stalled_count >= 15: # 3 seconds (15 * 0.2)
|
|
397
|
+
pbar.set_description(f"{Colors.RED}Stalled")
|
|
398
|
+
else:
|
|
399
|
+
stalled_count = 0
|
|
400
|
+
pbar.set_description(f"{Colors.GREEN}Active")
|
|
401
|
+
|
|
402
|
+
was_downloading = currently_downloading
|
|
403
|
+
|
|
404
|
+
# Update progress bar display with formatted statistics
|
|
405
|
+
pbar.set_postfix_str(
|
|
406
|
+
f"{Colors.GREEN}{dl_size_num} {Colors.RED}{dl_size_unit} {Colors.WHITE}< "
|
|
407
|
+
f"{Colors.GREEN}{total_size_num} {Colors.RED}{total_size_unit}{Colors.WHITE}, "
|
|
408
|
+
f"{Colors.CYAN}{speed_num} {Colors.RED}{speed_unit}"
|
|
409
|
+
)
|
|
410
|
+
pbar.refresh()
|
|
411
|
+
|
|
412
|
+
# Check for completion
|
|
413
|
+
if int(progress) == 100:
|
|
414
|
+
pbar.n = 100
|
|
415
|
+
pbar.refresh()
|
|
416
|
+
break
|
|
417
|
+
|
|
418
|
+
# Check torrent state for errors
|
|
419
|
+
if torrent_info.state in ('error', 'missingFiles', 'unknown'):
|
|
420
|
+
self.console.print(f"[bold red]Error in torrent: {torrent_info.state}[/bold red]")
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
time.sleep(0.3)
|
|
424
|
+
|
|
425
|
+
self.console.print(f"[bold green]Download complete: {self.torrent_name}[/bold green]")
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
except KeyboardInterrupt:
|
|
429
|
+
self.console.print("[yellow]Download process interrupted[/yellow]")
|
|
430
|
+
return False
|
|
431
|
+
|
|
304
432
|
except Exception as e:
|
|
305
|
-
|
|
306
|
-
|
|
433
|
+
logging.error(f"Error monitoring download: {str(e)}")
|
|
434
|
+
self.console.print(f"[bold red]Error monitoring download: {str(e)}[/bold red]")
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
def is_file_in_use(self, file_path):
|
|
438
|
+
"""
|
|
439
|
+
Check if a file is currently being used by any process.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
file_path (str): Path to the file to check
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
bool: True if file is in use, False otherwise
|
|
446
|
+
"""
|
|
447
|
+
# Convert to absolute path for consistency
|
|
448
|
+
file_path = str(Path(file_path).resolve())
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
for proc in psutil.process_iter(['open_files', 'name']):
|
|
452
|
+
try:
|
|
453
|
+
proc_info = proc.info
|
|
454
|
+
if 'open_files' in proc_info and proc_info['open_files']:
|
|
455
|
+
for file_info in proc_info['open_files']:
|
|
456
|
+
if file_path == file_info.path:
|
|
457
|
+
return True
|
|
458
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
459
|
+
continue
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logging.error(f"Error checking if file is in use: {str(e)}")
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
def cleanup(self):
|
|
467
|
+
"""
|
|
468
|
+
Clean up resources and perform final operations before shutting down.
|
|
469
|
+
"""
|
|
470
|
+
if self.latest_torrent_hash:
|
|
471
|
+
self._remove_torrent(self.latest_torrent_hash)
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
self.qb.auth_log_out()
|
|
475
|
+
except:
|
|
476
|
+
pass
|