StreamingCommunity 3.3.9__py3-none-any.whl → 3.4.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.

Files changed (60) hide show
  1. StreamingCommunity/Api/Player/hdplayer.py +0 -5
  2. StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
  3. StreamingCommunity/Api/Player/supervideo.py +3 -8
  4. StreamingCommunity/Api/Player/sweetpixel.py +1 -9
  5. StreamingCommunity/Api/Player/vixcloud.py +5 -16
  6. StreamingCommunity/Api/Site/altadefinizione/film.py +4 -15
  7. StreamingCommunity/Api/Site/altadefinizione/site.py +2 -7
  8. StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
  9. StreamingCommunity/Api/Site/animeunity/site.py +9 -24
  10. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
  11. StreamingCommunity/Api/Site/animeworld/film.py +4 -2
  12. StreamingCommunity/Api/Site/animeworld/site.py +3 -11
  13. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
  14. StreamingCommunity/Api/Site/crunchyroll/film.py +4 -5
  15. StreamingCommunity/Api/Site/crunchyroll/series.py +2 -3
  16. StreamingCommunity/Api/Site/crunchyroll/site.py +2 -9
  17. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
  18. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
  19. StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
  20. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
  21. StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
  22. StreamingCommunity/Api/Site/mediasetinfinity/series.py +1 -2
  23. StreamingCommunity/Api/Site/mediasetinfinity/site.py +3 -11
  24. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +7 -25
  27. StreamingCommunity/Api/Site/raiplay/film.py +6 -7
  28. StreamingCommunity/Api/Site/raiplay/series.py +0 -2
  29. StreamingCommunity/Api/Site/raiplay/site.py +3 -11
  30. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +4 -11
  31. StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
  32. StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
  33. StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
  34. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -26
  35. StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
  36. StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
  37. StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
  38. StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
  39. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
  40. StreamingCommunity/Api/Template/config_loader.py +0 -7
  41. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +54 -1
  42. StreamingCommunity/Lib/Downloader/DASH/downloader.py +131 -54
  43. StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
  44. StreamingCommunity/Lib/Downloader/DASH/segments.py +66 -54
  45. StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
  46. StreamingCommunity/Lib/Downloader/HLS/segments.py +23 -28
  47. StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
  48. StreamingCommunity/Lib/FFmpeg/command.py +32 -90
  49. StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
  50. StreamingCommunity/TelegramHelp/config.json +0 -1
  51. StreamingCommunity/Upload/version.py +1 -1
  52. StreamingCommunity/Util/config_json.py +28 -21
  53. StreamingCommunity/Util/http_client.py +28 -0
  54. StreamingCommunity/Util/os.py +16 -6
  55. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
  56. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/RECORD +60 -60
  57. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
  58. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
  59. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
  60. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/top_level.txt +0 -0
@@ -3,19 +3,12 @@
3
3
  import logging
4
4
 
5
5
 
6
- # External libraries
7
- import httpx
8
-
9
-
10
6
  # Internal utilities
11
7
  from StreamingCommunity.Util.headers import get_headers
12
- from StreamingCommunity.Util.config_json import config_manager
8
+ from StreamingCommunity.Util.http_client import create_client
13
9
  from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager
14
10
 
15
11
 
16
- # Variable
17
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
18
-
19
12
 
20
13
  class GetSerieInfo:
21
14
  def __init__(self, path_id: str):
@@ -32,7 +25,7 @@ class GetSerieInfo:
32
25
  """Get series info including seasons."""
33
26
  try:
34
27
  program_url = f"{self.base_url}/{self.path_id}"
35
- response = httpx.get(url=program_url, headers=get_headers(), timeout=max_timeout)
28
+ response = create_client(headers=get_headers()).get(program_url)
36
29
 
37
30
  # If 404, content is not yet available
38
31
  if response.status_code == 404:
@@ -88,8 +81,8 @@ class GetSerieInfo:
88
81
  season = self.seasons_manager.get_season_by_number(number_season)
89
82
 
90
83
  # Se stai leggendo questo codice spieami perche hai fatto cosi.
91
- url = f"{self.base_url}/{self.path_id.replace(".json", "")}/{self.publishing_block_id}/{season.id}/episodes.json"
92
- response = httpx.get(url=url, headers=get_headers(), timeout=max_timeout)
84
+ url = f"{self.base_url}/{self.path_id.replace('.json', '')}/{self.publishing_block_id}/{season.id}/episodes.json"
85
+ response = create_client(headers=get_headers()).get(url)
93
86
  response.raise_for_status()
94
87
 
95
88
  episodes_data = response.json()
@@ -1,20 +1,11 @@
1
1
  # 16.03.25
2
2
 
3
3
 
4
- # External library
5
- import httpx
6
-
7
-
8
4
  # Internal utilities
9
- from StreamingCommunity.Util.config_json import config_manager
5
+ from StreamingCommunity.Util.http_client import create_client
10
6
  from StreamingCommunity.Util.headers import get_headers
11
7
 
12
8
 
13
- # Variable
14
- MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
15
-
16
-
17
-
18
9
  def generate_license_url(mpd_id: str):
19
10
  """
20
11
  Generates the URL to obtain the Widevine license.
@@ -29,8 +20,8 @@ def generate_license_url(mpd_id: str):
29
20
  'cont': mpd_id,
30
21
  'output': '62',
31
22
  }
32
-
33
- response = httpx.get('https://mediapolisvod.rai.it/relinker/relinkerServlet.htm', params=params, headers=get_headers(), timeout=MAX_TIMEOUT)
23
+
24
+ response = create_client(headers=get_headers()).get('https://mediapolisvod.rai.it/relinker/relinkerServlet.htm', params=params)
34
25
  response.raise_for_status()
35
26
 
36
27
  # Extract the license URL from the response in two lines
@@ -9,8 +9,9 @@ from rich.console import Console
9
9
 
10
10
  # Internal utilities
11
11
  from StreamingCommunity.Util.os import os_manager
12
+ from StreamingCommunity.Util.config_json import config_manager
12
13
  from StreamingCommunity.Util.message import start_message
13
- from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession, get_bot_instance
14
+ from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession
14
15
 
15
16
 
16
17
  # Logic class
@@ -25,6 +26,7 @@ from StreamingCommunity.Api.Player.vixcloud import VideoSource
25
26
 
26
27
  # Variable
27
28
  console = Console()
29
+ extension_output = config_manager.get("M3U8_CONVERSION", "extension")
28
30
 
29
31
 
30
32
  def download_film(select_title: MediaItem) -> str:
@@ -38,19 +40,6 @@ def download_film(select_title: MediaItem) -> str:
38
40
  Return:
39
41
  - str: output path
40
42
  """
41
- if site_constant.TELEGRAM_BOT:
42
- bot = get_bot_instance()
43
- bot.send_message(f"Download in corso:\n{select_title.name}", None)
44
-
45
- # Viene usato per lo screen
46
- console.print(f"## Download: [red]{select_title.name} ##")
47
-
48
- # Get script_id
49
- script_id = TelegramSession.get_session()
50
- if script_id != "unknown":
51
- TelegramSession.updateScriptId(script_id, select_title.name)
52
-
53
- # Start message and display film information
54
43
  start_message()
55
44
  console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n")
56
45
 
@@ -67,8 +56,8 @@ def download_film(select_title: MediaItem) -> str:
67
56
  return None
68
57
 
69
58
  # Define the filename and path for the downloaded film
70
- title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
71
- mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", ""))
59
+ title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output
60
+ mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, ""))
72
61
 
73
62
  # Download the film using the m3u8 playlist, and output filename
74
63
  hls_process = HLS_Downloader(
@@ -4,14 +4,13 @@ import json
4
4
 
5
5
 
6
6
  # External libraries
7
- import httpx
8
7
  from bs4 import BeautifulSoup
9
8
  from rich.console import Console
10
9
 
11
10
 
12
11
  # Internal utilities
13
- from StreamingCommunity.Util.config_json import config_manager
14
12
  from StreamingCommunity.Util.headers import get_userAgent
13
+ from StreamingCommunity.Util.http_client import create_client
15
14
  from StreamingCommunity.Util.table import TVShowManager
16
15
  from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance
17
16
 
@@ -25,8 +24,6 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
25
24
  console = Console()
26
25
  media_search_manager = MediaManager()
27
26
  table_show_manager = TVShowManager()
28
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
29
- ssl_verify = config_manager.get_bool("REQUESTS", "verify")
30
27
 
31
28
 
32
29
  def title_search(query: str) -> int:
@@ -46,13 +43,7 @@ def title_search(query: str) -> int:
46
43
  table_show_manager.clear()
47
44
 
48
45
  try:
49
- response = httpx.get(
50
- f"{site_constant.FULL_URL}/it",
51
- headers={'user-agent': get_userAgent()},
52
- timeout=max_timeout,
53
- verify=ssl_verify,
54
- follow_redirects=True
55
- )
46
+ response = create_client(headers={'user-agent': get_userAgent()}).get(f"{site_constant.FULL_URL}/it")
56
47
  response.raise_for_status()
57
48
 
58
49
  soup = BeautifulSoup(response.text, 'html.parser')
@@ -66,17 +57,7 @@ def title_search(query: str) -> int:
66
57
  console.print(f"[cyan]Search url: [yellow]{search_url}")
67
58
 
68
59
  try:
69
- response = httpx.get(
70
- search_url,
71
- headers = {
72
- 'referer': site_constant.FULL_URL,
73
- 'user-agent': get_userAgent(),
74
- 'x-inertia': 'true',
75
- 'x-inertia-version': version
76
- },
77
- timeout=max_timeout,
78
- verify=ssl_verify
79
- )
60
+ response = create_client(headers={'user-agent': get_userAgent(), 'x-inertia': 'true', 'x-inertia-version': version}).get(search_url)
80
61
  response.raise_for_status()
81
62
 
82
63
  except Exception as e:
@@ -5,21 +5,15 @@ import logging
5
5
 
6
6
 
7
7
  # External libraries
8
- import httpx
9
8
  from bs4 import BeautifulSoup
10
9
 
11
10
 
12
11
  # Internal utilities
13
- from StreamingCommunity.Util.headers import get_userAgent
14
- from StreamingCommunity.Util.config_json import config_manager
12
+ from StreamingCommunity.Util.headers import get_headers
13
+ from StreamingCommunity.Util.http_client import create_client
15
14
  from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager
16
15
 
17
16
 
18
- # Variable
19
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
20
- ssl_verify = config_manager.get_bool("REQUESTS", "verify")
21
-
22
-
23
17
  class GetSerieInfo:
24
18
  def __init__(self, url, media_id: int = None, series_name: str = None):
25
19
  """
@@ -31,7 +25,7 @@ class GetSerieInfo:
31
25
  - series_name (str, optional): Name of the TV series
32
26
  """
33
27
  self.is_series = False
34
- self.headers = {'user-agent': get_userAgent()}
28
+ self.headers = get_headers()
35
29
  self.url = url
36
30
  self.media_id = media_id
37
31
  self.seasons_manager = SeasonManager()
@@ -48,12 +42,7 @@ class GetSerieInfo:
48
42
  Exception: If there's an error fetching series information
49
43
  """
50
44
  try:
51
- response = httpx.get(
52
- url=f"{self.url}/titles/{self.media_id}-{self.series_name}",
53
- headers=self.headers,
54
- timeout=max_timeout,
55
- verify=ssl_verify
56
- )
45
+ response = create_client(headers=self.headers).get(f"{self.url}/titles/{self.media_id}-{self.series_name}")
57
46
  response.raise_for_status()
58
47
 
59
48
  # Extract series info from JSON response
@@ -98,17 +87,13 @@ class GetSerieInfo:
98
87
  if not season:
99
88
  logging.error(f"Season {number_season} not found")
100
89
  return
101
-
102
- response = httpx.get(
103
- url=f'{self.url}/titles/{self.media_id}-{self.series_name}/season-{number_season}',
104
- headers={
105
- 'User-Agent': self.headers['user-agent'],
106
- 'x-inertia': 'true',
107
- 'x-inertia-version': self.version,
108
- },
109
- timeout=max_timeout,
110
- verify=ssl_verify
111
- )
90
+
91
+ custom_headers = self.headers.copy()
92
+ custom_headers.update({
93
+ 'x-inertia': 'true',
94
+ 'x-inertia-version': self.version,
95
+ })
96
+ response = create_client(headers=custom_headers).get(f"{self.url}/titles/{self.media_id}-{self.series_name}/season-{number_season}")
112
97
 
113
98
  # Extract episodes from JSON response
114
99
  json_response = response.json().get('props', {}).get('loadedSeason', {}).get('episodes', [])
@@ -3,6 +3,7 @@
3
3
  import sys
4
4
  import subprocess
5
5
 
6
+
6
7
  # External library
7
8
  from rich.console import Console
8
9
  from rich.prompt import Prompt
@@ -9,6 +9,7 @@ from rich.console import Console
9
9
 
10
10
  # Internal utilities
11
11
  from StreamingCommunity.Util.os import os_manager
12
+ from StreamingCommunity.Util.config_json import config_manager
12
13
  from StreamingCommunity.Util.message import start_message
13
14
 
14
15
 
@@ -24,6 +25,7 @@ from StreamingCommunity.Api.Player.hdplayer import VideoSource
24
25
 
25
26
  # Variable
26
27
  console = Console()
28
+ extension_output = config_manager.get("M3U8_CONVERSION", "extension")
27
29
 
28
30
 
29
31
  def download_film(select_title: MediaItem) -> str:
@@ -45,8 +47,8 @@ def download_film(select_title: MediaItem) -> str:
45
47
  master_playlist = video_source.get_m3u8_url(select_title.url)
46
48
 
47
49
  # Define the filename and path for the downloaded film
48
- title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
49
- mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", ""))
50
+ title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output
51
+ mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, ""))
50
52
 
51
53
  # Download the film using the m3u8 playlist, and output filename
52
54
  hls_process = HLS_Downloader(
@@ -53,7 +53,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
53
53
 
54
54
  # Get episode information
55
55
  obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1)
56
- console.print(f"\n[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n")
56
+ console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n")
57
57
 
58
58
  # Define filename and path for the downloaded video
59
59
  mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.mp4"
@@ -4,13 +4,12 @@ import re
4
4
 
5
5
 
6
6
  # External libraries
7
- import httpx
8
7
  from bs4 import BeautifulSoup
9
8
  from rich.console import Console
10
9
 
11
10
 
12
11
  # Internal utilities
13
- from StreamingCommunity.Util.config_json import config_manager
12
+ from StreamingCommunity.Util.http_client import create_client
14
13
  from StreamingCommunity.Util.headers import get_userAgent
15
14
  from StreamingCommunity.Util.table import TVShowManager
16
15
 
@@ -24,18 +23,13 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
24
23
  console = Console()
25
24
  media_search_manager = MediaManager()
26
25
  table_show_manager = TVShowManager()
27
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
28
26
 
29
27
 
30
28
  def extract_nonce() -> str:
31
29
  """Extract nonce value from the page script"""
32
- response = httpx.get(
33
- site_constant.FULL_URL,
34
- headers={'user-agent': get_userAgent()},
35
- timeout=max_timeout
36
- )
37
-
30
+ response = create_client(headers={'user-agent': get_userAgent()}).get(site_constant.FULL_URL)
38
31
  soup = BeautifulSoup(response.content, 'html.parser')
32
+
39
33
  script = soup.find('script', id='live-search-js-extra')
40
34
  if script:
41
35
  match = re.search(r'"admin_ajax_nonce":"([^"]+)"', script.text)
@@ -73,15 +67,7 @@ def title_search(query: str) -> int:
73
67
  '_wpnonce': _wpnonce
74
68
  }
75
69
 
76
- response = httpx.post(
77
- search_url,
78
- headers={
79
- 'origin': site_constant.FULL_URL,
80
- 'user-agent': get_userAgent()
81
- },
82
- data=data,
83
- timeout=max_timeout
84
- )
70
+ response = create_client(headers={'origin': site_constant.FULL_URL, 'user-agent': get_userAgent()}).post(search_url, data=data)
85
71
  response.raise_for_status()
86
72
  soup = BeautifulSoup(response.text, 'html.parser')
87
73
 
@@ -11,12 +11,9 @@ from bs4 import BeautifulSoup
11
11
  # Internal utilities
12
12
  from StreamingCommunity.Util.headers import get_userAgent
13
13
  from StreamingCommunity.Util.http_client import create_client
14
- from StreamingCommunity.Util.config_json import config_manager
15
14
  from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager, Episode
16
15
 
17
16
 
18
- # Variable
19
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
20
17
 
21
18
 
22
19
  class GetSerieInfo:
@@ -56,13 +56,6 @@ class SiteConstant:
56
56
  base_path = os.path.join(base_path, self.SITE_NAME)
57
57
  return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'anime_folder_name'))
58
58
 
59
- @property
60
- def COOKIE(self):
61
- try:
62
- return config_manager.get_dict('SITE_EXTRA', self.SITE_NAME)
63
- except KeyError:
64
- return None
65
-
66
59
  @property
67
60
  def TELEGRAM_BOT(self):
68
61
  return config_manager.get_bool('DEFAULT', 'telegram_bot')
@@ -3,25 +3,30 @@
3
3
  import os
4
4
  import subprocess
5
5
  import logging
6
+ import threading
7
+ import time
6
8
 
7
9
 
8
10
  # External libraries
9
11
  from rich.console import Console
12
+ from tqdm import tqdm
10
13
 
11
14
 
12
15
  # Internal utilities
13
16
  from StreamingCommunity.Util.os import get_mp4decrypt_path
17
+ from StreamingCommunity.Util.color import Colors
14
18
 
15
19
  # Variable
16
20
  console = Console()
17
21
 
18
22
 
19
23
  # NOTE!: SAREBBE MEGLIO FARLO PER OGNI FILE DURANTE IL DOWNLOAD ... MA PER ORA LO LASCIO COSI
20
- def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=True):
24
+ def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cleanup=True):
21
25
  """
22
26
  Decrypt an mp4/m4s file using mp4decrypt.
23
27
 
24
28
  Args:
29
+ type (str): Type of file ('video' or 'audio').
25
30
  encrypted_path (str): Path to encrypted file.
26
31
  kid (str): Hexadecimal KID.
27
32
  key (str): Hexadecimal key.
@@ -48,15 +53,63 @@ def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=
48
53
  if not output_path:
49
54
  output_path = os.path.splitext(encrypted_path)[0] + "_decrypted.mp4"
50
55
 
56
+ # Get file size for progress tracking
57
+ file_size = os.path.getsize(encrypted_path)
58
+
51
59
  key_format = f"{kid.lower()}:{key.lower()}"
52
60
  cmd = [get_mp4decrypt_path(), "--key", key_format, encrypted_path, output_path]
53
61
  logging.info(f"Running command: {' '.join(cmd)}")
54
62
 
63
+ # Create progress bar with custom format
64
+ bar_format = (
65
+ f"{Colors.YELLOW}DECRYPT{Colors.CYAN} {type}{Colors.WHITE}: "
66
+ f"{Colors.MAGENTA}{{bar:40}} "
67
+ f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} "
68
+ f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
69
+ f"{Colors.WHITE}{{postfix}}"
70
+ )
71
+
72
+ progress_bar = tqdm(
73
+ total=100,
74
+ bar_format=bar_format,
75
+ unit="",
76
+ ncols=150
77
+ )
78
+
79
+ def monitor_output_file():
80
+ """Monitor output file growth and update progress bar."""
81
+ last_size = 0
82
+ while True:
83
+ if os.path.exists(output_path):
84
+ current_size = os.path.getsize(output_path)
85
+ if current_size > 0:
86
+ progress_percent = min(int((current_size / file_size) * 100), 100)
87
+ progress_bar.n = progress_percent
88
+ progress_bar.refresh()
89
+
90
+ if current_size == last_size and current_size > 0:
91
+ # File stopped growing, likely finished
92
+ break
93
+
94
+ last_size = current_size
95
+
96
+ time.sleep(0.1)
97
+
98
+ # Start monitoring thread
99
+ monitor_thread = threading.Thread(target=monitor_output_file, daemon=True)
100
+ monitor_thread.start()
101
+
55
102
  try:
56
103
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
57
104
  except Exception as e:
105
+ progress_bar.close()
58
106
  console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]")
59
107
  return None
108
+
109
+ # Ensure progress bar reaches 100%
110
+ progress_bar.n = 100
111
+ progress_bar.refresh()
112
+ progress_bar.close()
60
113
 
61
114
  if result.returncode == 0 and os.path.exists(output_path):
62
115