StreamingCommunity 2.9.4__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.

Files changed (48) hide show
  1. StreamingCommunity/Api/Player/sweetpixel.py +49 -0
  2. StreamingCommunity/Api/Site/1337xx/__init__.py +26 -12
  3. StreamingCommunity/Api/Site/1337xx/site.py +5 -4
  4. StreamingCommunity/Api/Site/1337xx/title.py +4 -6
  5. StreamingCommunity/Api/Site/altadefinizione/__init__.py +64 -17
  6. StreamingCommunity/Api/Site/altadefinizione/film.py +32 -2
  7. StreamingCommunity/Api/Site/altadefinizione/series.py +54 -10
  8. StreamingCommunity/Api/Site/altadefinizione/site.py +25 -7
  9. StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -2
  10. StreamingCommunity/Api/Site/animeunity/__init__.py +53 -32
  11. StreamingCommunity/Api/Site/animeunity/film_serie.py +8 -5
  12. StreamingCommunity/Api/Site/animeunity/site.py +4 -6
  13. StreamingCommunity/Api/Site/animeworld/__init__.py +71 -0
  14. StreamingCommunity/Api/Site/animeworld/serie.py +107 -0
  15. StreamingCommunity/Api/Site/animeworld/site.py +111 -0
  16. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +79 -0
  17. StreamingCommunity/Api/Site/cb01new/__init__.py +26 -14
  18. StreamingCommunity/Api/Site/cb01new/film.py +1 -1
  19. StreamingCommunity/Api/Site/cb01new/site.py +9 -7
  20. StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +26 -15
  21. StreamingCommunity/Api/Site/ddlstreamitaly/series.py +2 -2
  22. StreamingCommunity/Api/Site/ddlstreamitaly/site.py +3 -3
  23. StreamingCommunity/Api/Site/guardaserie/__init__.py +23 -11
  24. StreamingCommunity/Api/Site/guardaserie/series.py +1 -1
  25. StreamingCommunity/Api/Site/guardaserie/site.py +5 -4
  26. StreamingCommunity/Api/Site/mostraguarda/__init__.py +27 -7
  27. StreamingCommunity/Api/Site/mostraguarda/film.py +1 -1
  28. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +50 -27
  29. StreamingCommunity/Api/Site/streamingcommunity/film.py +1 -1
  30. StreamingCommunity/Api/Site/streamingcommunity/series.py +6 -3
  31. StreamingCommunity/Api/Site/streamingcommunity/site.py +7 -3
  32. StreamingCommunity/Lib/Downloader/HLS/segments.py +2 -4
  33. StreamingCommunity/Lib/Downloader/MP4/downloader.py +7 -6
  34. StreamingCommunity/Lib/Downloader/TOR/downloader.py +397 -227
  35. StreamingCommunity/Lib/FFmpeg/util.py +12 -0
  36. StreamingCommunity/Lib/M3U8/estimator.py +5 -8
  37. StreamingCommunity/Upload/version.py +1 -1
  38. StreamingCommunity/Util/config_json.py +2 -8
  39. StreamingCommunity/Util/table.py +12 -2
  40. StreamingCommunity/global_search.py +315 -0
  41. StreamingCommunity/run.py +39 -5
  42. {streamingcommunity-2.9.4.dist-info → streamingcommunity-2.9.6.dist-info}/METADATA +42 -15
  43. streamingcommunity-2.9.6.dist-info/RECORD +85 -0
  44. {streamingcommunity-2.9.4.dist-info → streamingcommunity-2.9.6.dist-info}/WHEEL +1 -1
  45. streamingcommunity-2.9.4.dist-info/RECORD +0 -79
  46. {streamingcommunity-2.9.4.dist-info → streamingcommunity-2.9.6.dist-info}/entry_points.txt +0 -0
  47. {streamingcommunity-2.9.4.dist-info → streamingcommunity-2.9.6.dist-info/licenses}/LICENSE +0 -0
  48. {streamingcommunity-2.9.4.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 library
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
- # External libraries
23
- from tqdm import tqdm
24
- import qbittorrentapi
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 TorrentManager instance.
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
- Parameters:
45
- - host (str): IP address or hostname of the qBittorrent Web UI.
46
- - port (int): Port of the qBittorrent Web UI.
47
- - username (str): Username for accessing qBittorrent.
48
- - password (str): Password for accessing qBittorrent.
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
- console.print(f"[cyan]Connect to: [green]{HOST}:{PORT}")
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
- except:
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
- self.logged_in = True
78
- logging.info("Successfully logged in to qBittorrent.")
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"Failed to log in: {str(e)}")
82
- self.logged_in = False
83
-
84
- def delete_magnet(self, torrent_info):
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
- Deletes a torrent if it is not downloadable (no seeds/peers).
77
+ Adds a magnet link to qBittorrent and retrieves torrent information.
87
78
 
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):
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
- dict: Information about the added torrent, or None in case of error.
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("Magnet link hash not found")
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 the torrent name, if available
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 "Name not available"
98
+ torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Unknown"
122
99
 
123
- # Save the timestamp before adding the torrent
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
- time.sleep(1)
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
- torrents = self.qb.torrents_info()
132
- matching_torrents = [
133
- t for t in torrents
134
- if (t.hash.lower() == magnet_hash) or (getattr(t, 'added_on', 0) > before_add_time)
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("No matching torrent found")
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
- 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
- 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
- # Wait and verify if the download is possible
152
- time.sleep(5)
153
- self.delete_magnet(self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0])
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 start_download(self):
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
- if self.latest_torrent_hash is not None:
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
- Parameters:
251
- - file_path (str): The file path to check.
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
- - bool: True if the file is in use, False otherwise.
164
+ list: List of matching torrent objects
255
165
  """
256
- for proc in psutil.process_iter(['open_files']):
257
- try:
258
- if any(file_path == f.path for f in proc.info['open_files'] or []):
259
- return True
260
- except (psutil.NoSuchProcess, psutil.AccessDenied):
261
- continue
262
-
263
- return False
264
-
265
- def move_downloaded_files(self, destination: str):
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
- Moves the downloaded files of the most recent torrent to a new location.
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
- Parameters:
270
- - destination (str): Destination folder.
222
+ try:
223
+ # Get updated torrent info
224
+ torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0]
271
225
 
272
- Returns:
273
- - bool: True if the move was successful, False otherwise.
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
- console.print(f"[cyan]Destination folder: [red]{destination}")
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
- timeout = 5
279
- elapsed = 0
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
- while self.is_file_in_use(self.output_file) and elapsed < timeout:
282
- time.sleep(1)
283
- elapsed += 1
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
- if elapsed == timeout:
286
- raise Exception(f"File '{self.output_file}' is in use and could not be moved.")
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
- try:
291
- shutil.move(self.output_file, destination)
292
- except OSError as e:
293
- if e.errno == 17: # Error when moving between different disks
294
- shutil.copy2(self.output_file, destination)
295
- os.remove(self.output_file)
296
- else:
297
- raise
298
-
299
- time.sleep(5)
300
- last_torrent = self.qb.torrents_info()[-1]
301
- self.qb.torrents_delete(delete_files=True, torrent_hashes=last_torrent.hash)
302
- return True
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
- print(f"Error moving file: {e}")
306
- return False
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