StreamingCommunity 3.2.7__tar.gz → 3.2.8__tar.gz
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-3.2.7/StreamingCommunity.egg-info → streamingcommunity-3.2.8}/PKG-INFO +1 -1
- streamingcommunity-3.2.8/StreamingCommunity/Lib/Downloader/HLS/segments.py +464 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Upload/version.py +1 -1
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8/StreamingCommunity.egg-info}/PKG-INFO +1 -1
- streamingcommunity-3.2.7/StreamingCommunity/Lib/Downloader/HLS/segments.py +0 -350
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/LICENSE +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/MANIFEST.in +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/README.md +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/ddl.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/hdplayer.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/maxstream.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/mediapolisvod.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/mixdrop.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/supervideo.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/sweetpixel.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Player/vixcloud.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/altadefinizione/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/altadefinizione/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/altadefinizione/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/altadefinizione/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeunity/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeunity/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeunity/serie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeunity/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeworld/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeworld/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeworld/serie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeworld/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/cb01new/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/cb01new/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/cb01new/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/crunchyroll/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/crunchyroll/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/crunchyroll/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/crunchyroll/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/guardaserie/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/guardaserie/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/guardaserie/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/raiplay/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/raiplay/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/raiplay/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/raiplay/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingcommunity/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingcommunity/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingcommunity/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingcommunity/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingwatch/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingwatch/film.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingwatch/series.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingwatch/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Template/Class/SearchType.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Template/Util/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Template/Util/manage_ep.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Template/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Template/config_loader.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Api/Template/site.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/DASH/decrypt.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/DASH/downloader.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/DASH/parser.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/DASH/segments.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/HLS/downloader.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/MP4/downloader.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/TOR/downloader.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/Downloader/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/FFmpeg/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/FFmpeg/capture.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/FFmpeg/command.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/FFmpeg/util.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/M3U8/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/M3U8/decryptor.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/M3U8/estimator.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/M3U8/parser.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/M3U8/url_fixer.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/TMBD/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/TMBD/obj_tmbd.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Lib/TMBD/tmdb.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/TelegramHelp/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/TelegramHelp/config.json +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/TelegramHelp/telegram_bot.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Upload/update.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/bento4_installer.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/color.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/config_json.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/ffmpeg_installer.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/headers.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/logger.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/message.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/os.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/Util/table.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/__init__.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/global_search.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity/run.py +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity.egg-info/SOURCES.txt +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity.egg-info/dependency_links.txt +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity.egg-info/entry_points.txt +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity.egg-info/requires.txt +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/StreamingCommunity.egg-info/top_level.txt +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/requirements.txt +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/setup.cfg +0 -0
- {streamingcommunity-3.2.7 → streamingcommunity-3.2.8}/setup.py +0 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# 18.04.24
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import queue
|
|
7
|
+
import signal
|
|
8
|
+
import logging
|
|
9
|
+
import binascii
|
|
10
|
+
import threading
|
|
11
|
+
from queue import PriorityQueue
|
|
12
|
+
from urllib.parse import urljoin, urlparse
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
|
+
from typing import Dict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# External libraries
|
|
18
|
+
import httpx
|
|
19
|
+
from tqdm import tqdm
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Internal utilities
|
|
24
|
+
from StreamingCommunity.Util.color import Colors
|
|
25
|
+
from StreamingCommunity.Util.headers import get_userAgent
|
|
26
|
+
from StreamingCommunity.Util.config_json import config_manager
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Logic class
|
|
30
|
+
from ...M3U8 import (
|
|
31
|
+
M3U8_Decryption,
|
|
32
|
+
M3U8_Ts_Estimator,
|
|
33
|
+
M3U8_Parser,
|
|
34
|
+
M3U8_UrlFix
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Config
|
|
38
|
+
TQDM_DELAY_WORKER = 0.01
|
|
39
|
+
REQUEST_MAX_RETRY = config_manager.get_int('REQUESTS', 'max_retry')
|
|
40
|
+
REQUEST_VERIFY = config_manager.get_bool('REQUESTS', 'verify')
|
|
41
|
+
DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_workers')
|
|
42
|
+
DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workers')
|
|
43
|
+
MAX_TIMEOOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
44
|
+
SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
|
|
45
|
+
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
46
|
+
MAX_INTERRUPT_COUNT = 3
|
|
47
|
+
|
|
48
|
+
# Variable
|
|
49
|
+
console = Console()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class M3U8_Segments:
|
|
53
|
+
def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True):
|
|
54
|
+
"""
|
|
55
|
+
Initializes the M3U8_Segments object.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
- url (str): The URL of the M3U8 playlist.
|
|
59
|
+
- tmp_folder (str): The temporary folder to store downloaded segments.
|
|
60
|
+
- is_index_url (bool): Flag indicating if `m3u8_index` is a URL (default True).
|
|
61
|
+
"""
|
|
62
|
+
self.url = url
|
|
63
|
+
self.tmp_folder = tmp_folder
|
|
64
|
+
self.is_index_url = is_index_url
|
|
65
|
+
self.expected_real_time = None
|
|
66
|
+
self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts")
|
|
67
|
+
os.makedirs(self.tmp_folder, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
# Util class
|
|
70
|
+
self.decryption: M3U8_Decryption = None
|
|
71
|
+
self.class_ts_estimator = M3U8_Ts_Estimator(0, self)
|
|
72
|
+
self.class_url_fixer = M3U8_UrlFix(url)
|
|
73
|
+
|
|
74
|
+
# Sync
|
|
75
|
+
self.queue = PriorityQueue()
|
|
76
|
+
self.buffer = {}
|
|
77
|
+
self.expected_index = 0
|
|
78
|
+
|
|
79
|
+
self.stop_event = threading.Event()
|
|
80
|
+
self.downloaded_segments = set()
|
|
81
|
+
self.base_timeout = 0.5
|
|
82
|
+
self.current_timeout = 3.0
|
|
83
|
+
|
|
84
|
+
# Stopping
|
|
85
|
+
self.interrupt_flag = threading.Event()
|
|
86
|
+
self.download_interrupted = False
|
|
87
|
+
self.interrupt_count = 0
|
|
88
|
+
self.force_stop = False
|
|
89
|
+
self.interrupt_lock = threading.Lock()
|
|
90
|
+
|
|
91
|
+
# OTHER INFO
|
|
92
|
+
self.info_maxRetry = 0
|
|
93
|
+
self.info_nRetry = 0
|
|
94
|
+
self.info_nFailed = 0
|
|
95
|
+
self.active_retries = 0
|
|
96
|
+
self.active_retries_lock = threading.Lock()
|
|
97
|
+
|
|
98
|
+
def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes:
|
|
99
|
+
"""
|
|
100
|
+
Fetches the encryption key from the M3U8 playlist.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
m3u8_parser (M3U8_Parser): An instance of M3U8_Parser containing parsed M3U8 data.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
bytes: The decryption key in byte format.
|
|
107
|
+
"""
|
|
108
|
+
key_uri = urljoin(self.url, m3u8_parser.keys.get('uri'))
|
|
109
|
+
parsed_url = urlparse(key_uri)
|
|
110
|
+
self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT, 'verify': REQUEST_VERIFY}
|
|
114
|
+
response = httpx.get(url=key_uri, **client_params)
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
|
|
117
|
+
hex_content = binascii.hexlify(response.content).decode('utf-8')
|
|
118
|
+
return bytes.fromhex(hex_content)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise Exception(f"Failed to fetch key: {e}")
|
|
122
|
+
|
|
123
|
+
def parse_data(self, m3u8_content: str) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Parses the M3U8 content and extracts necessary data.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
m3u8_content (str): The raw M3U8 playlist content.
|
|
129
|
+
"""
|
|
130
|
+
m3u8_parser = M3U8_Parser()
|
|
131
|
+
m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content)
|
|
132
|
+
|
|
133
|
+
self.expected_real_time_s = m3u8_parser.duration
|
|
134
|
+
|
|
135
|
+
if m3u8_parser.keys:
|
|
136
|
+
key = self.__get_key__(m3u8_parser)
|
|
137
|
+
self.decryption = M3U8_Decryption(
|
|
138
|
+
key,
|
|
139
|
+
m3u8_parser.keys.get('iv'),
|
|
140
|
+
m3u8_parser.keys.get('method')
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self.segments = [
|
|
144
|
+
self.class_url_fixer.generate_full_url(seg)
|
|
145
|
+
if "http" not in seg else seg
|
|
146
|
+
for seg in m3u8_parser.segments
|
|
147
|
+
]
|
|
148
|
+
self.class_ts_estimator.total_segments = len(self.segments)
|
|
149
|
+
|
|
150
|
+
def get_info(self) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Retrieves M3U8 playlist information from the given URL.
|
|
153
|
+
|
|
154
|
+
If the URL is an index URL, this method:
|
|
155
|
+
- Sends an HTTP GET request to fetch the M3U8 playlist.
|
|
156
|
+
- Parses the M3U8 content using `parse_data`.
|
|
157
|
+
- Saves the playlist to a temporary folder.
|
|
158
|
+
"""
|
|
159
|
+
if self.is_index_url:
|
|
160
|
+
try:
|
|
161
|
+
client_params = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT, 'verify': REQUEST_VERIFY}
|
|
162
|
+
response = httpx.get(self.url, **client_params, follow_redirects=True)
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
|
|
165
|
+
self.parse_data(response.text)
|
|
166
|
+
with open(os.path.join(self.tmp_folder, "playlist.m3u8"), "w") as f:
|
|
167
|
+
f.write(response.text)
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
raise RuntimeError(f"M3U8 info retrieval failed: {e}")
|
|
171
|
+
|
|
172
|
+
def setup_interrupt_handler(self):
|
|
173
|
+
"""
|
|
174
|
+
Set up a signal handler for graceful interruption.
|
|
175
|
+
"""
|
|
176
|
+
def interrupt_handler(signum, frame):
|
|
177
|
+
with self.interrupt_lock:
|
|
178
|
+
self.interrupt_count += 1
|
|
179
|
+
if self.interrupt_count >= MAX_INTERRUPT_COUNT:
|
|
180
|
+
self.force_stop = True
|
|
181
|
+
|
|
182
|
+
if self.force_stop:
|
|
183
|
+
console.print("\n[red]Force stop triggered! Exiting immediately.")
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
if not self.interrupt_flag.is_set():
|
|
187
|
+
remaining = MAX_INTERRUPT_COUNT - self.interrupt_count
|
|
188
|
+
console.print(f"\n[red]- Stopping gracefully... (Ctrl+C {remaining}x to force)")
|
|
189
|
+
self.download_interrupted = True
|
|
190
|
+
|
|
191
|
+
if remaining == 1:
|
|
192
|
+
self.interrupt_flag.set()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if threading.current_thread() is threading.main_thread():
|
|
196
|
+
signal.signal(signal.SIGINT, interrupt_handler)
|
|
197
|
+
else:
|
|
198
|
+
print("Signal handler must be set in the main thread")
|
|
199
|
+
|
|
200
|
+
def _get_http_client(self):
|
|
201
|
+
client_params = {
|
|
202
|
+
'headers': {'User-Agent': get_userAgent()},
|
|
203
|
+
'timeout': SEGMENT_MAX_TIMEOUT,
|
|
204
|
+
'follow_redirects': True,
|
|
205
|
+
'http2': False,
|
|
206
|
+
'verify': REQUEST_VERIFY
|
|
207
|
+
}
|
|
208
|
+
return httpx.Client(**client_params)
|
|
209
|
+
|
|
210
|
+
def download_segment(self, ts_url: str, index: int, progress_bar: tqdm, backoff_factor: float = 1.1) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Downloads a TS segment and adds it to the segment queue with retry logic.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
- ts_url (str): The URL of the TS segment.
|
|
216
|
+
- index (int): The index of the segment.
|
|
217
|
+
- progress_bar (tqdm): Progress counter for tracking download progress.
|
|
218
|
+
- backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds).
|
|
219
|
+
"""
|
|
220
|
+
for attempt in range(REQUEST_MAX_RETRY):
|
|
221
|
+
if self.interrupt_flag.is_set():
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
with self._get_http_client() as client:
|
|
226
|
+
response = client.get(ts_url)
|
|
227
|
+
|
|
228
|
+
# Validate response and content
|
|
229
|
+
response.raise_for_status()
|
|
230
|
+
segment_content = response.content
|
|
231
|
+
content_size = len(segment_content)
|
|
232
|
+
|
|
233
|
+
# Decrypt if needed and verify decrypted content
|
|
234
|
+
if self.decryption is not None:
|
|
235
|
+
try:
|
|
236
|
+
segment_content = self.decryption.decrypt(segment_content)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logging.error(f"Decryption failed for segment {index}: {str(e)}")
|
|
240
|
+
self.interrupt_flag.set() # Interrupt the download process
|
|
241
|
+
self.stop_event.set() # Trigger the stopping event for all threads
|
|
242
|
+
break # Stop the current task immediately
|
|
243
|
+
|
|
244
|
+
self.class_ts_estimator.update_progress_bar(content_size, progress_bar)
|
|
245
|
+
self.queue.put((index, segment_content))
|
|
246
|
+
self.downloaded_segments.add(index)
|
|
247
|
+
progress_bar.update(1)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logging.info(f"Attempt {attempt + 1} failed for segment {index} - '{ts_url}': {e}")
|
|
252
|
+
|
|
253
|
+
if attempt > self.info_maxRetry:
|
|
254
|
+
self.info_maxRetry = ( attempt + 1 )
|
|
255
|
+
self.info_nRetry += 1
|
|
256
|
+
|
|
257
|
+
if attempt + 1 == REQUEST_MAX_RETRY:
|
|
258
|
+
console.log(f"[red]Final retry failed for segment: {index}")
|
|
259
|
+
self.queue.put((index, None)) # Marker for failed segment
|
|
260
|
+
progress_bar.update(1)
|
|
261
|
+
self.info_nFailed += 1
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
with self.active_retries_lock:
|
|
265
|
+
self.active_retries += 1
|
|
266
|
+
|
|
267
|
+
sleep_time = backoff_factor * (2 ** attempt)
|
|
268
|
+
logging.info(f"Retrying segment {index} in {sleep_time} seconds...")
|
|
269
|
+
time.sleep(sleep_time)
|
|
270
|
+
|
|
271
|
+
with self.active_retries_lock:
|
|
272
|
+
self.active_retries -= 1
|
|
273
|
+
|
|
274
|
+
def write_segments_to_file(self):
|
|
275
|
+
"""
|
|
276
|
+
Writes segments to file with additional verification.
|
|
277
|
+
"""
|
|
278
|
+
with open(self.tmp_file_path, 'wb') as f:
|
|
279
|
+
while not self.stop_event.is_set() or not self.queue.empty():
|
|
280
|
+
if self.interrupt_flag.is_set():
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
index, segment_content = self.queue.get(timeout=self.current_timeout)
|
|
285
|
+
|
|
286
|
+
# Successful queue retrieval: reduce timeout
|
|
287
|
+
self.current_timeout = max(self.base_timeout, self.current_timeout / 2)
|
|
288
|
+
|
|
289
|
+
# Handle failed segments
|
|
290
|
+
if segment_content is None:
|
|
291
|
+
if index == self.expected_index:
|
|
292
|
+
self.expected_index += 1
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
# Write segment if it's the next expected one
|
|
296
|
+
if index == self.expected_index:
|
|
297
|
+
f.write(segment_content)
|
|
298
|
+
f.flush()
|
|
299
|
+
self.expected_index += 1
|
|
300
|
+
|
|
301
|
+
# Write any buffered segments that are now in order
|
|
302
|
+
while self.expected_index in self.buffer:
|
|
303
|
+
next_segment = self.buffer.pop(self.expected_index)
|
|
304
|
+
|
|
305
|
+
if next_segment is not None:
|
|
306
|
+
f.write(next_segment)
|
|
307
|
+
f.flush()
|
|
308
|
+
|
|
309
|
+
self.expected_index += 1
|
|
310
|
+
|
|
311
|
+
else:
|
|
312
|
+
self.buffer[index] = segment_content
|
|
313
|
+
|
|
314
|
+
except queue.Empty:
|
|
315
|
+
self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
|
|
316
|
+
time.sleep(0.05)
|
|
317
|
+
|
|
318
|
+
if self.stop_event.is_set():
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logging.error(f"Error writing segment {index}: {str(e)}")
|
|
323
|
+
|
|
324
|
+
def download_streams(self, description: str, type: str):
|
|
325
|
+
"""
|
|
326
|
+
Downloads all TS segments in parallel and writes them to a file.
|
|
327
|
+
|
|
328
|
+
Parameters:
|
|
329
|
+
- description: Description to insert on tqdm bar
|
|
330
|
+
- type (str): Type of download: 'video' or 'audio'
|
|
331
|
+
"""
|
|
332
|
+
if TELEGRAM_BOT:
|
|
333
|
+
|
|
334
|
+
# Viene usato per lo screen
|
|
335
|
+
console.log("####")
|
|
336
|
+
|
|
337
|
+
self.get_info()
|
|
338
|
+
self.setup_interrupt_handler()
|
|
339
|
+
|
|
340
|
+
progress_bar = tqdm(
|
|
341
|
+
total=len(self.segments),
|
|
342
|
+
unit='s',
|
|
343
|
+
ascii='░▒█',
|
|
344
|
+
bar_format=self._get_bar_format(description),
|
|
345
|
+
mininterval=0.6,
|
|
346
|
+
maxinterval=1.0,
|
|
347
|
+
file=sys.stdout, # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
writer_thread = threading.Thread(target=self.write_segments_to_file)
|
|
352
|
+
writer_thread.daemon = True
|
|
353
|
+
writer_thread.start()
|
|
354
|
+
|
|
355
|
+
# Configure workers and delay
|
|
356
|
+
max_workers = self._get_worker_count(type)
|
|
357
|
+
|
|
358
|
+
# Download segments with completion verification
|
|
359
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
360
|
+
futures = []
|
|
361
|
+
for index, segment_url in enumerate(self.segments):
|
|
362
|
+
|
|
363
|
+
# Check for interrupt before submitting each task
|
|
364
|
+
if self.interrupt_flag.is_set():
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
time.sleep(TQDM_DELAY_WORKER)
|
|
368
|
+
futures.append(executor.submit(self.download_segment, segment_url, index, progress_bar))
|
|
369
|
+
|
|
370
|
+
# Wait for futures with interrupt handling
|
|
371
|
+
for future in as_completed(futures):
|
|
372
|
+
if self.interrupt_flag.is_set():
|
|
373
|
+
break
|
|
374
|
+
try:
|
|
375
|
+
future.result()
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logging.error(f"Error in download thread: {str(e)}")
|
|
378
|
+
|
|
379
|
+
# Interrupt handling for missing segments
|
|
380
|
+
if not self.interrupt_flag.is_set():
|
|
381
|
+
total_segments = len(self.segments)
|
|
382
|
+
completed_segments = len(self.downloaded_segments)
|
|
383
|
+
|
|
384
|
+
if completed_segments < total_segments:
|
|
385
|
+
missing_segments = set(range(total_segments)) - self.downloaded_segments
|
|
386
|
+
logging.warning(f"Missing segments: {sorted(missing_segments)}")
|
|
387
|
+
|
|
388
|
+
# Retry missing segments with interrupt check
|
|
389
|
+
for index in missing_segments:
|
|
390
|
+
if self.interrupt_flag.is_set():
|
|
391
|
+
break
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
self.download_segment(self.segments[index], index, progress_bar)
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logging.error(f"Failed to retry segment {index}: {str(e)}")
|
|
398
|
+
|
|
399
|
+
finally:
|
|
400
|
+
self._cleanup_resources(writer_thread, progress_bar)
|
|
401
|
+
|
|
402
|
+
if not self.interrupt_flag.is_set():
|
|
403
|
+
self._verify_download_completion()
|
|
404
|
+
|
|
405
|
+
return self._generate_results(type)
|
|
406
|
+
|
|
407
|
+
def _get_bar_format(self, description: str) -> str:
|
|
408
|
+
"""
|
|
409
|
+
Generate platform-appropriate progress bar format.
|
|
410
|
+
"""
|
|
411
|
+
return (
|
|
412
|
+
f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{description}{Colors.WHITE}): "
|
|
413
|
+
f"{Colors.RED}{{percentage:.2f}}% "
|
|
414
|
+
f"{Colors.MAGENTA}{{bar}} "
|
|
415
|
+
f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.WHITE}{{postfix}}{Colors.WHITE}"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def _get_worker_count(self, stream_type: str) -> int:
|
|
419
|
+
"""
|
|
420
|
+
Calculate optimal parallel workers based on stream type and infrastructure.
|
|
421
|
+
"""
|
|
422
|
+
base_workers = {
|
|
423
|
+
'video': DEFAULT_VIDEO_WORKERS,
|
|
424
|
+
'audio': DEFAULT_AUDIO_WORKERS
|
|
425
|
+
}.get(stream_type.lower(), 1)
|
|
426
|
+
|
|
427
|
+
return base_workers
|
|
428
|
+
|
|
429
|
+
def _generate_results(self, stream_type: str) -> Dict:
|
|
430
|
+
"""Package final download results."""
|
|
431
|
+
return {
|
|
432
|
+
'type': stream_type,
|
|
433
|
+
'nFailed': self.info_nFailed,
|
|
434
|
+
'stopped': self.download_interrupted
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
def _verify_download_completion(self) -> None:
|
|
438
|
+
"""Validate final download integrity."""
|
|
439
|
+
total = len(self.segments)
|
|
440
|
+
if len(self.downloaded_segments) / total < 0.999:
|
|
441
|
+
missing = sorted(set(range(total)) - self.downloaded_segments)
|
|
442
|
+
raise RuntimeError(f"Download incomplete ({len(self.downloaded_segments)/total:.1%}). Missing segments: {missing}")
|
|
443
|
+
|
|
444
|
+
def _cleanup_resources(self, writer_thread: threading.Thread, progress_bar: tqdm) -> None:
|
|
445
|
+
"""Ensure resource cleanup and final reporting."""
|
|
446
|
+
self.stop_event.set()
|
|
447
|
+
writer_thread.join(timeout=30)
|
|
448
|
+
progress_bar.close()
|
|
449
|
+
|
|
450
|
+
if self.info_nFailed > 0:
|
|
451
|
+
self._display_error_summary()
|
|
452
|
+
|
|
453
|
+
self.buffer = {}
|
|
454
|
+
self.expected_index = 0
|
|
455
|
+
|
|
456
|
+
def _display_error_summary(self) -> None:
|
|
457
|
+
"""Generate final error report."""
|
|
458
|
+
console.print(f"\n[cyan]Retry Summary: "
|
|
459
|
+
f"[white]Max retries: [green]{self.info_maxRetry} "
|
|
460
|
+
f"[white]Total retries: [green]{self.info_nRetry} "
|
|
461
|
+
f"[white]Failed segments: [red]{self.info_nFailed}")
|
|
462
|
+
|
|
463
|
+
if self.info_nRetry > len(self.segments) * 0.3:
|
|
464
|
+
console.print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.")
|