try fix stuttering

This commit is contained in:
Ghost 2024-04-09 17:33:10 +02:00
parent 37b2161a1a
commit dafb0f201b
5 changed files with 127 additions and 52 deletions

View File

@ -44,13 +44,14 @@ from .util import (
M3U8_Decryption,
M3U8_Ts_Files,
M3U8_Parser,
M3U8_Codec,
M3U8_UrlFix
)
# Config
Download_audio = config_manager.get_bool('M3U8_OPTIONS', 'download_audio')
Donwload_subtitles = config_manager.get_bool('M3U8_OPTIONS', 'download_subtitles')
DOWNLOAD_AUDIO = config_manager.get_bool('M3U8_OPTIONS', 'download_audio')
DOWNLOAD_SUBTITLES = config_manager.get_bool('M3U8_OPTIONS', 'download_subtitles')
DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_OPTIONS', 'specific_list_audio')
DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_OPTIONS', 'specific_list_subtitles')
TQDM_MAX_WORKER = config_manager.get_int('M3U8', 'tdqm_workers')
@ -394,10 +395,10 @@ class M3U8_Segments:
# Refresh progress bar
progress_counter.refresh()
def join(self, output_filename: str, video_decoding: str = None, audio_decoding: str = None):
def join(self, output_filename: str):
"""
Join all segments file to a mp4 file name
!! NOT USED
!! NOT USED IN THIS VERSION
Parameters:
- video_decoding(str): video decoding to use with ffmpeg for only video
@ -437,9 +438,7 @@ class M3U8_Segments:
# ADD IF
concatenate_and_save(
file_list_path = file_list_path,
output_filename = output_filename,
video_decoding = video_decoding,
audio_decoding = audio_decoding
output_filename = output_filename
)
@ -455,7 +454,6 @@ class Downloader():
- key (str, optional): Hexadecimal representation of the encryption key.
"""
self.m3u8_playlist = m3u8_playlist
self.m3u8_index = m3u8_index
self.key = bytes.fromhex(key) if key is not None else key
@ -471,15 +469,16 @@ class Downloader():
if self.key != None:
hex_data = convert_to_hex(self.key)
console.log(f"[cyan]Key use [white]=> [red]{hex_data}")
logging.info(f"Key use: {self.key}")
# Initialize temp base path
self.base_path = os.path.join(str(self.output_filename).replace(".mp4", ""))
self.video_segments_path = os.path.join(self.base_path, "tmp", "video")
self.audio_segments_path = os.path.join(self.base_path, "tmp", "audio")
self.subtitle_segments_path = os.path.join(self.base_path, "tmp", "subtitle")
logging.info(f"Output base path: {self.base_path}")
# Create temp folder
logging.info("Create temp folder")
os.makedirs(self.video_segments_path, exist_ok=True)
os.makedirs(self.audio_segments_path, exist_ok=True)
os.makedirs(self.subtitle_segments_path, exist_ok=True)
@ -489,13 +488,9 @@ class Downloader():
self.downloaded_subtitle = []
self.downloaded_video = []
# Default decoding
self.video_decoding = "avc1.640028"
self.audio_decoding = "mp4a.40.2"
def __df_make_req__(self, url: str) -> str:
"""
Make a request to get text from the provided URL.
Make a request to get text from the provided URL to test if index or m3u8 work correcly.
Parameters:
- url (str): The URL to make the request to.
@ -503,23 +498,30 @@ class Downloader():
Returns:
- str: The text content of the response.
"""
try:
# Send a GET request to the provided URL
config_headers.get('index')['user-agent'] = get_headers()
response = requests.get(url, headers=config_headers.get('index'))
# Check status response of request
logging.info(f"Test url: {url}")
response.raise_for_status()
if response.ok:
return response.text
else:
logging.error(f"[df_make_req] Request to {url} failed with status code: {response.status_code}")
logging.error(f"Request to {url} failed with status code: {response.status_code}")
return None
except requests.RequestException as req_err:
logging.error(f"[df_make_req] Error occurred during request: {req_err}")
logging.error(f"Error occurred during request: {req_err}")
return None
except Exception as e:
logging.error(f"[df_make_req] An unexpected error occurred: {e}")
logging.error(f"An unexpected error occurred: {e}")
return None
def manage_playlist(self, m3u8_playlist_text):
@ -530,7 +532,7 @@ class Downloader():
m3u8_playlist_text (str): The text content of the M3U8 playlist.
"""
global Download_audio, Donwload_subtitles
global DOWNLOAD_AUDIO, DOWNLOAD_SUBTITLES
# Create an instance of the M3U8_Parser class
parse_class_m3u8 = M3U8_Parser(DOWNLOAD_SPECIFIC_SUBTITLE)
@ -547,7 +549,7 @@ class Downloader():
console.log(f"[cyan]Find audios language: [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}")
else:
console.log("[red]Cant find a list of audios")
Download_audio = False
DOWNLOAD_AUDIO = False
# Collect available subtitles and default subtitle
self.list_available_subtitles = parse_class_m3u8.get_subtitles()
@ -558,7 +560,7 @@ class Downloader():
console.log(f"[cyan]Find subtitles language: [red]{[obj_sub.get('language') for obj_sub in self.list_available_subtitles]}")
else:
console.log("[red]Cant find a list of audios")
Donwload_subtitles = False
DOWNLOAD_SUBTITLES = False
# Collect best quality video
m3u8_index_obj = parse_class_m3u8.get_best_quality()
@ -566,7 +568,6 @@ class Downloader():
# Get URI of the best quality and codecs parameters
console.log(f"[cyan]Select resolution: [red]{m3u8_index_obj.get('width')}")
m3u8_index = m3u8_index_obj.get('uri')
m3u8_index_decoding = m3u8_index_obj.get('codecs')
# Fix URL if it is not complete with http:\\site_name.domain\...
if "http" not in m3u8_index:
@ -581,13 +582,12 @@ class Downloader():
logging.warning("[download_m3u8] Can't find a valid m3u8 index")
sys.exit(0)
# Collect best index, video decoding, and audio decoding
# Set m3u8_index
self.m3u8_index = m3u8_index
# if is present in playlist
if m3u8_index_decoding != None:
self.video_decoding = m3u8_index_decoding.split(",")[0]
self.audio_decoding = m3u8_index_decoding.split(",")[1]
# Get obj codec
self.codec: M3U8_Codec = parse_class_m3u8.codec
logging.info(f"Get coded: {self.codec}")
def manage_subtitle(self):
"""
@ -622,8 +622,8 @@ class Downloader():
'path': os.path.abspath(sub_full_path)
})
# If the subtitle file doesn't exist, download it
logging.info(f"Download uri subtitles: {obj_subtitle.get('uri')} => {sub_full_path}")
response = requests.get(obj_subtitle.get('uri'))
open(sub_full_path, "wb").write(response.content)
@ -658,6 +658,7 @@ class Downloader():
if not os.path.exists(full_path_audio):
# If the audio segment directory doesn't exist, download audio segments
logging.info(f"Download uri audio: {obj_audio.get('uri')} => {full_path_audio}")
audio_m3u8 = M3U8_Segments(obj_audio.get('uri'), full_path_audio, self.key)
console.log(f"[purple]Download audio segments [white]=> [red]{obj_audio.get('language')}.")
@ -716,7 +717,6 @@ class Downloader():
ts_files = [f for f in os.listdir(full_path) if f.endswith(".ts")]
ts_files.sort(key=Downloader.extract_number)
logging.info(f"Find {len(ts_files)} stream files to join")
logging.info(f"Using parameter: \n-c:v = {self.video_decoding} -c:a = {self.audio_decoding}])")
# Check if there are enough .ts files to join (at least 10)
if len(ts_files) < 10:
@ -735,8 +735,9 @@ class Downloader():
return concatenate_and_save(
file_list_path=file_list_path,
output_filename=out_file_name,
video_decoding=self.video_decoding,
audio_decoding=self.audio_decoding
v_codec=self.codec.video_codec,
a_codec=self.codec.audio_codec,
bandwidth=self.codec.bandwidth
)
def download_audios(self):
@ -903,12 +904,12 @@ class Downloader():
self.manage_playlist(m3u8_playlist_text)
# Download subtitles
if Donwload_subtitles:
if DOWNLOAD_SUBTITLES:
logging.info("Download subtitles ...")
self.manage_subtitle()
# Download segmenets of audio tracks
if Download_audio:
if DOWNLOAD_AUDIO:
logging.info("Download audios ...")
self.manage_audio()

View File

@ -13,5 +13,5 @@ from .helper import (
from .decryption import M3U8_Decryption
from .installer import check_ffmpeg
from .math_calc import M3U8_Ts_Files
from .parser import M3U8_Parser
from .parser import M3U8_Parser, M3U8_Codec
from .url_fix import M3U8_UrlFix

View File

@ -5,6 +5,7 @@ import os
import json
import logging
import shutil
import sys
# External libraries
@ -182,17 +183,19 @@ def add_subtitle(input_video_path: str, input_subtitle_path: str, output_video_p
return output_video_path
def concatenate_and_save(file_list_path: str, output_filename: str, video_decoding: str = None, audio_decoding: str = None, prefix: str = "segments", output_directory: str = None) -> str:
def concatenate_and_save(file_list_path: str, output_filename: str, v_codec: str = None, a_codec: str = None, bandwidth: int = None, prefix: str = "segments", output_directory: str = None) -> str:
"""
Concatenate input files and save the output with specified decoding parameters.
Parameters:
- file_list_path (str): Path to the file list containing the segments.
- output_filename (str): Output filename for the concatenated video.
- video_decoding (str): Video decoding parameter (optional).
- audio_decoding (str): Audio decoding parameter (optional).
- v_codec (str): Video decoding parameter (optional).
- a_codec (str): Audio decoding parameter (optional).
- bandwidth (int): Bitrate for the output video (optional).
- prefix (str): Prefix to add at the end of output file name (default is "segments").
- output_directory (str): Directory to save the output file. If not provided, defaults to the current directory.
- codecs (str): Codecs for video and audio (optional).
Returns:
- output_file_path (str): Path to the saved output file.
@ -208,9 +211,17 @@ def concatenate_and_save(file_list_path: str, output_filename: str, video_decodi
output_args = {
'c': 'copy',
'loglevel': DEBUG_FFMPEG,
'y': None
'y': None,
}
# Add BANDWIDTH and CODECS if provided
if bandwidth is not None:
output_args['b:v'] = str(bandwidth)
"""if v_codec is not None:
output_args['vcodec'] = v_codec
if a_codec is not None:
output_args['acodec'] = a_codec"""
# Set up the output file name by modifying the video file name
output_file_name = os.path.splitext(output_filename)[0] + f"_{prefix}.mp4"

View File

@ -12,6 +12,58 @@ import requests
from m3u8 import M3U8
class M3U8_Codec():
"""
Represents codec information for an M3U8 playlist.
Attributes:
- bandwidth (int): Bandwidth of the codec.
- resolution (str): Resolution of the codec.
- codecs (str): Codecs information in the format "avc1.xxxxxx,mp4a.xx".
- audio_codec (str): Audio codec extracted from the codecs information.
- video_codec (str): Video codec extracted from the codecs information.
"""
def __init__(self, bandwidth, resolution, codecs):
"""
Initializes the M3U8Codec object with the provided parameters.
Parameters:
- bandwidth (int): Bandwidth of the codec.
- resolution (str): Resolution of the codec.
- codecs (str): Codecs information in the format "avc1.xxxxxx,mp4a.xx".
"""
self.bandwidth = bandwidth
self.resolution = resolution
self.codecs = codecs
self.audio_codec = None
self.video_codec = None
self.parse_codecs()
def parse_codecs(self):
"""
Parses the codecs information to extract audio and video codecs.
Extracted codecs are set as attributes: audio_codec and video_codec.
"""
# Split the codecs string by comma
codecs_list = self.codecs.split(',')
# Separate audio and video codecs
for codec in codecs_list:
if codec.startswith('avc'):
self.video_codec = codec
elif codec.startswith('mp4a'):
self.audio_codec = codec
def __str__(self):
"""
Returns a string representation of the M3U8Codec object.
"""
return f"BANDWIDTH={self.bandwidth},RESOLUTION={self.resolution},CODECS=\"{self.codecs}\""
class M3U8_Parser:
def __init__(self, DOWNLOAD_SPECIFIC_SUBTITLE = None):
"""
@ -24,6 +76,7 @@ class M3U8_Parser:
self.subtitle_playlist = [] # No vvt ma url a vvt
self.subtitle = [] # Url a vvt
self.audio_ts = []
self.codec: M3U8_Codec = None
self.DOWNLOAD_SPECIFIC_SUBTITLE = DOWNLOAD_SPECIFIC_SUBTITLE
def parse_data(self, m3u8_content: str) -> (None):
@ -41,12 +94,20 @@ class M3U8_Parser:
# Collect video info with url, resolution and codecs
for playlist in m3u8_obj.playlists:
self.video_playlist.append({
"uri": playlist.uri,
"width": playlist.stream_info.resolution,
"codecs": playlist.stream_info.codecs
})
self.codec = M3U8_Codec(
playlist.stream_info.bandwidth,
playlist.stream_info.resolution,
playlist.stream_info.codecs
)
logging.info(f"Parse: {playlist.stream_info}")
logging.info(f"Coded test: {self.codec.bandwidth}")
# Collect info of encryption if present, method, uri and iv
for key in m3u8_obj.keys:
if key is not None:
@ -92,7 +153,7 @@ class M3U8_Parser:
self.subtitle.append(segment.uri)
except Exception as e:
logging.error(f"[M3U8_Parser] Error parsing M3U8 content: {e}")
logging.error(f"Error parsing M3U8 content: {e}")
def get_resolution(self, uri: str) -> (int):
"""
@ -134,8 +195,8 @@ class M3U8_Parser:
return sorted_uris[0]
except:
logging.error("[M3U8_Parser] Error: Can't find M3U8 resolution by width...")
logging.info("[M3U8_Parser] Try searching in URI")
logging.error("Error: Can't find M3U8 resolution by width...")
logging.info("Try searching in URI")
# Sort the list of video playlist items based on the 'width' attribute if present,
# otherwise, use the resolution obtained from the 'uri' attribute as a fallback.
@ -146,7 +207,7 @@ class M3U8_Parser:
return sorted_uris[0]
else:
logging.info("[M3U8_Parser] No video playlists found.")
logging.info("No video playlists found.")
return None
def get_subtitles(self):
@ -168,7 +229,7 @@ class M3U8_Parser:
# Get language name
name_language = sub_info.get("language")
logging.info(f"[M3U8_Parser] Find subtitle: {name_language}")
logging.info(f"Find subtitle: {name_language}")
# Check if there is custom subtitles to download
if len(self.DOWNLOAD_SPECIFIC_SUBTITLE) > 0:
@ -178,7 +239,7 @@ class M3U8_Parser:
continue
# Make request to m3u8 subtitle to extract vtt
logging.info(f"[M3U8_Parser] Download subtitle: {name_language}")
logging.info(f"Download subtitle: {name_language}")
req_sub_content = requests.get(sub_info.get("uri"), headers={'user-agent': get_headers()})
try:
@ -196,13 +257,13 @@ class M3U8_Parser:
})
except Exception as e:
logging.error(f"[M3U8_Parser] Cant donwload: {name_language}, error: {e}")
logging.error(f"Cant donwload: {name_language}, error: {e}")
# Return
return output
else:
logging.info("[M3U8_Parser] No subtitle find")
logging.info("No subtitle find")
return None
def get_track_audios(self) -> list:
@ -213,10 +274,10 @@ class M3U8_Parser:
list: A list of dictionaries containing language and URI information for audio tracks, or None if no audio tracks are found.
"""
logging.info(f"[M3U8_Parser] Finding {len(self.audio_ts)} playlist(s) with audio.")
logging.info(f"Finding {len(self.audio_ts)} playlist(s) with audio.")
if self.audio_ts:
logging.info("[M3U8_Parser] Getting list of available audio names")
logging.info("Getting list of available audio names")
list_output = []
# For all languages present in m3u8
@ -232,7 +293,7 @@ class M3U8_Parser:
return list_output
else:
logging.info("[M3U8_Parser] No audio tracks found")
logging.info("No audio tracks found")
return None
def get_default_subtitle(self):
@ -290,3 +351,4 @@ class M3U8_Parser:
# Return the default audio track dictionary
return dict_default_audio

View File

@ -1,6 +1,6 @@
# 07.04.24
# to do
# run somehwere backup
# add config to trace if ffmpeg is install, using config in local or temp
@ -11,6 +11,7 @@ import logging
# Winreg only work for windows
if platform.system() == "Windows":
# Winreg only work for windows
import winreg
# Define Windows registry key for user environment variables