mirror of
https://github.com/Arrowar/StreamingCommunity.git
synced 2025-06-05 02:55:25 +00:00
Add qbit config.
Add separate cookie manager in config. Add api to emulate node js . Add 0 at the end of episode. Add except time vs real time in HLS.
This commit is contained in:
parent
433928b50f
commit
2b4d355d17
@ -29,8 +29,6 @@ Make sure you have the following prerequisites installed on your system:
|
||||
* [ffmpeg](https://www.gyan.dev/ffmpeg/builds/)
|
||||
* [opnessl](https://www.openssl.org) or [pycryptodome](https://pypi.org/project/pycryptodome/)
|
||||
|
||||
* [nodejs](https://nodejs.org/)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the required Python libraries using the following command:
|
||||
|
@ -14,6 +14,27 @@ from Src.Util.os import remove_special_characters
|
||||
MAP_EPISODE = config_manager.get('DEFAULT', 'map_episode_name')
|
||||
|
||||
|
||||
def dynamic_format_number(n: int) -> str:
|
||||
"""
|
||||
Formats a number by adding a leading zero.
|
||||
The width of the resulting string is dynamic, calculated as the number of digits in the number plus one.
|
||||
|
||||
Args:
|
||||
- n (int): The number to format.
|
||||
|
||||
Returns:
|
||||
- str: The formatted number as a string with a leading zero.
|
||||
|
||||
Examples:
|
||||
>>> dynamic_format_number(1)
|
||||
'01'
|
||||
>>> dynamic_format_number(20)
|
||||
'020'
|
||||
"""
|
||||
width = len(str(n)) + 1
|
||||
return str(n).zfill(width)
|
||||
|
||||
|
||||
def manage_selection(cmd_insert: str, max_count: int) -> List[int]:
|
||||
"""
|
||||
Manage user selection for seasons to download.
|
||||
@ -45,6 +66,7 @@ def manage_selection(cmd_insert: str, max_count: int) -> List[int]:
|
||||
logging.info(f"List return: {list_season_select}")
|
||||
return list_season_select
|
||||
|
||||
|
||||
def map_episode_title(tv_name: str, number_season: int, episode_number: int, episode_name: str) -> str:
|
||||
"""
|
||||
Maps the episode title to a specific format.
|
||||
@ -60,8 +82,8 @@ def map_episode_title(tv_name: str, number_season: int, episode_number: int, epi
|
||||
"""
|
||||
map_episode_temp = MAP_EPISODE
|
||||
map_episode_temp = map_episode_temp.replace("%(tv_name)", remove_special_characters(tv_name))
|
||||
map_episode_temp = map_episode_temp.replace("%(season)", str(number_season))
|
||||
map_episode_temp = map_episode_temp.replace("%(episode)", str(episode_number))
|
||||
map_episode_temp = map_episode_temp.replace("%(season)", dynamic_format_number(number_season))
|
||||
map_episode_temp = map_episode_temp.replace("%(episode)", dynamic_format_number(episode_number))
|
||||
map_episode_temp = map_episode_temp.replace("%(episode_name)", remove_special_characters(episode_name))
|
||||
|
||||
# Additional fix
|
||||
|
@ -12,7 +12,7 @@ from bs4 import BeautifulSoup
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.headers import get_headers
|
||||
from Src.Util.os import run_node_script
|
||||
from Src.Util.os import run_node_script, run_node_script_api
|
||||
|
||||
|
||||
class VideoSource:
|
||||
@ -118,9 +118,14 @@ class VideoSource:
|
||||
"""
|
||||
for script in soup.find_all("script"):
|
||||
if "eval" in str(script):
|
||||
new_script = str(script.text).replace("eval", "var a = ")
|
||||
new_script = new_script.replace(")))", ")));console.log(a);")
|
||||
return run_node_script(new_script)
|
||||
|
||||
# WITH INSTALL NODE JS
|
||||
#new_script = str(script.text).replace("eval", "var a = ")
|
||||
#new_script = new_script.replace(")))", ")));console.log(a);")
|
||||
#return run_node_script(new_script)
|
||||
|
||||
# WITH API
|
||||
return run_node_script_api(script.text)
|
||||
|
||||
return None
|
||||
|
||||
@ -155,7 +160,7 @@ class VideoSource:
|
||||
pattern = r'data-link="(//supervideo[^"]+)"'
|
||||
match = re.search(pattern, str(down_page_soup))
|
||||
if not match:
|
||||
logging.error("No match found for supervideo URL.")
|
||||
logging.error("No player available for download.")
|
||||
return None
|
||||
|
||||
supervideo_url = "https:" + match.group(1)
|
||||
|
@ -7,7 +7,7 @@ import logging
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.console import console
|
||||
from Src.Lib.Hls.downloader import Downloader
|
||||
from Src.Lib.Downloader import HLS_Downloader
|
||||
from Src.Util.message import start_message
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ def download_film(title_name: str, url: str):
|
||||
master_playlist = video_source.get_playlist()
|
||||
|
||||
# Download the film using the m3u8 playlist, and output filename
|
||||
Downloader(
|
||||
HLS_Downloader(
|
||||
m3u8_playlist = master_playlist,
|
||||
output_filename = os.path.join(mp4_path, mp4_name)
|
||||
).start()
|
@ -6,7 +6,7 @@ import logging
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.console import console, msg
|
||||
from Src.Lib.Hls.downloader import Downloader
|
||||
from Src.Lib.Downloader import HLS_Downloader
|
||||
from Src.Util.message import start_message
|
||||
from ..Template import manage_selection
|
||||
|
||||
@ -50,7 +50,7 @@ def download_episode(index_select: int):
|
||||
mp4_path = os.path.join(ROOT_PATH, SITE_NAME, MOVIE_FOLDER, video_source.series_name)
|
||||
|
||||
# Start downloading
|
||||
Downloader(
|
||||
HLS_Downloader(
|
||||
m3u8_playlist = video_source.get_playlist(),
|
||||
output_filename = os.path.join(mp4_path, mp4_name)
|
||||
).start()
|
||||
|
@ -13,13 +13,16 @@ from bs4 import BeautifulSoup
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.headers import get_headers
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
|
||||
|
||||
# Logic class
|
||||
from .SearchType import MediaItem
|
||||
|
||||
|
||||
# Variable
|
||||
from ...costant import COOKIE
|
||||
|
||||
|
||||
|
||||
class GetSerieInfo:
|
||||
|
||||
@ -28,10 +31,10 @@ class GetSerieInfo:
|
||||
Initializes the GetSerieInfo object with default values.
|
||||
|
||||
Args:
|
||||
dict_serie (MediaItem): Dictionary containing series information (optional).
|
||||
- dict_serie (MediaItem): Dictionary containing series information (optional).
|
||||
"""
|
||||
self.headers = {'user-agent': get_headers()}
|
||||
self.cookies = config_manager.get_dict('REQUESTS', 'index')
|
||||
self.cookies = COOKIE
|
||||
self.url = dict_serie.url
|
||||
self.tv_name = None
|
||||
self.list_episodes = None
|
||||
@ -49,7 +52,7 @@ class GetSerieInfo:
|
||||
|
||||
# Make an HTTP request to the series URL
|
||||
try:
|
||||
response = httpx.get(self.url + "?area=online", cookies=self.cookies, headers=self.headers)
|
||||
response = httpx.get(self.url + "?area=online", cookies=self.cookies, headers=self.headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
|
@ -14,6 +14,10 @@ from Src.Util.headers import get_headers
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
|
||||
|
||||
# Variable
|
||||
from ...costant import COOKIE
|
||||
|
||||
|
||||
class VideoSource:
|
||||
|
||||
def __init__(self) -> None:
|
||||
@ -25,7 +29,7 @@ class VideoSource:
|
||||
cookie (dict): A dictionary to store cookies.
|
||||
"""
|
||||
self.headers = {'user-agent': get_headers()}
|
||||
self.cookie = config_manager.get_dict('REQUESTS', 'index')
|
||||
self.cookie = COOKIE
|
||||
|
||||
def setup(self, url: str) -> None:
|
||||
"""
|
||||
|
@ -10,6 +10,7 @@ from Src.Util._jsonConfig import config_manager
|
||||
SITE_NAME = os.path.basename(os.path.dirname(os.path.abspath(__file__)))
|
||||
ROOT_PATH = config_manager.get('DEFAULT', 'root_path')
|
||||
DOMAIN_NOW = config_manager.get('SITE', SITE_NAME)
|
||||
COOKIE = config_manager.get_dict('SITE', SITE_NAME)['cookie']
|
||||
|
||||
MOVIE_FOLDER = "Movie"
|
||||
SERIES_FOLDER = "Serie"
|
||||
|
@ -7,12 +7,11 @@ from urllib.parse import urlparse
|
||||
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.color import Colors
|
||||
from Src.Util.console import console, msg
|
||||
from Src.Util.console import console
|
||||
from Src.Util.message import start_message
|
||||
from Src.Util.os import create_folder, can_create_file
|
||||
from Src.Util.table import TVShowManager
|
||||
from Src.Lib.Hls.download_mp4 import MP4_downloader
|
||||
from Src.Lib.Downloader import MP4_downloader
|
||||
from ..Template import manage_selection, map_episode_title
|
||||
|
||||
|
||||
@ -70,7 +69,6 @@ def donwload_video(scape_info_serie: GetSerieInfo, index_episode_selected: int)
|
||||
url = master_playlist,
|
||||
path = os.path.join(mp4_path, mp4_name),
|
||||
referer = f"{parsed_url.scheme}://{parsed_url.netloc}/",
|
||||
add_desc=f"{Colors.MAGENTA}video"
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# 09.06.24
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
|
||||
@ -21,7 +22,6 @@ from .Core.Class.SearchType import MediaManager
|
||||
|
||||
# Variable
|
||||
from .costant import SITE_NAME
|
||||
cookie_index = config_manager.get_dict('REQUESTS', 'index')
|
||||
media_search_manager = MediaManager()
|
||||
table_show_manager = TVShowManager()
|
||||
|
||||
|
@ -11,7 +11,7 @@ from bs4 import BeautifulSoup
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.headers import get_headers
|
||||
from Src.Util.os import run_node_script
|
||||
from Src.Util.os import run_node_script, run_node_script_api
|
||||
|
||||
|
||||
class VideoSource:
|
||||
@ -46,7 +46,7 @@ class VideoSource:
|
||||
"""
|
||||
|
||||
try:
|
||||
response = httpx.get(url, headers=self.headers, follow_redirects=True)
|
||||
response = httpx.get(url, headers=self.headers, follow_redirects=True, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
@ -85,9 +85,14 @@ class VideoSource:
|
||||
"""
|
||||
for script in soup.find_all("script"):
|
||||
if "eval" in str(script):
|
||||
new_script = str(script.text).replace("eval", "var a = ")
|
||||
new_script = new_script.replace(")))", ")));console.log(a);")
|
||||
return run_node_script(new_script)
|
||||
|
||||
# WITH INSTALL NODE JS
|
||||
#new_script = str(script.text).replace("eval", "var a = ")
|
||||
#new_script = new_script.replace(")))", ")));console.log(a);")
|
||||
#return run_node_script(new_script)
|
||||
|
||||
# WITH API
|
||||
return run_node_script_api(script.text)
|
||||
|
||||
return None
|
||||
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
from Src.Util.console import console, msg
|
||||
from Src.Util.table import TVShowManager
|
||||
from Src.Util.message import start_message
|
||||
from Src.Lib.Hls.downloader import Downloader
|
||||
from Src.Lib.Downloader import HLS_Downloader
|
||||
from ..Template import manage_selection, map_episode_title
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ def donwload_video(scape_info_serie: GetSerieInfo, index_season_selected: int, i
|
||||
# Get m3u8 master playlist
|
||||
master_playlist = video_source.get_playlist()
|
||||
|
||||
Downloader(
|
||||
HLS_Downloader(
|
||||
m3u8_playlist = master_playlist,
|
||||
output_filename = os.path.join(mp4_path, mp4_name)
|
||||
).start()
|
||||
|
@ -6,7 +6,7 @@ import logging
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.console import console
|
||||
from Src.Lib.Hls.downloader import Downloader
|
||||
from Src.Lib.Downloader import HLS_Downloader
|
||||
from Src.Util.message import start_message
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ def download_film(id_film: str, title_name: str, domain: str):
|
||||
mp4_path = os.path.join(ROOT_PATH, SITE_NAME, MOVIE_FOLDER, title_name)
|
||||
|
||||
# Download the film using the m3u8 playlist, and output filename
|
||||
Downloader(
|
||||
HLS_Downloader(
|
||||
m3u8_playlist = master_playlist,
|
||||
output_filename = os.path.join(mp4_path, mp4_format)
|
||||
).start()
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
from Src.Util.console import console, msg
|
||||
from Src.Util.message import start_message
|
||||
from Src.Util.table import TVShowManager
|
||||
from Src.Lib.Hls.downloader import Downloader
|
||||
from Src.Lib.Downloader import HLS_Downloader
|
||||
from ..Template import manage_selection, map_episode_title
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ def donwload_video(tv_name: str, index_season_selected: int, index_episode_selec
|
||||
master_playlist = video_source.get_playlist()
|
||||
|
||||
# Download the episode
|
||||
Downloader(
|
||||
HLS_Downloader(
|
||||
m3u8_playlist = master_playlist,
|
||||
output_filename = os.path.join(mp4_path, mp4_name)
|
||||
).start()
|
||||
|
3
Src/Lib/Downloader/HLS/__init__.py
Normal file
3
Src/Lib/Downloader/HLS/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# 20.02.24
|
||||
|
||||
from .downloader import HLS_Downloader
|
@ -11,8 +11,6 @@ import httpx
|
||||
from unidecode import unidecode
|
||||
|
||||
|
||||
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.headers import get_headers
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
@ -22,7 +20,7 @@ from Src.Util.os import (
|
||||
remove_folder,
|
||||
delete_files_except_one,
|
||||
compute_sha1_hash,
|
||||
format_size,
|
||||
format_file_size,
|
||||
create_folder,
|
||||
reduce_base_name,
|
||||
remove_special_characters,
|
||||
@ -31,19 +29,18 @@ from Src.Util.os import (
|
||||
|
||||
|
||||
# Logic class
|
||||
from ..FFmpeg import (
|
||||
from ...FFmpeg import (
|
||||
print_duration_table,
|
||||
join_video,
|
||||
join_audios,
|
||||
join_subtitle
|
||||
)
|
||||
from ..M3U8 import (
|
||||
from ...M3U8 import (
|
||||
M3U8_Parser,
|
||||
M3U8_Codec,
|
||||
M3U8_UrlFix
|
||||
)
|
||||
from .segments import M3U8_Segments
|
||||
from ..E_Table import report_table
|
||||
|
||||
|
||||
# Config
|
||||
@ -59,11 +56,11 @@ FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolutio
|
||||
|
||||
|
||||
# Variable
|
||||
headers_index = config_manager.get_dict('REQUESTS', 'index')
|
||||
headers_index = config_manager.get_dict('REQUESTS', 'user-agent')
|
||||
|
||||
|
||||
|
||||
class Downloader():
|
||||
class HLS_Downloader():
|
||||
def __init__(self, output_filename: str = None, m3u8_playlist:str = None, m3u8_index:str = None):
|
||||
|
||||
"""
|
||||
@ -77,7 +74,8 @@ class Downloader():
|
||||
|
||||
self.m3u8_playlist = m3u8_playlist
|
||||
self.m3u8_index = m3u8_index
|
||||
self.output_filename = output_filename.replace(" ", "_")
|
||||
self.output_filename = output_filename
|
||||
self.expected_real_time = None
|
||||
|
||||
# Auto generate out file name if not present
|
||||
if output_filename == None:
|
||||
@ -87,6 +85,8 @@ class Downloader():
|
||||
self.output_filename = os.path.join("missing", compute_sha1_hash(m3u8_index))
|
||||
|
||||
else:
|
||||
|
||||
# For missing output_filename
|
||||
folder, base_name = os.path.split(self.output_filename) # Split file_folder output
|
||||
base_name = reduce_base_name(remove_special_characters(base_name)) # Remove special char
|
||||
create_folder(folder) # Create folder and check if exist
|
||||
@ -94,6 +94,7 @@ class Downloader():
|
||||
logging.error("Invalid mp4 name.")
|
||||
sys.exit(0)
|
||||
|
||||
# Parse to only ascii for win, linux, mac and termux
|
||||
self.output_filename = os.path.join(folder, base_name)
|
||||
self.output_filename = unidecode(self.output_filename)
|
||||
|
||||
@ -134,23 +135,17 @@ class Downloader():
|
||||
str: The text content of the response.
|
||||
"""
|
||||
|
||||
# Send a GET request to the provided URL
|
||||
logging.info(f"Test url: {url}")
|
||||
headers_index = {'user-agent': get_headers()}
|
||||
response = httpx.get(url, headers=headers_index)
|
||||
|
||||
try:
|
||||
|
||||
# Send a GET request to the provided URL
|
||||
logging.info(f"Test url: {url}")
|
||||
headers_index['user-agent'] = get_headers()
|
||||
response = httpx.get(url, headers=headers_index)
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
return response.text
|
||||
|
||||
else:
|
||||
logging.error(f"Test request to {url} failed with status code: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"An unexpected error occurred with test request: {e}")
|
||||
logging.error(f"Test request to {url} failed with error: {e}")
|
||||
return None
|
||||
|
||||
def __manage_playlist__(self, m3u8_playlist_text):
|
||||
@ -179,7 +174,7 @@ class Downloader():
|
||||
|
||||
# Check if there is some audios, else disable download
|
||||
if self.list_available_audio != None:
|
||||
console.print(f"[cyan]Find audios [white]=> [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}")
|
||||
console.print(f"[cyan]Audios [white]=> [red]{[obj_audio.get('language') for obj_audio in self.list_available_audio]}")
|
||||
else:
|
||||
console.log("[red]Cant find a list of audios")
|
||||
|
||||
@ -209,7 +204,7 @@ class Downloader():
|
||||
logging.info(f"M3U8 index select: {self.m3u8_index}, with resolution: {video_res}")
|
||||
|
||||
# Get URI of the best quality and codecs parameters
|
||||
console.print(f"[cyan]Find resolution [white]=> [red]{sorted(list_available_resolution, reverse=True)}")
|
||||
console.print(f"[cyan]Resolutions [white]=> [red]{sorted(list_available_resolution, reverse=True)}")
|
||||
|
||||
# Fix URL if it is not complete with http:\\site_name.domain\...
|
||||
if "http" not in self.m3u8_index:
|
||||
@ -230,7 +225,7 @@ class Downloader():
|
||||
logging.info(f"Find codec: {self.codec}")
|
||||
|
||||
if self.codec is not None:
|
||||
console.print(f"[cyan]Find codec [white]=> ([green]'v'[white]: [yellow]{self.codec.video_codec_name}[white] ([green]b[white]: [yellow]{self.codec.video_bitrate // 1000}k[white]), [green]'a'[white]: [yellow]{self.codec.audio_codec_name}[white] ([green]b[white]: [yellow]{self.codec.audio_bitrate // 1000}k[white]))")
|
||||
console.print(f"[cyan]Codec [white]=> ([green]'v'[white]: [yellow]{self.codec.video_codec_name}[white] ([green]b[white]: [yellow]{self.codec.video_bitrate // 1000}k[white]), [green]'a'[white]: [yellow]{self.codec.audio_codec_name}[white] ([green]b[white]: [yellow]{self.codec.audio_bitrate // 1000}k[white]))")
|
||||
|
||||
|
||||
def __donwload_video__(self):
|
||||
@ -260,6 +255,7 @@ class Downloader():
|
||||
|
||||
# Download the video segments
|
||||
video_m3u8.download_streams(f"{Colors.MAGENTA}video")
|
||||
self.expected_real_time = video_m3u8.expected_real_time
|
||||
|
||||
# Get time of output file
|
||||
print_duration_table(os.path.join(full_path_video, "0.ts"))
|
||||
@ -459,6 +455,10 @@ class Downloader():
|
||||
- out_path (str): Path of the output file.
|
||||
"""
|
||||
|
||||
def dict_to_seconds(d):
|
||||
return d['h'] * 3600 + d['m'] * 60 + d['s']
|
||||
|
||||
|
||||
# Check if file to rename exist
|
||||
logging.info(f"Check if end file converted exist: {out_path}")
|
||||
if out_path is None or not os.path.isfile(out_path):
|
||||
@ -471,12 +471,29 @@ class Downloader():
|
||||
# Rename file converted to original set in init
|
||||
os.rename(out_path, self.output_filename)
|
||||
|
||||
# Print size of the file
|
||||
console.print(Panel(
|
||||
# Get dict with h m s for ouput file name
|
||||
end_output_time = print_duration_table(self.output_filename, description=False, return_string=False)
|
||||
|
||||
# Calculate info for panel
|
||||
formatted_size = format_file_size(os.path.getsize(self.output_filename))
|
||||
formatted_duration = print_duration_table(self.output_filename, description=False, return_string=True)
|
||||
|
||||
expected_real_seconds = dict_to_seconds(self.expected_real_time)
|
||||
end_output_seconds = dict_to_seconds(end_output_time)
|
||||
missing_ts = not (expected_real_seconds - 3 <= end_output_seconds <= expected_real_seconds + 3)
|
||||
|
||||
panel_content = (
|
||||
f"[bold green]Download completed![/bold green]\n"
|
||||
f"File size: [bold red]{format_size(os.path.getsize(self.output_filename))}[/bold red]\n"
|
||||
f"Duration: [bold]{print_duration_table(self.output_filename, show=False)}[/bold]",
|
||||
title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}", border_style="green"))
|
||||
f"[cyan]File size: [bold red]{formatted_size}[/bold red]\n"
|
||||
f"[cyan]Duration: [bold]{formatted_duration}[/bold]\n"
|
||||
f"[cyan]Missing TS: [bold red]{missing_ts}[/bold red]"
|
||||
)
|
||||
|
||||
console.print(Panel(
|
||||
panel_content,
|
||||
title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}",
|
||||
border_style="green"
|
||||
))
|
||||
|
||||
# Delete all files except the output file
|
||||
delete_files_except_one(self.base_path, os.path.basename(self.output_filename))
|
||||
@ -501,8 +518,6 @@ class Downloader():
|
||||
|
||||
if self.m3u8_playlist:
|
||||
logging.info("Download from PLAYLIST")
|
||||
|
||||
|
||||
m3u8_playlist_text = self.__df_make_req__(self.m3u8_playlist)
|
||||
|
||||
# Add full URL of the M3U8 playlist to fix next .ts without https if necessary
|
@ -27,7 +27,7 @@ from Src.Util.call_stack import get_call_stack
|
||||
|
||||
|
||||
# Logic class
|
||||
from ..M3U8 import (
|
||||
from ...M3U8 import (
|
||||
M3U8_Decryption,
|
||||
M3U8_Ts_Estimator,
|
||||
M3U8_Parser,
|
||||
@ -53,7 +53,7 @@ PROXY_START_MAX = config_manager.get_float('REQUESTS', 'proxy_start_max')
|
||||
|
||||
|
||||
# Variable
|
||||
headers_index = config_manager.get_dict('REQUESTS', 'index')
|
||||
headers_index = config_manager.get_dict('REQUESTS', 'user-agent')
|
||||
|
||||
|
||||
|
||||
@ -68,6 +68,7 @@ class M3U8_Segments:
|
||||
"""
|
||||
self.url = url
|
||||
self.tmp_folder = tmp_folder
|
||||
self.expected_real_time = None
|
||||
self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts")
|
||||
os.makedirs(self.tmp_folder, exist_ok=True)
|
||||
|
||||
@ -90,7 +91,7 @@ class M3U8_Segments:
|
||||
Returns:
|
||||
bytes: The encryption key in bytes.
|
||||
"""
|
||||
headers_index['user-agent'] = get_headers()
|
||||
headers_index = {'user-agent': get_headers()}
|
||||
|
||||
# Construct the full URL of the key
|
||||
key_uri = urljoin(self.url, m3u8_parser.keys.get('uri'))
|
||||
@ -123,8 +124,9 @@ class M3U8_Segments:
|
||||
m3u8_parser = M3U8_Parser()
|
||||
m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content)
|
||||
|
||||
console.log(f"[red]Expected duration after download: {m3u8_parser.get_duration()}")
|
||||
console.log(f"[red]There is key: [yellow]{m3u8_parser.keys is not None}")
|
||||
#console.log(f"[red]Expected duration after download: {m3u8_parser.get_duration()}")
|
||||
#console.log(f"[red]There is key: [yellow]{m3u8_parser.keys is not None}")
|
||||
self.expected_real_time = m3u8_parser.get_duration(return_string=False)
|
||||
|
||||
# Check if there is an encryption key in the playlis
|
||||
if m3u8_parser.keys is not None:
|
||||
@ -170,7 +172,7 @@ class M3U8_Segments:
|
||||
"""
|
||||
Makes a request to the index M3U8 file to get information about segments.
|
||||
"""
|
||||
headers_index['user-agent'] = get_headers()
|
||||
headers_index = {'user-agent': get_headers()}
|
||||
|
||||
# Send a GET request to retrieve the index M3U8 file
|
||||
response = httpx.get(self.url, headers=headers_index)
|
||||
@ -206,14 +208,14 @@ class M3U8_Segments:
|
||||
proxy = self.valid_proxy[index % len(self.valid_proxy)]
|
||||
logging.info(f"Use proxy: {proxy}")
|
||||
|
||||
with httpx.Client(transport=httpx.HTTPTransport(retries=3), proxies=proxy, verify=REQUEST_VERIFY) as client:
|
||||
with httpx.Client(proxies=proxy, verify=REQUEST_VERIFY) as client:
|
||||
if 'key_base_url' in self.__dict__:
|
||||
response = client.get(ts_url, headers=random_headers(self.key_base_url), timeout=REQUEST_TIMEOUT)
|
||||
else:
|
||||
response = client.get(ts_url, headers={'user-agent': get_headers()}, timeout=REQUEST_TIMEOUT)
|
||||
else:
|
||||
|
||||
with httpx.Client(transport=httpx.HTTPTransport(retries=3), verify=REQUEST_VERIFY) as client_2:
|
||||
with httpx.Client(verify=REQUEST_VERIFY) as client_2:
|
||||
if 'key_base_url' in self.__dict__:
|
||||
response = client_2.get(ts_url, headers=random_headers(self.key_base_url), timeout=REQUEST_TIMEOUT)
|
||||
else:
|
||||
@ -298,16 +300,28 @@ class M3U8_Segments:
|
||||
|
||||
# Custom bar for mobile and pc
|
||||
if TQDM_USE_LARGE_BAR:
|
||||
bar_format=f"{Colors.YELLOW}Downloading {Colors.WHITE}({add_desc}{Colors.WHITE}): {Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ {Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] {Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
||||
bar_format = (
|
||||
f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{add_desc}{Colors.WHITE}): "
|
||||
f"{Colors.RED}{{percentage:.2f}}% "
|
||||
f"{Colors.MAGENTA}{{bar}} "
|
||||
f"{Colors.WHITE}[ {Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] "
|
||||
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
||||
)
|
||||
else:
|
||||
bar_format=f"{Colors.YELLOW}Proc{Colors.WHITE}: {Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
||||
bar_format = (
|
||||
f"{Colors.YELLOW}Proc{Colors.WHITE}: "
|
||||
f"{Colors.RED}{{percentage:.2f}}% "
|
||||
f"{Colors.WHITE}| "
|
||||
f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
||||
)
|
||||
|
||||
# Create progress bar
|
||||
progress_bar = tqdm(
|
||||
total=len(self.segments),
|
||||
unit='s',
|
||||
ascii='░▒█',
|
||||
bar_format=bar_format
|
||||
bar_format=bar_format,
|
||||
mininterval=0.05
|
||||
)
|
||||
|
||||
# Start a separate thread to write segments to the file
|
3
Src/Lib/Downloader/MP4/__init__.py
Normal file
3
Src/Lib/Downloader/MP4/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# 23.06.24
|
||||
|
||||
from .downloader import MP4_downloader
|
@ -15,11 +15,11 @@ from Src.Util.headers import get_headers
|
||||
from Src.Util.color import Colors
|
||||
from Src.Util.console import console, Panel
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
from Src.Util.os import format_size
|
||||
from Src.Util.os import format_file_size
|
||||
|
||||
|
||||
# Logic class
|
||||
from ..FFmpeg import print_duration_table
|
||||
from ...FFmpeg import print_duration_table
|
||||
|
||||
|
||||
# Config
|
||||
@ -29,19 +29,32 @@ REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
||||
|
||||
|
||||
|
||||
def MP4_downloader(url: str, path: str, referer: str, add_desc: str):
|
||||
def MP4_downloader(url: str, path: str, referer: str):
|
||||
|
||||
"""
|
||||
Downloads an MP4 video from a given URL using the specified referer header.
|
||||
|
||||
Parameter:
|
||||
- url (str): The URL of the MP4 video to download.
|
||||
- path (str): The local path where the downloaded MP4 file will be saved.
|
||||
- referer (str): The referer header value to include in the HTTP request headers.
|
||||
"""
|
||||
|
||||
# Make request to get content of video
|
||||
logging.info(f"Make request to fetch mp4 from: {url}")
|
||||
headers = {'Referer': referer, 'user-agent': get_headers()}
|
||||
|
||||
if referer != None:
|
||||
headers = {'Referer': referer, 'user-agent': get_headers()}
|
||||
else:
|
||||
headers = {'user-agent': get_headers()}
|
||||
|
||||
with httpx.Client(verify=REQUEST_VERIFY, timeout=REQUEST_TIMEOUT) as client:
|
||||
with client.stream("GET", url, headers=headers, timeout=99) as response:
|
||||
with client.stream("GET", url, headers=headers, timeout=10) as response:
|
||||
total = int(response.headers.get('content-length', 0))
|
||||
|
||||
# Create bar format
|
||||
if TQDM_USE_LARGE_BAR:
|
||||
bar_format = (f"{Colors.YELLOW}Downloading {Colors.WHITE}({add_desc}{Colors.WHITE}): "
|
||||
bar_format = (f"{Colors.YELLOW}[MP4] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
||||
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
||||
f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] "
|
||||
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}} {Colors.WHITE}| "
|
||||
@ -53,11 +66,11 @@ def MP4_downloader(url: str, path: str, referer: str, add_desc: str):
|
||||
# Create progress bar
|
||||
progress_bar = tqdm(
|
||||
total=total,
|
||||
unit='iB',
|
||||
ascii='░▒█',
|
||||
bar_format=bar_format,
|
||||
unit_scale=True,
|
||||
unit_divisor=1024
|
||||
unit_divisor=1024,
|
||||
mininterval=0.05
|
||||
)
|
||||
|
||||
# Download file
|
||||
@ -70,8 +83,8 @@ def MP4_downloader(url: str, path: str, referer: str, add_desc: str):
|
||||
# Get summary
|
||||
console.print(Panel(
|
||||
f"[bold green]Download completed![/bold green]\n"
|
||||
f"File size: [bold red]{format_size(os.path.getsize(path))}[/bold red]\n"
|
||||
f"Duration: [bold]{print_duration_table(path, show=False)}[/bold]",
|
||||
f"[cyan]File size: [bold red]{format_file_size(os.path.getsize(path))}[/bold red]\n"
|
||||
f"[cyan]Duration: [bold]{print_duration_table(path, description=False, return_string=True)}[/bold]",
|
||||
title=f"{os.path.basename(path.replace('.mp4', ''))}",
|
||||
border_style="green"
|
||||
))
|
3
Src/Lib/Downloader/TOR/__init__.py
Normal file
3
Src/Lib/Downloader/TOR/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# 23.06.24
|
||||
|
||||
from .downloader import TOR_downloader
|
209
Src/Lib/Downloader/TOR/downloader.py
Normal file
209
Src/Lib/Downloader/TOR/downloader.py
Normal file
@ -0,0 +1,209 @@
|
||||
# 23.06.24
|
||||
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.color import Colors
|
||||
from Src.Util.os import format_file_size, format_transfer_speed
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
|
||||
|
||||
# External libraries
|
||||
from tqdm import tqdm
|
||||
from qbittorrent import Client
|
||||
|
||||
|
||||
# Tor config
|
||||
HOST = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['host'])
|
||||
PORT = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['port'])
|
||||
USERNAME = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['user'])
|
||||
PASSWORD = str(config_manager.get_dict('DEFAULT', 'config_qbit_tor')['pass'])
|
||||
|
||||
# Config
|
||||
TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar')
|
||||
REQUEST_VERIFY = config_manager.get_float('REQUESTS', 'verify_ssl')
|
||||
REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout')
|
||||
|
||||
|
||||
|
||||
|
||||
class TOR_downloader:
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the TorrentManager instance.
|
||||
|
||||
Parameters:
|
||||
- host (str): IP address or hostname of the qBittorrent Web UI.
|
||||
- port (int): Port number of the qBittorrent Web UI.
|
||||
- username (str): Username for logging into qBittorrent.
|
||||
- password (str): Password for logging into qBittorrent.
|
||||
"""
|
||||
self.qb = Client(f'http://{HOST}:{PORT}/')
|
||||
self.username = USERNAME
|
||||
self.password = PASSWORD
|
||||
self.logged_in = False
|
||||
self.save_path = None
|
||||
self.torrent_name = None
|
||||
|
||||
self.login()
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Logs into the qBittorrent Web UI.
|
||||
"""
|
||||
try:
|
||||
self.qb.login(self.username, self.password)
|
||||
self.logged_in = True
|
||||
logging.info("Successfully logged in to qBittorrent.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to log in: {str(e)}")
|
||||
self.logged_in = False
|
||||
|
||||
def add_magnet_link(self, magnet_link):
|
||||
"""
|
||||
Adds a torrent via magnet link to qBittorrent.
|
||||
|
||||
Parameters:
|
||||
- magnet_link (str): Magnet link of the torrent to be added.
|
||||
"""
|
||||
try:
|
||||
self.qb.download_from_link(magnet_link)
|
||||
logging.info("Added magnet link to qBittorrent.")
|
||||
|
||||
# Get the hash of the latest added torrent
|
||||
torrents = self.qb.torrents()
|
||||
if torrents:
|
||||
self.latest_torrent_hash = torrents[-1]['hash']
|
||||
logging.info(f"Latest torrent hash: {self.latest_torrent_hash}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to add magnet link: {str(e)}")
|
||||
|
||||
|
||||
def start_download(self):
|
||||
"""
|
||||
Starts downloading the latest added torrent and monitors progress.
|
||||
"""
|
||||
try:
|
||||
|
||||
torrents = self.qb.torrents()
|
||||
if not torrents:
|
||||
logging.error("No torrents found.")
|
||||
return
|
||||
|
||||
# Sleep to load magnet to qbit app
|
||||
time.sleep(5)
|
||||
latest_torrent = torrents[-1]
|
||||
torrent_hash = latest_torrent['hash']
|
||||
|
||||
# Custom bar for mobile and pc
|
||||
if TQDM_USE_LARGE_BAR:
|
||||
bar_format = (
|
||||
f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): "
|
||||
f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
|
||||
f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
||||
)
|
||||
else:
|
||||
bar_format = (
|
||||
f"{Colors.YELLOW}Proc{Colors.WHITE}: "
|
||||
f"{Colors.RED}{{percentage:.2f}}% {Colors.WHITE}| "
|
||||
f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
|
||||
)
|
||||
|
||||
progress_bar = tqdm(
|
||||
total=100,
|
||||
ascii='░▒█',
|
||||
bar_format=bar_format,
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
mininterval=0.05
|
||||
)
|
||||
|
||||
with progress_bar as pbar:
|
||||
while True:
|
||||
|
||||
# Get variable from qtorrent
|
||||
torrent_info = self.qb.get_torrent(torrent_hash)
|
||||
self.save_path = torrent_info['save_path']
|
||||
self.torrent_name = torrent_info['name']
|
||||
|
||||
# Fetch important variable
|
||||
pieces_have = torrent_info['pieces_have']
|
||||
pieces_num = torrent_info['pieces_num']
|
||||
progress = (pieces_have / pieces_num) * 100 if pieces_num else 0
|
||||
pbar.n = progress
|
||||
|
||||
download_speed = torrent_info['dl_speed']
|
||||
total_size = torrent_info['total_size']
|
||||
downloaded_size = torrent_info['total_downloaded']
|
||||
|
||||
# Format variable
|
||||
downloaded_size_str = format_file_size(downloaded_size)
|
||||
downloaded_size = downloaded_size_str.split(' ')[0]
|
||||
|
||||
total_size_str = format_file_size(total_size)
|
||||
total_size = total_size_str.split(' ')[0]
|
||||
total_size_unit = total_size_str.split(' ')[1]
|
||||
|
||||
average_internet_str = format_transfer_speed(download_speed)
|
||||
average_internet = average_internet_str.split(' ')[0]
|
||||
average_internet_unit = average_internet_str.split(' ')[1]
|
||||
|
||||
# Update the progress bar's postfix
|
||||
if TQDM_USE_LARGE_BAR:
|
||||
pbar.set_postfix_str(
|
||||
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size} {Colors.WHITE}< {Colors.GREEN}{total_size} {Colors.RED}{total_size_unit} "
|
||||
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
||||
)
|
||||
else:
|
||||
pbar.set_postfix_str(
|
||||
f"{Colors.WHITE}[ {Colors.GREEN}{downloaded_size}{Colors.RED} {total_size} "
|
||||
f"{Colors.WHITE}| {Colors.CYAN}{average_internet} {Colors.RED}{average_internet_unit}"
|
||||
)
|
||||
|
||||
pbar.refresh()
|
||||
time.sleep(0.2)
|
||||
|
||||
# Break at the end
|
||||
if int(progress) == 100:
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Download process interrupted.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Download error: {str(e)}")
|
||||
|
||||
def move_downloaded_files(self, destination=None):
|
||||
"""
|
||||
Moves downloaded files of the latest torrent to another location.
|
||||
|
||||
Parameters:
|
||||
- save_path (str): Current save path (output directory) of the torrent.
|
||||
- destination (str, optional): Destination directory to move files. If None, moves to current directory.
|
||||
|
||||
Returns:
|
||||
- bool: True if files are moved successfully, False otherwise.
|
||||
"""
|
||||
|
||||
# List directories in the save path
|
||||
dirs = [d for d in os.listdir(self.save_path) if os.path.isdir(os.path.join(self.save_path, d))]
|
||||
|
||||
for dir_name in dirs:
|
||||
if dir_name in self.torrent_name :
|
||||
dir_path = os.path.join(self.save_path, dir_name)
|
||||
if destination:
|
||||
destination_path = os.path.join(destination, dir_name)
|
||||
else:
|
||||
destination_path = os.path.join(os.getcwd(), dir_name)
|
||||
|
||||
shutil.move(dir_path, destination_path)
|
||||
logging.info(f"Moved directory {dir_name} to {destination_path}")
|
||||
break
|
||||
|
||||
return True
|
5
Src/Lib/Downloader/__init__.py
Normal file
5
Src/Lib/Downloader/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# 23.06.24
|
||||
|
||||
from .HLS import HLS_Downloader
|
||||
from .MP4 import MP4_downloader
|
||||
from .TOR import TOR_downloader
|
@ -1,4 +0,0 @@
|
||||
# 20.05.24
|
||||
|
||||
from .sql_table import SimpleDBManager, report_table
|
||||
report_table: SimpleDBManager = report_table
|
@ -1,212 +0,0 @@
|
||||
# 20.05.24
|
||||
|
||||
import csv
|
||||
import logging
|
||||
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
|
||||
|
||||
# Variable
|
||||
CREATE_REPORT = config_manager.get_bool('M3U8_DOWNLOAD', 'create_report')
|
||||
|
||||
|
||||
class SimpleDBManager:
|
||||
def __init__(self, filename, columns):
|
||||
"""
|
||||
Initialize a new database manager.
|
||||
|
||||
Args:
|
||||
- filename (str): The name of the CSV file containing the database.
|
||||
- columns (list): List of database columns.
|
||||
"""
|
||||
self.filename = filename
|
||||
self.db = []
|
||||
self.columns = columns
|
||||
logging.info("Database manager initialized.")
|
||||
|
||||
def load_database(self):
|
||||
"""
|
||||
Load the database from the specified CSV file.
|
||||
If the file doesn't exist, initialize a new database.
|
||||
"""
|
||||
try:
|
||||
with open(self.filename, 'r', newline='') as file:
|
||||
reader = csv.reader(file)
|
||||
self.db = list(reader)
|
||||
logging.info(f"Database {self.filename} loaded successfully.")
|
||||
|
||||
except FileNotFoundError:
|
||||
logging.warning(f"File {self.filename} not found, creating a new database...")
|
||||
self.initialize_database()
|
||||
|
||||
def initialize_database(self, columns=None, rows=None):
|
||||
"""
|
||||
Initialize a new database with specified columns and rows.
|
||||
|
||||
Args:
|
||||
- columns (list, optional): List of database columns. If not specified, uses the columns provided in the constructor.
|
||||
- rows (list, optional): List of database rows. Each row should be a list of values. If not specified, the database will be empty.
|
||||
"""
|
||||
self.db = [self.columns]
|
||||
if rows:
|
||||
for row_data in rows:
|
||||
self.add_row_to_database(row_data)
|
||||
|
||||
logging.info("Database initialized successfully.")
|
||||
|
||||
def add_row_to_database(self, *row_data):
|
||||
"""
|
||||
Add a new row to the database.
|
||||
|
||||
Args:
|
||||
- row_data (list): List of values for the new row.
|
||||
"""
|
||||
self.db.append(list(row_data))
|
||||
logging.info("New row added to the database.")
|
||||
|
||||
def add_column_to_database(self, column_name, default_value=''):
|
||||
"""
|
||||
Add a new column to the database.
|
||||
|
||||
Args:
|
||||
- column_name (str): Name of the new column.
|
||||
- default_value (str, optional): Default value to be inserted in cells of the new column. Default is an empty string.
|
||||
"""
|
||||
for row in self.db:
|
||||
row.append(default_value)
|
||||
self.db[0][-1] = column_name
|
||||
logging.info(f"New column '{column_name}' added to the database.")
|
||||
|
||||
def update_row_in_database(self, row_index, new_row_data):
|
||||
"""
|
||||
Update an existing row in the database.
|
||||
|
||||
Args:
|
||||
- row_index (int): Index of the row to update.
|
||||
- new_row_data (list): List of the new values for the updated row.
|
||||
"""
|
||||
self.db[row_index] = new_row_data
|
||||
logging.info(f"Row {row_index} of the database updated.")
|
||||
|
||||
def remove_row_from_database(self, column_index: int, search_value) -> list:
|
||||
"""
|
||||
Remove a row from the database based on a specific column value.
|
||||
|
||||
Args:
|
||||
- column_index (int): Index of the column to search in.
|
||||
- search_value: The value to search for in the specified column.
|
||||
|
||||
Returns:
|
||||
list: The removed row from the database, if found; otherwise, an empty list.
|
||||
"""
|
||||
|
||||
# Find the index of the row with the specified value in the specified column
|
||||
row_index = None
|
||||
for i, row in enumerate(self.db):
|
||||
if row[column_index] == search_value:
|
||||
row_index = i
|
||||
break
|
||||
|
||||
# If the row with the specified value is found, remove it
|
||||
remove_row = []
|
||||
if row_index is not None:
|
||||
remove_row = self.db[row_index]
|
||||
del self.db[row_index]
|
||||
logging.info(f"Row at index {row_index} with value {search_value} in column {column_index} removed from the database.")
|
||||
else:
|
||||
logging.warning(f"No row found with value {search_value} in column {column_index}. Nothing was removed from the database.")
|
||||
|
||||
return remove_row
|
||||
|
||||
def remove_column_from_database(self, col_index):
|
||||
"""
|
||||
Remove a column from the database.
|
||||
|
||||
Args:
|
||||
- col_index (int): Index of the column to remove.
|
||||
"""
|
||||
for row in self.db:
|
||||
del row[col_index]
|
||||
logging.info(f"Column {col_index} of the database removed.")
|
||||
|
||||
def save_database(self):
|
||||
"""
|
||||
Save the database to the CSV file specified in the constructor.
|
||||
"""
|
||||
with open(self.filename, 'w', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(self.db)
|
||||
logging.info("Database saved to file.")
|
||||
|
||||
def print_database_as_sql(self):
|
||||
"""
|
||||
Print the database in SQL format to the console.
|
||||
"""
|
||||
max_lengths = [max(len(str(cell)) for cell in col) for col in zip(*self.db)]
|
||||
line = "+-" + "-+-".join("-" * length for length in max_lengths) + "-+"
|
||||
print(line)
|
||||
print("| " + " | ".join(f"{cell:<{length}}" for cell, length in zip(self.db[0], max_lengths)) + " |")
|
||||
print(line)
|
||||
for row in self.db[1:]:
|
||||
print("| " + " | ".join(f"{cell:<{length}}" for cell, length in zip(row, max_lengths)) + " |")
|
||||
print(line)
|
||||
logging.info("Database printed as SQL.")
|
||||
|
||||
def search_database(self, column_index, value):
|
||||
"""
|
||||
Search the database for rows matching the specified value in the given column.
|
||||
|
||||
Args:
|
||||
- column_index (int): Index of the column to search on.
|
||||
- value (str): Value to search for.
|
||||
|
||||
Returns:
|
||||
list: List of rows matching the search.
|
||||
"""
|
||||
results = []
|
||||
for row in self.db[1:]:
|
||||
if row[column_index] == value:
|
||||
results.append(row)
|
||||
logging.info(f"Database searched for value '{value}' in column {column_index}. Found {len(results)} matches.")
|
||||
return results
|
||||
|
||||
def sort_database(self, column_index):
|
||||
"""
|
||||
Sort the database based on values in the specified column.
|
||||
|
||||
Args:
|
||||
- column_index (int): Index of the column to sort on.
|
||||
"""
|
||||
self.db[1:] = sorted(self.db[1:], key=lambda x: x[column_index])
|
||||
logging.info(f"Database sorted based on column {column_index}.")
|
||||
|
||||
def filter_database(self, column_index, condition):
|
||||
"""
|
||||
Filter the database based on a condition on the specified column.
|
||||
|
||||
Args:
|
||||
- column_index (int): Index of the column to apply the condition on.
|
||||
- condition (function): Condition function to apply on the values of the column. Should return True or False.
|
||||
|
||||
Returns:
|
||||
list: List of rows satisfying the condition.
|
||||
"""
|
||||
results = [self.db[0]] # Keep the header row
|
||||
for row in self.db[1:]:
|
||||
if condition(row[column_index]):
|
||||
results.append(row)
|
||||
logging.info(f"Filter applied on column {column_index}. Found {len(results) - 1} rows satisfying the condition.")
|
||||
return results
|
||||
|
||||
|
||||
|
||||
|
||||
# Output
|
||||
if CREATE_REPORT:
|
||||
report_table = SimpleDBManager("riepilogo.csv", ["Date", "Name", "Size"])
|
||||
report_table.load_database()
|
||||
report_table.save_database()
|
||||
else:
|
||||
report_table = None
|
@ -12,7 +12,7 @@ from typing import Tuple
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.console import console
|
||||
from Src.Util.os import format_size
|
||||
from Src.Util.os import format_file_size
|
||||
|
||||
|
||||
# Variable
|
||||
@ -61,12 +61,11 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
||||
else:
|
||||
byte_size = int(re.findall(r'\d+', data.get('size', '0'))[0]) * 1000
|
||||
|
||||
time_now = datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
# Construct the progress string with formatted output information
|
||||
progress_string = (f"[blue][{time_now}][purple] FFmpeg [white][{description}[white]]: "
|
||||
progress_string = (f"[yellow][FFmpeg] [white][{description}[white]]: "
|
||||
f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
|
||||
f"[green]'size': [yellow]{format_size(byte_size)}[white])")
|
||||
f"[green]'size': [yellow]{format_file_size(byte_size)}[white])")
|
||||
max_length = max(max_length, len(progress_string))
|
||||
|
||||
# Print the progress string to the console, overwriting the previous line
|
||||
|
@ -92,22 +92,44 @@ def format_duration(seconds: float) -> Tuple[int, int, int]:
|
||||
return int(hours), int(minutes), int(seconds)
|
||||
|
||||
|
||||
def print_duration_table(file_path: str, show = True) -> None:
|
||||
def print_duration_table(file_path: str, description: str = "Duration", return_string: bool = False):
|
||||
"""
|
||||
Print duration of a video file in hours, minutes, and seconds.
|
||||
Print the duration of a video file in hours, minutes, and seconds, or return it as a formatted string.
|
||||
|
||||
Args:
|
||||
- file_path (str): The path to the video file.
|
||||
- description (str): Optional description to be included in the output. Defaults to "Duration". If not provided, the duration will not be printed.
|
||||
- return_string (bool): If True, returns the formatted duration string. If False, returns a dictionary with hours, minutes, and seconds.
|
||||
|
||||
Returns:
|
||||
- str: The formatted duration string if return_string is True.
|
||||
- dict: A dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds if return_string is False.
|
||||
|
||||
Example usage:
|
||||
>>> print_duration_table("path/to/video.mp4")
|
||||
[cyan]Duration for [white]([green]video.mp4[white]): [yellow]1[red]h [yellow]1[red]m [yellow]1[red]s
|
||||
|
||||
>>> print_duration_table("path/to/video.mp4", description=None)
|
||||
'[yellow]1[red]h [yellow]1[red]m [yellow]1[red]s'
|
||||
|
||||
>>> print_duration_table("path/to/video.mp4", description=None, return_string=False)
|
||||
{'h': 1, 'm': 1, 's': 1}
|
||||
"""
|
||||
|
||||
video_duration = get_video_duration(file_path)
|
||||
|
||||
if video_duration is not None:
|
||||
hours, minutes, seconds = format_duration(video_duration)
|
||||
if show:
|
||||
console.print(f"[cyan]Duration for [white]([green]{os.path.basename(file_path)}[white]): [yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s")
|
||||
formatted_duration = f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s"
|
||||
duration_dict = {'h': hours, 'm': minutes, 's': seconds}
|
||||
|
||||
if description:
|
||||
console.print(f"[cyan]{description} for [white]([green]{os.path.basename(file_path)}[white]): {formatted_duration}")
|
||||
else:
|
||||
return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s"
|
||||
if return_string:
|
||||
return formatted_duration
|
||||
else:
|
||||
return duration_dict
|
||||
|
||||
|
||||
def get_ffprobe_info(file_path):
|
||||
|
@ -1,3 +0,0 @@
|
||||
# 20.02.24
|
||||
|
||||
from .downloader import Downloader
|
@ -14,7 +14,7 @@ from tqdm import tqdm
|
||||
|
||||
# Internal utilities
|
||||
from Src.Util.color import Colors
|
||||
from Src.Util.os import format_size
|
||||
from Src.Util.os import format_file_size, format_transfer_speed
|
||||
from Src.Util._jsonConfig import config_manager
|
||||
|
||||
|
||||
@ -64,15 +64,6 @@ class M3U8_Ts_Estimator:
|
||||
io_counters = psutil.net_io_counters()
|
||||
return io_counters
|
||||
|
||||
def format_bytes(bytes):
|
||||
if bytes < 1024:
|
||||
return f"{bytes:.2f} Bytes/s"
|
||||
elif bytes < 1024 * 1024:
|
||||
return f"{bytes / 1024:.2f} KB/s"
|
||||
else:
|
||||
return f"{bytes / (1024 * 1024):.2f} MB/s"
|
||||
|
||||
|
||||
# Get proc id
|
||||
pid = os.getpid()
|
||||
|
||||
@ -88,8 +79,8 @@ class M3U8_Ts_Estimator:
|
||||
download_speed = (new_value.bytes_recv - old_value.bytes_recv) / interval
|
||||
|
||||
self.speed = ({
|
||||
"upload": format_bytes(upload_speed),
|
||||
"download": format_bytes(download_speed)
|
||||
"upload": format_transfer_speed(upload_speed),
|
||||
"download": format_transfer_speed(download_speed)
|
||||
})
|
||||
|
||||
old_value = new_value
|
||||
@ -103,7 +94,7 @@ class M3U8_Ts_Estimator:
|
||||
float: The average internet speed in Mbps.
|
||||
"""
|
||||
with self.lock:
|
||||
return self.speed['download'].split(" ")
|
||||
return self.speed['download'].split(" ")
|
||||
|
||||
def calculate_total_size(self) -> str:
|
||||
"""
|
||||
@ -120,7 +111,7 @@ class M3U8_Ts_Estimator:
|
||||
mean_size = total_size / len(self.ts_file_sizes)
|
||||
|
||||
# Return formatted mean size
|
||||
return format_size(mean_size)
|
||||
return format_file_size(mean_size)
|
||||
|
||||
except ZeroDivisionError as e:
|
||||
logging.error("Division by zero error occurred: %s", e)
|
||||
@ -137,7 +128,7 @@ class M3U8_Ts_Estimator:
|
||||
Returns:
|
||||
str: The total downloaded size as a human-readable string.
|
||||
"""
|
||||
return format_size(self.now_downloaded_size)
|
||||
return format_file_size(self.now_downloaded_size)
|
||||
|
||||
def update_progress_bar(self, total_downloaded: int, duration: float, progress_counter: tqdm) -> None:
|
||||
"""
|
||||
|
@ -584,20 +584,34 @@ class M3U8_Parser:
|
||||
self._audio = M3U8_Audio(self.audio_playlist)
|
||||
self._subtitle = M3U8_Subtitle(self.subtitle_playlist)
|
||||
|
||||
def get_duration(self):
|
||||
def get_duration(self, return_string:bool = True):
|
||||
"""
|
||||
Convert duration from seconds to hours, minutes, and remaining seconds.
|
||||
|
||||
Parameters:
|
||||
- seconds (float): Duration in seconds.
|
||||
- return_string (bool): If True, returns the formatted duration string.
|
||||
If False, returns a dictionary with hours, minutes, and seconds.
|
||||
|
||||
Returns:
|
||||
- formatted_duration (str): Formatted duration string with hours, minutes, and seconds.
|
||||
- formatted_duration (str): Formatted duration string with hours, minutes, and seconds if return_string is True.
|
||||
- duration_dict (dict): Dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds respectively if return_string is False.
|
||||
|
||||
Example usage:
|
||||
>>> obj = YourClass(duration=3661)
|
||||
>>> obj.get_duration()
|
||||
'[yellow]1[red]h [yellow]1[red]m [yellow]1[red]s'
|
||||
>>> obj.get_duration(return_string=False)
|
||||
{'h': 1, 'm': 1, 's': 1}
|
||||
"""
|
||||
|
||||
# Calculate hours, minutes, and remaining seconds
|
||||
hours = int(self.duration / 3600)
|
||||
minutes = int((self.duration % 3600) / 60)
|
||||
remaining_seconds = int(self.duration % 60)
|
||||
hours, remainder = divmod(self.duration, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
|
||||
|
||||
# Format the duration string with colors
|
||||
return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(remaining_seconds)}[red]s"
|
||||
if return_string:
|
||||
return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s"
|
||||
else:
|
||||
return {'h': int(hours), 'm': int(minutes), 's': int(seconds)}
|
||||
|
105
Src/Util/os.py
105
Src/Util/os.py
@ -23,11 +23,13 @@ from typing import List
|
||||
|
||||
|
||||
# External library
|
||||
import httpx
|
||||
import unicodedata
|
||||
|
||||
|
||||
# Internal utilities
|
||||
from .console import console
|
||||
from .headers import get_headers
|
||||
|
||||
|
||||
|
||||
@ -316,32 +318,45 @@ def clean_json(path: str) -> None:
|
||||
|
||||
|
||||
|
||||
# --> OS MANAGE SIZE FILE
|
||||
def format_size(size_bytes: float) -> str:
|
||||
# --> OS MANAGE SIZE FILE AND INTERNET SPEED
|
||||
def format_file_size(size_bytes: float) -> str:
|
||||
"""
|
||||
Format the size in bytes into a human-readable format.
|
||||
Formats a file size from bytes into a human-readable string representation.
|
||||
|
||||
Args:
|
||||
- size_bytes (float): The size in bytes to be formatted.
|
||||
size_bytes (float): Size in bytes to be formatted.
|
||||
|
||||
Returns:
|
||||
str: The formatted size.
|
||||
str: Formatted string representing the file size with appropriate unit (B, KB, MB, GB, TB).
|
||||
"""
|
||||
|
||||
if size_bytes <= 0:
|
||||
return "0B"
|
||||
|
||||
units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
unit_index = 0
|
||||
|
||||
# Convert bytes to appropriate unit
|
||||
while size_bytes >= 1024 and unit_index < len(units) - 1:
|
||||
size_bytes /= 1024
|
||||
unit_index += 1
|
||||
|
||||
# Round the size to two decimal places and return with the appropriate unit
|
||||
return f"{size_bytes:.2f} {units[unit_index]}"
|
||||
|
||||
def format_transfer_speed(bytes: float) -> str:
|
||||
"""
|
||||
Formats a transfer speed from bytes per second into a human-readable string representation.
|
||||
|
||||
Args:
|
||||
bytes (float): Speed in bytes per second to be formatted.
|
||||
|
||||
Returns:
|
||||
str: Formatted string representing the transfer speed with appropriate unit (Bytes/s, KB/s, MB/s).
|
||||
"""
|
||||
if bytes < 1024:
|
||||
return f"{bytes:.2f} Bytes/s"
|
||||
elif bytes < 1024 * 1024:
|
||||
return f"{bytes / 1024:.2f} KB/s"
|
||||
else:
|
||||
return f"{bytes / (1024 * 1024):.2f} MB/s"
|
||||
|
||||
|
||||
|
||||
@ -452,7 +467,6 @@ def get_system_summary():
|
||||
|
||||
console.print(f"[cyan]Python[white]: [bold red]{python_version} ({python_implementation} {arch}) - {os_info} ({openssl_version}, {glibc_version})[/bold red]")
|
||||
logging.info(f"Python: {python_version} ({python_implementation} {arch}) - {os_info} ({openssl_version}, {glibc_version})")
|
||||
|
||||
|
||||
# ffmpeg and ffprobe versions
|
||||
ffmpeg_version = get_executable_version(['ffmpeg', '-version'])
|
||||
@ -524,6 +538,79 @@ def run_node_script(script_content: str) -> str:
|
||||
import os
|
||||
os.remove('script.js')
|
||||
|
||||
def run_node_script_api(script_content: str) -> str:
|
||||
"""
|
||||
Runs a Node.js script and returns its output.
|
||||
|
||||
Args:
|
||||
script_content (str): The content of the Node.js script to run.
|
||||
|
||||
Returns:
|
||||
str: The output of the Node.js script.
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'accept': '*/*',
|
||||
'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'dnt': '1',
|
||||
'origin': 'https://onecompiler.com',
|
||||
'priority': 'u=1, i',
|
||||
'referer': 'https://onecompiler.com/javascript',
|
||||
'user-agent': get_headers()
|
||||
}
|
||||
|
||||
json_data = {
|
||||
'name': 'JavaScript',
|
||||
'title': '42gyum6qn',
|
||||
'version': 'ES6',
|
||||
'mode': 'javascript',
|
||||
'description': None,
|
||||
'extension': 'js',
|
||||
'languageType': 'programming',
|
||||
'active': False,
|
||||
'properties': {
|
||||
'language': 'javascript',
|
||||
'docs': False,
|
||||
'tutorials': False,
|
||||
'cheatsheets': False,
|
||||
'filesEditable': False,
|
||||
'filesDeletable': False,
|
||||
'files': [
|
||||
{
|
||||
'name': 'index.js',
|
||||
'content': script_content
|
||||
},
|
||||
],
|
||||
'newFileOptions': [
|
||||
{
|
||||
'helpText': 'New JS file',
|
||||
'name': 'script${i}.js',
|
||||
'content': "/**\n * In main file\n * let script${i} = require('./script${i}');\n * console.log(script${i}.sum(1, 2));\n */\n\nfunction sum(a, b) {\n return a + b;\n}\n\nmodule.exports = { sum };",
|
||||
},
|
||||
{
|
||||
'helpText': 'Add Dependencies',
|
||||
'name': 'package.json',
|
||||
'content': '{\n "name": "main_app",\n "version": "1.0.0",\n "description": "",\n "main": "HelloWorld.js",\n "dependencies": {\n "lodash": "^4.17.21"\n }\n}',
|
||||
},
|
||||
],
|
||||
},
|
||||
'_id': '42gcvpkbg_42gyuud7m',
|
||||
'user': None,
|
||||
'visibility': 'public',
|
||||
}
|
||||
|
||||
# Return error
|
||||
response = httpx.post('https://onecompiler.com/api/code/exec', headers=headers, json=json_data)
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code == 200:
|
||||
return str(response.json()['stderr']).split("\n")[1]
|
||||
|
||||
else:
|
||||
logging.error("Cant connect to site: onecompiler.com")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
|
||||
# --> OS FILE VALIDATOR
|
||||
|
@ -1,8 +1,14 @@
|
||||
import tkinter as tk
|
||||
from threading import Thread, Lock
|
||||
from collections import deque
|
||||
import psutil
|
||||
# 23.06.24
|
||||
|
||||
import time
|
||||
from collections import deque
|
||||
from threading import Thread, Lock
|
||||
|
||||
|
||||
# External library
|
||||
import psutil
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
class NetworkMonitor:
|
||||
def __init__(self, maxlen=10):
|
||||
@ -38,6 +44,7 @@ class NetworkMonitor:
|
||||
|
||||
old_value = new_value
|
||||
|
||||
|
||||
class NetworkMonitorApp:
|
||||
def __init__(self, root):
|
||||
self.monitor = NetworkMonitor()
|
||||
@ -77,7 +84,8 @@ class NetworkMonitorApp:
|
||||
self.monitor_thread = Thread(target=self.monitor.capture_speed, args=(0.5,), daemon=True)
|
||||
self.monitor_thread.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = NetworkMonitorApp(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
root = tk.Tk()
|
||||
app = NetworkMonitorApp(root)
|
||||
root.mainloop()
|
||||
|
9
Test/t_down_hls.py
Normal file
9
Test/t_down_hls.py
Normal file
@ -0,0 +1,9 @@
|
||||
# 23.06.24
|
||||
|
||||
from Src.Lib.Downloader import HLS_Downloader
|
||||
|
||||
|
||||
HLS_Downloader(
|
||||
output_filename="EP_1.mp4",
|
||||
m3u8_playlist=""
|
||||
)
|
8
Test/t_down_mp4.py
Normal file
8
Test/t_down_mp4.py
Normal file
@ -0,0 +1,8 @@
|
||||
# 23.06.24
|
||||
|
||||
from Src.Lib.Downloader import MP4_downloader
|
||||
|
||||
MP4_downloader(
|
||||
"",
|
||||
"EP_1.mp4"
|
||||
)
|
10
Test/t_down_tor.py
Normal file
10
Test/t_down_tor.py
Normal file
@ -0,0 +1,10 @@
|
||||
# 23.06.24
|
||||
|
||||
from Src.Lib.Downloader import TOR_downloader
|
||||
|
||||
manager = TOR_downloader()
|
||||
|
||||
magnet_link = "magnet:?x"
|
||||
manager.add_magnet_link(magnet_link)
|
||||
manager.start_download()
|
||||
manager.move_downloaded_files()
|
17
config.json
17
config.json
@ -8,15 +8,19 @@
|
||||
"root_path": "Video",
|
||||
"map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)",
|
||||
"auto_update_domain": true,
|
||||
"config_qbit_tor": {
|
||||
"host": "192.168.1.1",
|
||||
"port": "8080",
|
||||
"user": "admin",
|
||||
"pass": "admin"
|
||||
},
|
||||
"not_close": false
|
||||
},
|
||||
"REQUESTS": {
|
||||
"timeout": 10,
|
||||
"max_retry": 3,
|
||||
"verify_ssl": false,
|
||||
"index": {
|
||||
"user-agent": ""
|
||||
},
|
||||
"user-agent": "",
|
||||
"proxy_start_min": 0.1,
|
||||
"proxy_start_max": 0.5,
|
||||
"proxy": []
|
||||
@ -75,7 +79,12 @@
|
||||
"ddlstreamitaly": {
|
||||
"video_workers": -1,
|
||||
"audio_workers": -1,
|
||||
"domain": "co"
|
||||
"domain": "co",
|
||||
"cookie": {
|
||||
"ips4_device_key": "",
|
||||
"ips4_member_id": "",
|
||||
"ips4_login_key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,4 +7,5 @@ tqdm
|
||||
m3u8
|
||||
psutil
|
||||
unidecode
|
||||
fake-useragent
|
||||
fake-useragent
|
||||
qbittorrent-api
|
Loading…
x
Reference in New Issue
Block a user