Un giorno ...

This commit is contained in:
Ghost 2024-04-01 10:10:54 +02:00
parent d82133f381
commit cd3001f6b2
33 changed files with 3342 additions and 1148 deletions

137
.gitignore vendored
View File

@ -1,13 +1,128 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Folder
bin
lib64
*__pycache__
pyvenv.cfg
.idea
# C extensions
*.so
# Project specific
videos/
tmp/
debug.log
run.exe
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Other
Video

131
README.md
View File

@ -1,94 +1,153 @@
<p align="center">
<img src="Src/Assets/min_logo.png" style="max-width: 55%;" alt="video working" />
<p align="center" >
<img src="./Src/Assets/min_logo.png" title="SDWebImage logo" float=left>
</p>
## Overview.
# Overview.
This repository provide a simple script designed to facilitate the downloading of films and series from a popular streaming community platform. The script allows users to download individual films, entire series, or specific episodes, providing a seamless experience for content consumers.
## Join us
You can chat, help improve this repo, or just hang around for some fun in the **Git_StreamingCommunity** Discord [Server](https://discord.gg/c3JSUM5Hqw)
You can chat, help improve this repo, or just hang around for some fun in the **Git_StreamingCommunity** Discord [Server](https://discord.gg/by8UsqhPWx)
# Table of Contents
* [INSTALLATION](#installation)
* [Requirement](#requirement)
* [Usage](#usage)
* [Update](#update)
* [FEATURES](#features)
* [INSTALLATION](#installation)
* [Requirement](#requirement)
* [Usage](#usage)
* [Update](#update)
* [USAGE AND OPTIONS](#options)
* [TUTORIAL](#tutorial)
## Requirement
Make sure you have the following prerequisites installed on your system:
* python > [3.11](https://www.python.org/downloads/)
* ffmpeg [win](https://www.gyan.dev/ffmpeg/builds/)
## Installation
Install the required Python libraries using the following command:
```
pip install -r requirements.txt
```
## Usage
Run the script with the following command:
#### On Windows:
```powershell
python run.py
```
#### On Linux/MacOS:
```bash
python3 run.py
```
## Update
Keep your script up to date with the latest features by running:
#### On Windows:
```powershell
python update.py
```
#### On Linux/MacOS:
```bash
python3 update.py
```
## Features
- Download Single Film: Easily download individual movies with a simple command.
- Download Specific Episodes or Entire Series: Seamlessly retrieve specific episodes or entire series using intuitive commands. Specify a range of episodes with square brackets notation, e.g., [5-7], or download all episodes with an asterisk (*).
- Download Subtitles: Automatically fetch subtitles if available for downloaded content. (Note: To disable this feature, see [Configuration](#configuration))
- Sync Audio and Video: Ensure perfect synchronization between audio and video during the download process for an enhanced viewing experience.
## Configuration
You can change some behaviors by tweaking the configuration file.
```json
{
"root_path": "videos",
"movies_folder_name": "Movies",
"series_folder_name": "Series",
"download_subtitles": true,
"download_default_language": true,
"selected_language": "English",
"max_worker": 20
"DEFAULT": {
"debug": false,
"get_info": false,
"show_message": true,
"clean_console": true,
"get_moment_title": false,
"root_path": "videos",
"movies_folder_name": "Movies",
"series_folder_name": "Series",
"anime_folder_name": "Anime",
"not_close": false,
"swith_anime": false
},
"SITE": {
"streaming_site_name": "streamingcommunity",
"streaming_domain": "forum",
"anime_site_name": "animeunity",
"anime_domain": "to"
},
"M3U8": {
"tdqm_workers": 20,
"tqdm_progress_timeout": 10,
"minium_ts_files_in_folder": 15,
"donwload_percentage": 1,
"requests_timeout": 5,
"enable_time_quit": false,
"tqdm_show_progress": false,
"cleanup_tmp_folder": true
},
"M3U8_OPTIONS": {
"download_audio": true,
"download_subtitles": true,
"specific_list_audio": [
"ita"
],
"specific_list_subtitles": [
"eng"
],
}
}
```
#### Options
| Key | Default Value | Description | Value Example |
|---------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------|--------------------------|
| root_path | videos | Path where the script will add movies and tv series folders (see [Path Examples](#Path-examples)). Do not put trailing slash. | media/streamingcommunity |
| movies_folder_name | Movies | The folder name where all the movies will be placed. Do not put trailing slash. | downloaded-movies |
| series_folder_name | Series | The folder name where all the TV Series will be placed. Do not put trailing slash. | mytvseries |
| download_subtitles | true | Whether or not you want all the found subtitles to be downloaded. | false |
| download_default_language | true | Whether or not you want to download only the default Italian audio language. | false |
| selected_language | English | If `"download_default_language"` is `False` the script will download this language. | French |
| max_worker | 20 | How many workers will cooperate to download .ts file. **High value may slow down your pc**. | 30 |
| Key | Default Value | Description | Value Example |
| -------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| DEFAULT | | Contains default configuration options for users. | |
| debug | false | Whether debugging information should be displayed or not. | true |
| get_info | false | Whether additional information should be fetched or not with debug enable. | true |
| show_message | true | Whether messages should be displayed to the user or not. | false |
| clean_console | true | Whether the console should be cleared before displaying new information or not. | false |
| get_moment_title | false | Whether to fetch the title of the moment or not. | true |
| root_path | videos | Path where the script will add movies and TV series folders (see[Path Examples](#Path-examples)). | media/streamingcommunity |
| movies_folder_name | Movies | The folder name where all the movies will be placed. Do not put a trailing slash. | downloaded-movies |
| series_folder_name | Series | The folder name where all the TV series will be placed. Do not put a trailing slash. | mytvseries |
| anime_folder_name | Anime | The folder name where all the anime will be placed. Do not put a trailing slash. | myanime |
| not_close | false | Whether to keep the application running after completion or not. | true |
| -------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| SITE | | Contains site-specific configuration options. | |
| streaming_domain | forum | The domain of the streaming site. | express |
| anime_domain | to | The domain of the anime site. | estate |
| -------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| M3U8 | | Contains options specific to M3U8. | |
| tdqm_workers | 20 | The number of workers that will cooperate to download .ts files.**A high value may slow down your PC** | 40 |
| tqdm_progress_timeout | 10 | The timeout duration for progress display updates in seconds after quit download. | 5 |
| minium_ts_files_in_folder | 15 | The minimum number of .ts files expected in a folder. | 10 |
| donwload_percentage | 1 | The percentage of download completion required to consider the download complete. | 0.95 |
| requests_timeout | 5 | The timeout duration for HTTP requests in seconds. | 10 |
| enable_time_quit | false | Whether to enable quitting the download after a certain time period. | true |
| tqdm_show_progress | false | Whether to show progress during downloads or not.**May slow down your PC** | true |
| cleanup_tmp_folder | true | Whether to clean up temporary folders after processing or not. | false |
| -------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| M3U8_OPTIONS | | Contains options specific to M3U8 file format. | |
| download_audio | true | Indicates whether audio files should be downloaded or not. | false |
| download_subtitles | true | Indicates whether subtitles should be downloaded or not. | false |
| specific_list_audio | ["ita"] | A list of specific audio languages to download. | ["eng", "fra"] |
| specific_list_subtitles | ["eng"] | A list of specific subtitle languages to download. | ["spa", "por"] |
> [!IMPORTANT]
> If you're on **Windows** you'll need to use double black slashes. On Linux/MacOS, one slash is fine.

View File

@ -0,0 +1,60 @@
# 03.03.24
from typing import Dict, Any, List
class Episode:
def __init__(self, data: Dict[str, Any]):
"""
Initialize an Episode object.
Args:
data (Dict[str, Any]): A dictionary containing data for the episode.
"""
self.id: int = data.get('id', '')
self.number: int = data.get('number', '')
self.name: str = data.get('name', '')
self.plot: str = data.get('plot', '')
self.duration: int = data.get('duration', '')
self.scws_id: int = data.get('scws_id', '')
self.season_id: int = data.get('season_id', '')
self.created_by: str = data.get('created_by', '')
self.created_at: str = data.get('created_at', '')
self.updated_at: str = data.get('updated_at', '')
class EpisodeManager:
def __init__(self):
"""
Initialize an EpisodeManager object.
"""
self.episodes: List[Episode] = []
def add_episode(self, episode_data: Dict[str, Any]):
"""
Add a new episode to the manager.
Args:
episode_data (Dict[str, Any]): A dictionary containing data for the new episode.
"""
episode = Episode(episode_data)
self.episodes.append(episode)
def get_episode_by_index(self, index: int) -> Episode:
"""
Get an episode by its index.
Args:
index (int): Index of the episode to retrieve.
Returns:
Episode: The episode object.
"""
return self.episodes[index]
def get_length(self) -> int:
"""
Get the number of episodes in the manager.
Returns:
int: Number of episodes.
"""
return len(self.episodes)

View File

@ -0,0 +1,80 @@
# 03.03.24
# Import
from typing import List
class Image:
def __init__(self, data: dict):
"""
Initialize an Image object.
Args:
data (dict): Data for initializing the Image.
"""
self.imageable_id: int = data.get('imageable_id')
self.imageable_type: str = data.get('imageable_type')
self.filename: str = data.get('filename')
self.type: str = data.get('type')
self.original_url_field: str = data.get('original_url_field')
class MediaItem:
def __init__(self, data: dict):
"""
Initialize a MediaItem object.
Args:
data (dict): Data for initializing the MediaItem.
"""
self.id: int = data.get('id')
self.slug: str = data.get('slug')
self.name: str = data.get('name')
self.type: str = data.get('type')
self.score: str = data.get('score')
self.sub_ita: int = data.get('sub_ita')
self.last_air_date: str = data.get('last_air_date')
self.seasons_count: int = data.get('seasons_count')
# Create Image objects for each image in the data
self.images: List[Image] = [Image(image_data) for image_data in data.get('images', [])]
self.comment: str = "" # Initialize comment as an empty string
class MediaManager:
def __init__(self):
"""
Initialize a MediaManager object.
"""
self.media_list: List[MediaItem] = []
def add_media(self, data: dict) -> None:
"""
Add media to the list.
Args:
data (dict): Media data to add.
"""
self.media_list.append(MediaItem(data))
def get(self, index: int) -> MediaItem:
"""
Get a media item from the list by index.
Args:
index (int): The index of the media item to retrieve.
Returns:
MediaItem: The media item at the specified index.
"""
return self.media_list[index]
def get_length(self) -> int:
"""
Get the number of media find with research
Returns:
int: Number of episodes.
"""
return len(self.media_list)

View File

@ -0,0 +1,62 @@
# 03.03.24
from typing import List, Dict, Union
class Title:
def __init__(self, title_data: Dict[str, Union[int, str, None]]):
"""
Initialize a Title object.
Args:
title_data (Dict[str, Union[int, str, None]]): A dictionary containing data for the title.
"""
self.id: int = title_data.get('id')
self.number: int = title_data.get('number')
self.name: str = title_data.get('name')
self.plot: str = title_data.get('plot')
self.release_date: str = title_data.get('release_date')
self.title_id: int = title_data.get('title_id')
self.created_at: str = title_data.get('created_at')
self.updated_at: str = title_data.get('updated_at')
self.episodes_count: int = title_data.get('episodes_count')
class TitleManager:
def __init__(self):
"""
Initialize a TitleManager object.
Args:
titles (List[Title]): A list of Title objects. Defaults to an empty list.
"""
self.titles: List[Title] = []
def add_title(self, title_data: Dict[str, Union[int, str, None]]):
"""
Add a new title to the manager.
Args:
title_data (Dict[str, Union[int, str, None]]): A dictionary containing data for the new title.
"""
title = Title(title_data)
self.titles.append(title)
def get_title_by_index(self, index: int) -> Title:
"""
Get a title by its index.
Args:
index (int): Index of the title to retrieve.
Returns:
Title: The title object.
"""
return self.titles[index]
def get_length(self) -> int:
"""
Get the number of titles in the manager.
Returns:
int: Number of titles.
"""
return len(self.titles)

326
Src/Api/Class/Video.py Normal file
View File

@ -0,0 +1,326 @@
# 01.03.24
# Class import
from Src.Util.headers import get_headers
from .SeriesType import TitleManager
from .EpisodeType import EpisodeManager
from .WindowType import WindowVideo, WindowParameter
# Import
import requests
import re
import json
import binascii
import logging
import sys
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlencode
class VideoSource:
def __init__(self):
"""
Initialize a VideoSource object.
"""
self.headers: dict[str, str] = {
'user-agent': get_headers()
}
self.is_series: bool = False
def set_version(self, version: str) -> None:
"""
Set the version.
Args:
version (str): The version to set.
"""
self.version = version
def set_domain(self, domain: str) -> None:
"""
Set the domain.
Args:
domain (str): The domain to set.
"""
self.domain = domain
def set_url_base_name(self, base_name: str) -> None:
"""
Set the base url of the site.
Args:
domain (str): The url of the site to set.
"""
self.base_name = base_name
def set_media_id(self, media_id: str) -> None:
"""
Set the media ID.
Args:
media_id (str): The media ID to set.
"""
self.media_id = media_id
def set_series_name(self, series_name: str) -> None:
"""
Set the series name.
Args:
series_name (str): The series name to set.
"""
self.is_series: bool = True
self.series_name: str = series_name
self.obj_title_manager: TitleManager = TitleManager()
self.obj_episode_manager: EpisodeManager = EpisodeManager()
def collect_info_seasons(self) -> None:
"""
Collect information about seasons.
"""
self.headers = {
'user-agent': get_headers(),
'x-inertia': 'true',
'x-inertia-version': self.version,
}
try:
# Make a request to collect information about seasons
response = requests.get(f"https://{self.base_name}.{self.domain}/titles/{self.media_id}-{self.series_name}", headers=self.headers)
response.raise_for_status() # Raise exception for non-200 status codes
if response.ok:
# Extract JSON response if available
json_response = response.json().get('props', {}).get('title', {}).get('seasons', [])
# Iterate over JSON data and add titles to the manager
for dict_season in json_response:
self.obj_title_manager.add_title(dict_season)
except Exception as e:
logging.error(f"Error collecting season info: {e}")
sys.exit(0)
def collect_title_season(self, number_season: int) -> None:
"""
Collect information about a specific season.
Args:
number_season (int): The season number.
"""
try:
# Make a request to collect information about a specific season
response = requests.get(f'https://{self.base_name}.{self.domain}/titles/{self.media_id}-{self.series_name}/stagione-{number_season}', headers=self.headers)
response.raise_for_status() # Raise exception for non-200 status codes
if response.ok:
# Extract JSON response if available
json_response = response.json().get('props', {}).get('loadedSeason', {}).get('episodes', [])
# Iterate over JSON data and add episodes to the manager
for dict_episode in json_response:
self.obj_episode_manager.add_episode(dict_episode)
except Exception as e:
logging.error(f"Error collecting title season info: {e}")
sys.exit(0)
def get_iframe(self, episode_id: str = None) -> None:
"""
Get iframe source.
Args:
episode_id (str): The episode ID, present only for series
"""
params = {}
if self.is_series:
params = {
'episode_id': episode_id,
'next_episode': '1'
}
try:
# Make a request to get iframe source
response = requests.get(f"https://{self.base_name}.{self.domain}/iframe/{self.media_id}", params=params)
response.raise_for_status() # Raise exception for non-200 status codes
if response.ok:
# Parse response with BeautifulSoup to get iframe source
soup = BeautifulSoup(response.text, "html.parser")
self.iframe_src: str = soup.find("iframe").get("src")
except Exception as e:
logging.error(f"Error getting iframe source: {e}")
sys.exit(0)
def parse_script(self, script_text: str) -> None:
"""
Parse script text.
Args:
script_text (str): The script text to parse.
"""
try:
# Extract window video and parameter information from script text
str_window_video = re.search(r"window.video = {.*}", str(script_text)).group()
str_window_parameter = re.search(r"params: {[\s\S]*}", str(script_text)).group()
# Fix windos and video parameter
str_window_video = str_window_video.split(" = ")[1]
str_window_parameter = str(str_window_parameter.replace("\n", "").replace(" ", "").split(",},")[0] + "}").split("params: ")[1]
# Create window video and parameter objects
self.window_video = WindowVideo(data = json.loads(str_window_video.replace("'", '"')))
self.window_parameter = WindowParameter(data = json.loads(str_window_parameter.replace("'", '"')))
except Exception as e:
logging.error(f"Error parsing script: {e}")
sys.exit(0)
def get_content(self) -> None:
"""
Get content.
"""
try:
# Check if iframe source is available
if self.iframe_src is not None:
# Make a request to get content
response = requests.get(self.iframe_src, headers=self.headers)
response.raise_for_status() # Raise exception for non-200 status codes
if response.ok:
# Parse response with BeautifulSoup to get content
soup = BeautifulSoup(response.text, "html.parser")
script = soup.find("body").find("script").text
# Parse script to get video information
self.parse_script(script_text=script)
except Exception as e:
logging.error(f"Error getting content: {e}")
sys.exit(0)
def get_playlist(self) -> str:
"""
Get playlist.
Returns:
str: The playlist URL, or None if there's an error.
"""
try:
# Generate playlist URL
query = urlencode(list(self.window_parameter.data.items()))
base_url = f'https://vixcloud.co/playlist/{self.window_video.id}'
full_url = urljoin(base_url, '?' + query)
return full_url
except AttributeError as e:
logging.error(f"Error getting playlist: {e}")
sys.exit(0)
def get_key(self) -> str:
"""
Get key.
Returns:
str: The key content, or None if there's an error.
"""
try:
# Set referer header for the request
self.headers['referer'] = f'https://vixcloud.co/embed/{self.window_video.id}?token={self.window_parameter.token}&title={self.window_video.name.replace(" ", "+")}&referer=1&expires={self.window_parameter.expires}&canPlayFHD=1'
# Make a request to get key content
response = requests.get('https://vixcloud.co/storage/enc.key', headers=self.headers)
response.raise_for_status() # Raise exception for non-200 status codes
if response.ok:
# Convert key content to hexadecimal format
hex_content = binascii.hexlify(response.content).decode('utf-8')
return hex_content
except Exception as e:
logging.error(f"Error getting key: {e}")
sys.exit(0)
class VideoSourceAnime(VideoSource):
"""
A class representing a video source for anime content.
Inherits from VideoSource class.
"""
def __init__(self) -> None:
super().__init__()
# MEDIA ID IS THE INDEX OF EPISODE
def collect_episode_info(self) -> None:
"""
Collects information about the episode.
"""
try:
if self.media_id is None:
raise ValueError("Media ID is not set.")
params = {
'start_range': self.media_id,
'end_range': self.media_id + 1
}
# series_name is the index of season in this case, index is the index of episode
response = requests.get(f'https://www.{self.base_name}.{self.domain}/info_api/{self.series_name}/{self.media_id}', params=params)
if not response.ok:
return None
response.raise_for_status()
# Get last episode in json request
json_response = response.json()['episodes'][-1]
# Add in array of episode ( only one is store )
self.obj_episode_manager.add_episode(json_response)
except Exception as e:
logging.error(f"An error occurred while collecting episode info: {e}")
raise
def get_embed(self) ->str:
"""
Retrieves the embed URL for the episode.
"""
try:
if not self.obj_episode_manager.episodes:
raise ValueError("No episodes available.")
# Make request to get vixcloud embed url
embed_url_response = requests.get(f'https:///www.{self.base_name}.{self.domain}/embed-url/{self.obj_episode_manager.episodes[0].id}')
if not embed_url_response.ok:
return None
embed_url_response.raise_for_status()
# Make request to embed url to get video paramter text
embed_url = requests.get(embed_url_response.text).text
# Parse script to get video information
self.parse_script(script_text=embed_url)
except Exception as e:
logging.error(f"An error occurred while getting embed URL: {e}")
raise

View File

@ -0,0 +1,42 @@
# 03.03.24
from typing import Dict, Any
class WindowVideo:
def __init__(self, data: Dict[str, Any]):
"""
Initialize a WindowVideo object.
Args:
data (dict): A dictionary containing data for the video.
"""
self.data = data
self.id: int = data.get('id', '')
self.name: str = data.get('name', '')
self.filename: str = data.get('filename', '')
self.size: str = data.get('size', '')
self.quality: str = data.get('quality', '')
self.duration: str = data.get('duration', '')
self.views: int = data.get('views', '')
self.is_viewable: bool = data.get('is_viewable', '')
self.status: str = data.get('status', '')
self.fps: float = data.get('fps', '')
self.legacy: bool = data.get('legacy', '')
self.folder_id: int = data.get('folder_id', '')
self.created_at_diff: str = data.get('created_at_diff', '')
class WindowParameter:
def __init__(self, data: Dict[str, Any]):
"""
Initialize a WindowParameter object.
Args:
data (dict): A dictionary containing parameters for the window.
"""
self.data = data
self.token: str = data.get('token', '')
self.token360p: str = data.get('token360p', '')
self.token480p: str = data.get('token480p', '')
self.token720p: str = data.get('token720p', '')
self.token1080p: str = data.get('token1080p', '')
self.expires: str = data.get('expires', '')

View File

@ -0,0 +1,4 @@
# 03.03.24
from .Video import VideoSource
from .SearchType import MediaManager, MediaItem

6
Src/Api/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# 03.03.24
from .film import download_film
from .series import download_series
from .site import get_version_and_domain, search, anime_search, get_select_title
from .anime import anime_download_film, anime_download_series

244
Src/Api/anime.py Normal file
View File

@ -0,0 +1,244 @@
# 11.03.24
# Class import
from Src.Util.console import console, msg
from Src.Util.config import config_manager
from Src.Lib.FFmpeg.my_m3u8 import Downloader
from Src.Util.message import start_message
from .Class import VideoSource
# General import
import os
import logging
import requests
# Config
ROOT_PATH = config_manager.get('DEFAULT', 'root_path')
ANIME_FOLDER = config_manager.get('DEFAULT', 'anime_folder_name')
MOVIE_FOLDER = config_manager.get('DEFAULT', 'movies_folder_name')
SERIES_FOLDER = config_manager.get('DEFAULT', 'series_folder_name')
URL_SITE_NAME = config_manager.get('SITE', 'anime_site_name')
SITE_DOMAIN = config_manager.get('SITE', 'anime_domain')
# Variable
video_source = VideoSource()
class EpisodeDownloader:
def __init__(self, tv_id: int, tv_name: str, is_series: bool = True):
"""
Initialize EpisodeDownloader class.
Args:
tv_id (int): The ID of the TV show.
tv_name (str): The name of series or for film the name of film
"""
self.tv_id = tv_id
self.tv_name = tv_name
self.is_series = is_series
@staticmethod
def manage_selection(cmd_insert: str, max_count: int):
list_season_select = []
# For a single number (e.g., '5')
if cmd_insert.isnumeric():
list_season_select.append(int(cmd_insert))
# For a range (e.g., '[5-12]')
elif "[" in cmd_insert:
start, end = map(int, cmd_insert[1:-1].split("-"))
list_season_select = list(range(start, end + 1))
# For all seasons
elif cmd_insert == "*":
list_season_select = list(range(1, max_count + 1))
# Return list of selected seasons
return list_season_select
def get_count_episodes(self):
try:
# Send a GET request to fetch information about the TV show
response = requests.get(
f"https://www.{URL_SITE_NAME}.{SITE_DOMAIN}/info_api/{self.tv_id}/"
)
# Raise an exception for bad status codes
response.raise_for_status()
# Extract the count of episodes from the JSON response
return response.json()["episodes_count"]
except Exception as e:
logging.error(f"(EpisodeDownloader) Error fetching episode count: {e}")
return 0
def get_info_episode(self, index_ep: int):
try:
# Send a GET request to fetch information about the specific episode
params = {"start_range": index_ep, "end_range": index_ep + 1}
response = requests.get(
f"https://www.{URL_SITE_NAME}.{SITE_DOMAIN}/info_api/{self.tv_id}/{index_ep}",
params=params,
)
# Raise an exception for bad status codes
response.raise_for_status()
# Extract episode information from the JSON response
return response.json()["episodes"][-1]
except Exception as e:
logging.error(
f"(EpisodeDownloader) Error fetching episode information: {e}"
)
return None
def get_embed(self, episode_id: int):
try:
# Send a GET request to fetch the embed URL for the episode
response = requests.get(
f"https://www.{URL_SITE_NAME}.{SITE_DOMAIN}/embed-url/{episode_id}"
)
# Raise an exception for bad status codes
response.raise_for_status()
# Extract the embed URL from the response
embed_url = response.text.strip()
# Validate the embed URL
if not embed_url.startswith("http"):
raise ValueError("Invalid embed URL")
# Fetch the actual video URL using the embed URL
video_response = requests.get(embed_url)
# Raise an exception for bad status codes
video_response.raise_for_status()
# Return the video URL
return video_response.text
except Exception as e:
logging.error(f"(EpisodeDownloader) Error fetching embed URL: {e}")
return None
def download_episode(self, index_select):
# Get information about the selected episode
info_ep_select = self.get_info_episode(index_select)
if not info_ep_select:
logging.error("(EpisodeDownloader) Error getting episode information.")
return
# Extract the ID of the selected episode
episode_id = info_ep_select.get("id")
start_message()
console.print(f"[yellow]Download: [red]{episode_id} \n")
# Get the embed URL for the episode
embed_url = self.get_embed(episode_id)
if not embed_url:
logging.error("Error getting embed URL.")
return
# Parse parameter in embed text
video_source.parse_script(embed_url)
# Download the film using the m3u8 playlist, key, and output filename
try:
if self.is_series:
obj_download = Downloader(
m3u8_playlist=video_source.get_playlist(),
key=video_source.get_key(),
output_filename=os.path.join(
ROOT_PATH,
ANIME_FOLDER,
SERIES_FOLDER,
self.tv_name,
f"{index_select}.mp4",
),
)
else:
obj_download = Downloader(
m3u8_playlist=video_source.get_playlist(),
key=video_source.get_key(),
output_filename=os.path.join(
ROOT_PATH,
ANIME_FOLDER,
MOVIE_FOLDER,
f"{self.tv_name}.mp4"
),
)
obj_download.download_m3u8()
except Exception as e:
logging.error(f"(EpisodeDownloader) Error downloading film: {e}")
# ONLY SERIES
def anime_download_series(tv_id: int, tv_name: str):
"""
Function to download episodes of a TV series.
Args:
- tv_id (int): The ID of the TV series.
- tv_name (str): The name of the TV series.
"""
# Get the count of episodes for the TV series
episodes_downloader = EpisodeDownloader(tv_id, tv_name)
episoded_count = episodes_downloader.get_count_episodes()
console.log(f"[cyan]Episodes find: [red]{episoded_count}")
# Prompt user to select an episode index
last_command = msg.ask("\n[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]for a range of media")
# Manage user selection
list_episode_select = EpisodeDownloader.manage_selection(last_command, episoded_count)
# Download selected episodes
if len(list_episode_select) == 1 and last_command != "*":
episodes_downloader.download_episode(list_episode_select[0])
# Download all other episodes selecter
else:
for i_episode in list_episode_select:
episodes_downloader.download_episode(i_episode)
# ONLY FILM
def anime_download_film(id_film: int, title_name: str):
"""
Function to download a film.
Args:
- id_film (int): The ID of the film.
- title_name (str): The title of the film.
"""
# Placeholder function for downloading a film
episodes_downloader = EpisodeDownloader(id_film, title_name, is_series=False)
# Extract the ID of the selected episode and download
episodes_downloader.download_episode(0)

View File

@ -1,134 +1,62 @@
# 3.12.23 -> 10.12.23
# General import
import binascii
import json
import os
import re
import requests
import sys
from bs4 import BeautifulSoup
# Class import
from Src.Lib.FFmpeg.my_m3u8 import download_m3u8
from Src.Lib.FFmpeg.util import audio_extractor_m3u8
from Src.Util.config import config
from Src.Util.console import console
from Src.Util.headers import get_headers
from Src.Util.config import config_manager
from Src.Lib.FFmpeg.my_m3u8 import Downloader
from Src.Util.message import start_message
from .Class import VideoSource
# General import
import os
import logging
# Config
ROOT_PATH = config_manager.get('DEFAULT', 'root_path')
MOVIE_FOLDER = config_manager.get('DEFAULT', 'movies_folder_name')
STREAM_SITE_NAME = config_manager.get('SITE', 'streaming_site_name')
def get_iframe(id_title, domain):
req = requests.get(url=f"https://streamingcommunity.{domain}/iframe/{id_title}", headers={
"User-agent": get_headers()
})
# Variable
video_source = VideoSource()
video_source.set_url_base_name(STREAM_SITE_NAME)
if req.ok:
url_embed = BeautifulSoup(req.text, "lxml").find("iframe").get("src")
req_embed = requests.get(url_embed, headers={"User-agent": get_headers()}).text
def download_film(id_film: str, title_name: str, domain: str):
"""
Downloads a film using the provided film ID, title name, and domain.
try:
return BeautifulSoup(req_embed, "lxml").find("body").find("script").text
except Exception:
console.log("[red]Couldn't play this video file (video not available)")
sys.exit(0)
Args:
id_film (str): The ID of the film.
title_name (str): The name of the film title.
domain (str): The domain of the site
"""
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
# Start message and display film information
start_message()
console.print(f"[yellow]Download: [red]{title_name} \n")
# Set domain and media ID for the video source
video_source.set_domain(domain)
video_source.set_media_id(id_film)
def select_quality(json_win_param):
if json_win_param['token1080p']:
return "1080p"
elif json_win_param['token720p']:
return "720p"
elif json_win_param['token480p']:
return "480p"
else:
return "360p"
# Retrieve the iframe and content for the video source
video_source.get_iframe()
video_source.get_content()
def parse_content(embed_content):
# Parse parameter from req embed content
win_video = re.search(r"window.video = {.*}", str(embed_content)).group()
win_param = re.search(r"params: {[\s\S]*}", str(embed_content)).group()
# Parse parameter to make read for json
json_win_video = "{" + win_video.split("{")[1].split("}")[0] + "}"
json_win_param = ("{" + win_param.split("{")[1]
.split("}")[0]
.replace("\n", "")
.replace(" ", "") + "}")
json_win_param = json_win_param.replace(",}", "}").replace("'", '"')
return json.loads(json_win_video), json.loads(json_win_param), select_quality(json.loads(json_win_param))
def get_m3u8_url(json_win_video, json_win_param, render_quality):
token_render = f"token{render_quality}"
return f"https://vixcloud.co/playlist/{json_win_video['id']}?type=video&rendition={render_quality}&token={json_win_param[token_render]}&expires={json_win_param['expires']}"
def get_playlist(json_win_video, json_win_param, render_quality):
token_render = f"token{render_quality}"
return f"https://vixcloud.co/playlist/{json_win_video['id']}?token={json_win_param['token']}&{token_render}={json_win_param[token_render]}&expires={json_win_param['expires']}"
def get_m3u8_key():
response = requests.get('https://vixcloud.co/storage/enc.key')
if response.ok:
return binascii.hexlify(response.content).decode('utf-8')
else:
console.log(f"[red]Error: {response.status_code}")
sys.exit(0)
def get_m3u8_audio(json_win_video, json_win_param, title_name, token_render):
req = requests.get(
f'https://vixcloud.co/playlist/{json_win_video["id"]}',
params={'token': json_win_param['token'],
'expires': json_win_param["expires"]},
headers={
'referer':
f'https://vixcloud.co/embed/{json_win_video["id"]}?token={json_win_param[token_render]}&title={title_name}&referer=1&expires={json_win_param["expires"]}'
})
if req.ok:
result = audio_extractor_m3u8(req)
return result
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
# [func \ main]
def main_dw_film(id_film, title_name, domain):
embed_content = get_iframe(id_film, domain)
json_win_video, json_win_param, render_quality = parse_content(embed_content)
token_render = f"token{render_quality}"
console.print(f"[blue]Selected quality => [red]{render_quality}")
m3u8_url = get_m3u8_url(json_win_video, json_win_param, render_quality)
m3u8_key = get_m3u8_key()
m3u8_playlist = get_playlist(json_win_video, json_win_param, render_quality)
mp4_name = title_name.replace("+", " ").replace(",", "").replace("-", "_")
# Define the filename and path for the downloaded film
mp4_name = title_name.replace("-", "_")
mp4_format = mp4_name + ".mp4"
mp4_path = os.path.join(config['root_path'], config['movies_folder_name'], mp4_name, mp4_format)
m3u8_url_audio = get_m3u8_audio(json_win_video, json_win_param, title_name, token_render)
# Download the film using the m3u8 playlist, key, and output filename
try:
obj_download = Downloader(
m3u8_playlist = video_source.get_playlist(),
key = video_source.get_key(),
output_filename = os.path.join(ROOT_PATH, MOVIE_FOLDER, title_name, mp4_format)
)
if m3u8_url_audio is not None:
console.print("[blue]Using m3u8 audio => [red]True")
subtitle_path = os.path.join(config['root_path'], config['movies_folder_name'], mp4_name)
obj_download.download_m3u8()
download_m3u8(
m3u8_index = m3u8_url,
m3u8_audio = m3u8_url_audio,
m3u8_subtitle = m3u8_playlist,
key = m3u8_key,
output_filename = mp4_path,
subtitle_folder = subtitle_path,
content_name = mp4_name
)
except Exception as e:
logging.error(f"(download_film) Error downloading film: {e}")
pass

View File

@ -1,60 +0,0 @@
# 10.12.23
# Class import
from Src.Util.headers import get_headers
from Src.Util.console import console
from Src.Util.config import config, config_manager
# General import
import requests, sys, json
from bs4 import BeautifulSoup
def domain_version():
site_url = f"https://streamingcommunity.{config['domain']}"
domain = None
try:
requests.get(site_url, headers={'user-agent': get_headers()})
except:
domain_req = requests.get("https://api.telegra.ph/getPage/Link-Aggiornato-StreamingCommunity-01-17")
domain = domain_req.json()['result']['description'].split(".")[1]
console.print("[green]Getting rules...")
console.print(f"[blue]Test domain [white]=> [red]{domain}")
config_manager.update_config('domain', domain)
if domain != None:
site_url = f"https://streamingcommunity.{domain}"
console.print(f"[blue]Use domain [white]=> [red]{domain}")
else:
domain = config['domain']
try:
site_request = requests.get(site_url, headers={'user-agent': get_headers()})
soup = BeautifulSoup(site_request.text, "lxml")
version = json.loads(soup.find("div", {"id": "app"}).get("data-page"))['version']
console.print(f"[blue]Rules [white]=> [red].{domain}")
return domain, version
except Exception as e:
console.log("[red]Couldn't get the version, there's a problem with the domain. Try again." , e)
sys.exit(0)
def search(title_search, domain):
req = requests.get(f"https://streamingcommunity.{domain}/api/search?q={title_search}", headers={'user-agent': get_headers()})
if req.ok:
return [{'name': title['name'], 'type': title['type'], 'id': title['id'], 'slug': title['slug']} for title in
req.json()['data']][0:21]
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
def display_search_results(db_title):
for i, title in enumerate(db_title):
console.print(f"[yellow]{i} [white]-> [green]{title['name']} [white]- [cyan]{title['type']}")

209
Src/Api/series.py Normal file
View File

@ -0,0 +1,209 @@
# 3.12.23 -> 10.12.23
# Class import
from Src.Util.console import console, msg
from Src.Util.config import config_manager
from Src.Util.table import TVShowManager
from Src.Util.message import start_message
from Src.Lib.FFmpeg.my_m3u8 import Downloader
from .Class import VideoSource
# General import
import os
import logging
import sys
# Config
ROOT_PATH = config_manager.get('DEFAULT', 'root_path')
SERIES_FOLDER = config_manager.get('DEFAULT', 'series_folder_name')
STREAM_SITE_NAME = config_manager.get('SITE', 'streaming_site_name')
# Variable
video_source = VideoSource()
video_source.set_url_base_name(STREAM_SITE_NAME)
table_show_manager = TVShowManager()
def manage_selection(cmd_insert: str, max_count: int) -> list[int]:
"""
Manage user selection for seasons to download.
Args:
cmd_insert (str): User input for season selection.
max_count (int): Maximum count of seasons available.
Returns:
list_season_select (List[int]): List of selected seasons.
"""
list_season_select = []
# For a single number (e.g., '5')
if cmd_insert.isnumeric():
list_season_select.append(int(cmd_insert))
# For a range (e.g., '[5-12]')
elif "[" in cmd_insert:
start, end = map(int, cmd_insert[1:-1].split('-'))
list_season_select = list(range(start, end + 1))
# For all seasons
elif cmd_insert == "*":
list_season_select = list(range(1, max_count+1))
# Return list of selected seasons
return list_season_select
def display_episodes_list() -> str:
"""
Display episodes list and handle user input.
Returns:
last_command (str): Last command entered by the user.
"""
# Set up table for displaying episodes
table_show_manager.set_slice_end(10)
# Add columns to the table
column_info = {
"Index": {'color': 'red'},
"Name": {'color': 'magenta'},
"Duration": {'color': 'green'}
}
table_show_manager.add_column(column_info)
# Populate the table with episodes information
for i, media in enumerate(video_source.obj_episode_manager.episodes):
table_show_manager.add_tv_show({
'Index': str(media.number),
'Name': media.name,
'Duration': str(media.duration)
})
# Run the table and handle user input
last_command = table_show_manager.run()
if last_command == "q":
console.print("\n[red]Quit [white]...")
sys.exit(0)
return last_command
def donwload_video(tv_name: str, index_season_selected: int, index_episode_selected: int) -> None:
"""
Download a single episode video.
Args:
tv_name (str): Name of the TV series.
index_season_selected (int): Index of the selected season.
index_episode_selected (int): Index of the selected episode.
"""
# Start message and display episode information
start_message()
console.print(f"[yellow]Download: [red]{video_source.obj_episode_manager.episodes[index_episode_selected - 1].name} \n")
episode_id = video_source.obj_episode_manager.episodes[index_episode_selected - 1].id
# Define filename and path for the downloaded video
mp4_name = f"{index_episode_selected}.mp4"
mp4_path = os.path.join(ROOT_PATH, SERIES_FOLDER, tv_name, f"S{index_season_selected}", f"E{index_episode_selected}")
os.makedirs(mp4_path, exist_ok=True)
# Get iframe and content for the episode
video_source.get_iframe(episode_id)
video_source.get_content()
video_source.set_url_base_name(STREAM_SITE_NAME)
# Download the episode
try:
obj_download = Downloader(
m3u8_playlist = video_source.get_playlist(),
key = video_source.get_key(),
output_filename = os.path.join(mp4_path, mp4_name)
)
obj_download.download_m3u8()
except Exception as e:
logging.error(f"(donwload_video) Error downloading film: {e}")
pass
def donwload_episode(tv_name: str, index_season_selected: int, donwload_all: bool = False) -> None:
"""
Download all episodes of a season.
Args:
tv_name (str): Name of the TV series.
index_season_selected (int): Index of the selected season.
donwload_all (bool): Donwload all seasons episodes
"""
# Start message and collect information about episodes
start_message()
video_source.collect_title_season(index_season_selected)
episodes_count = video_source.obj_episode_manager.get_length()
# Download all episodes wihtout ask
if donwload_all:
for i_episode in range(0, episodes_count):
donwload_video(tv_name, index_season_selected, i_episode)
# Exit
console.print("\n[red]Done")
sys.exit(0)
# Display episodes list and manage user selection
last_command = display_episodes_list()
list_episode_select = manage_selection(last_command, episodes_count)
# Download selected episodes
if len(list_episode_select) == 1 and last_command != "*":
donwload_video(tv_name, index_season_selected, list_episode_select[0])
# Download all other episodes selecter
else:
for i_episode in list_episode_select:
donwload_video(tv_name, index_season_selected, i_episode)
def download_series(tv_id: str, tv_name: str, version: str, domain: str) -> None:
"""
Download all episodes of a TV series.
Args:
tv_id (str): ID of the TV series.
tv_name (str): Name of the TV series.
version (str): Version of the TV series.
domain (str): Domain from which to download.
"""
# Start message and set up video source
start_message()
video_source.set_version(version)
video_source.set_domain(domain)
video_source.set_series_name(tv_name)
video_source.set_media_id(tv_id)
# Collect information about seasons
video_source.collect_info_seasons()
seasons_count = video_source.obj_title_manager.get_length()
# Prompt user for season selection and download episodes
console.print(f"\n[green]Season find: [red]{seasons_count}")
index_season_selected = str(msg.ask("\n[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]for a range of media"))
list_season_select = manage_selection(index_season_selected, seasons_count)
# Download selected episodes
if len(list_season_select) == 1 and index_season_selected != "*":
if 1 <= int(index_season_selected) <= seasons_count:
donwload_episode(tv_name, list_season_select[0])
# Dowload all seasons and episodes
elif index_season_selected == "*":
for i_season in list_season_select:
donwload_episode(tv_name, i_season, True)
# Download all other season selecter
else:
for i_season in list_season_select:
donwload_episode(tv_name, i_season)

369
Src/Api/site.py Normal file
View File

@ -0,0 +1,369 @@
# 10.12.23
# Class import
from Src.Util.table import TVShowManager
from Src.Util.headers import get_headers
from Src.Util.console import console
from Src.Util.config import config_manager
from .Class import MediaManager, MediaItem
# General import
import sys
import json
import logging
import requests
from bs4 import BeautifulSoup
# Config
GET_TITLES_OF_MOMENT = config_manager.get_bool('DEFAULT', 'get_moment_title')
# Variable
media_search_manager = MediaManager()
table_show_manager = TVShowManager()
def get_token(site_name: str, domain: str) -> dict:
"""
Function to retrieve session tokens from a specified website.
Args:
- site_name (str): The name of the site.
- domain (str): The domain of the site.
Returns:
- dict: A dictionary containing session tokens.
The keys are 'XSRF_TOKEN', 'animeunity_session', and 'csrf_token'.
"""
# Create a session object to handle the HTTP request and response
session = requests.Session()
# Send a GET request to the specified URL composed of the site name and domain
response = session.get(f"https://www.{site_name}.{domain}")
# Initialize variables to store CSRF token
find_csrf_token = None
# Parse the HTML response using BeautifulSoup
soup = BeautifulSoup(response.text, "lxml")
# Loop through all meta tags in the HTML response
for html_meta in soup.find_all("meta"):
# Check if the meta tag has a 'name' attribute equal to "csrf-token"
if html_meta.get('name') == "csrf-token":
# If found, retrieve the content of the meta tag, which is the CSRF token
find_csrf_token = html_meta.get('content')
return {
'XSRF_TOKEN': session.cookies['XSRF-TOKEN'],
'animeunity_session': session.cookies['animeunity_session'],
'csrf_token': find_csrf_token
}
def get_moment_titles(domain: str, version: str, prefix: str):
"""
Retrieves the title name from a specified domain using the provided version and prefix.
Args:
- domain (str): The domain of the site
- version (str): The version of the site
- prefix (str): The prefix used for retrieval. [film or serie-tv or "" for site]
Returns:
- str or None: The title name if retrieval is successful, otherwise None.
"""
try:
headers = {
'x-inertia': 'true',
'x-inertia-version': version,
'user-agent': get_headers()
}
response = requests.get(f'https://streamingcommunity.{domain}/{prefix}', headers=headers)
if response.ok:
# Extract title name
title_name = response.json()['props']['title']['name']
# Return
return title_name
else:
logging.error("Failed to retrieve data. Status code: %d", response.status_code)
return None
except Exception as e:
logging.error("Error occurred: %s", str(e))
return None
def get_domain() -> str:
"""
Fetches the domain from a Telegra.ph API response.
Returns:
str: The domain extracted from the API response.
"""
console.print("[cyan]Make request api [white]...")
try:
response = requests.get("https://api.telegra.ph/getPage/Link-Aggiornato-StreamingCommunity-01-17", headers={'user-agent': get_headers()})
console.print(f"[green]Request response [white]=> [red]{response.status_code} \n")
response.raise_for_status() # Raise an error if request fails
if response.ok:
domain = response.json()['result']['description'].split(".")[1]
return domain
except Exception as e:
logging.error(f"Error fetching domain: {e}")
sys.exit(0)
def test_site(domain: str) -> str:
"""
Tests the availability of a website.
Args:
domain (str): The domain to test.
Returns:
str: The response text if successful, otherwise None.
"""
console.print("[cyan]Make request site [white]...")
site_url = f"https://streamingcommunity.{domain}"
try:
response = requests.get(site_url, headers={'user-agent': get_headers()})
console.print(f"[green]Request response [white]=> [red]{response.status_code} \n")
response.raise_for_status() # Raise an error if request fails
if response.ok:
return response.text
else:
return None
except Exception as e:
logging.error(f"Error testing site: {e}")
return None
def get_version(text: str) -> str:
"""
Extracts the version from the HTML text of a webpage.
Args:
text (str): The HTML text of the webpage.
Returns:
str: The version extracted from the webpage.
"""
console.print("[cyan]Make request to get version [white]...")
try:
soup = BeautifulSoup(text, "html.parser")
version = json.loads(soup.find("div", {"id": "app"}).get("data-page"))['version']
console.print(f"[green]Request response [white]=> [red]200 \n")
return version
except Exception as e:
logging.error(f"Error extracting version: {e}")
sys.exit(0)
def get_version_and_domain() -> tuple[str, str]:
"""
Retrieves the version and domain of a website.
Returns:
tuple[str, str]: A tuple containing the version and domain.
"""
try:
config_domain = config_manager.get('SITE', 'streaming_domain')
response_test_site = test_site(config_domain)
if response_test_site is None:
config_domain = get_domain()
response_test_site = test_site(config_domain)
if response_test_site:
version = get_version(response_test_site)
# Update domain config file
config_manager.set_key('SITE', 'streaming_domain', str(config_domain))
config_manager.write_config()
# Get titles in the moment
if GET_TITLES_OF_MOMENT:
console.print("[cyan]Scrape information [white]...")
console.print(f"[green]Title of the moments: [purple]{get_moment_titles(config_domain, version, '')}")
console.print(f"[green]Film of the moments: [purple]{get_moment_titles(config_domain, version, 'film')}")
console.print(f"[green]Serie of the moments: [purple]{get_moment_titles(config_domain, version, 'serie-tv')}")
return version, config_domain
else:
print("Can't connect to site")
sys.exit(0)
except Exception as e:
logging.error(f"Error getting version and domain: {e}")
sys.exit(0)
def search(title_search: str, domain: str) -> int:
"""
Search for titles based on a search query.
Args:
title_search (str): The title to search for.
domain (str): The domain to search on.
Returns:
int: The number of titles found.
"""
# Send request to search for titles
req = requests.get(f"https://streamingcommunity.{domain}/api/search?q={title_search}", headers={'user-agent': get_headers()})
# Add found titles to media search manager
for dict_title in req.json()['data']:
media_search_manager.add_media(dict_title)
# Return the number of titles found
return media_search_manager.get_length()
def update_domain_anime():
"""
Update the domain for anime streaming site.
"""
# Read actual config
url_site_name = config_manager.get('SITE', 'anime_site_name')
url_domain = config_manager.get('SITE', 'anime_domain')
# Test actual site
test_response = requests.get(f"https://www.{url_site_name}.{url_domain}")
console.print(f"[green]Request test response [white]=> [red]{test_response.status_code} \n")
if not test_response.ok:
# Update streaming domain
version, domain = get_version_and_domain()
# Extract url
response = requests.get(f"https://streamingcommunity.{domain}/")
soup = BeautifulSoup(response.text, "html.parser")
# Found the anime site link
new_site_url = None
for html_a in soup.find_all("a"):
if config_manager.get('SITE', 'anime_site_name') in str(html_a.get("href")):
new_site_url = html_a.get("href")
# Extract the domain from the URL and update the config
config_manager.set_key('SITE', 'anime_domain', new_site_url.split(".")[-1])
def anime_search(title_search: str) -> int:
"""
Function to perform an anime search using a provided title.
Args:
- title_search (str): The title to search for.
Returns:
- int: A number containing the length of media search manager.
"""
# Update domain
update_domain_anime()
# Get token and session value from configuration
url_site_name = config_manager.get('SITE', 'anime_site_name')
url_domain = config_manager.get('SITE', 'anime_domain')
data = get_token(url_site_name, url_domain)
# Prepare cookies to be used in the request
cookies = {
'animeunity_session': data.get('animeunity_session') # Use the session token retrieved from data
}
# Prepare headers for the request
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7',
'content-type': 'application/json;charset=UTF-8',
'x-csrf-token': data.get('csrf_token') # Use the CSRF token retrieved from data
}
# Prepare JSON data to be sent in the request
json_data = {
'title': title_search # Use the provided title for the search
}
# Send a POST request to the API endpoint for live search
response = requests.post(f'https://www.{url_site_name}.{url_domain}/livesearch', cookies=cookies, headers=headers, json=json_data)
# Process each record returned in the response
for record in response.json()['records']:
# Rename keys for consistency
record['name'] = record.pop('title')
record['last_air_date'] = record.pop('date')
# Add the record to media search manager if the name is not None
if record['name'] is not None:
media_search_manager.add_media(record)
# Return the length of media search manager
return media_search_manager.get_length()
def get_select_title() -> MediaItem:
"""
Display a selection of titles and prompt the user to choose one.
Returns:
MediaItem: The selected media item.
"""
# Set up table for displaying titles
table_show_manager.set_slice_end(10)
# Add columns to the table
column_info = {
"Index": {'color': 'red'},
"Name": {'color': 'magenta'},
"Type": {'color': 'yellow'},
"Score": {'color': 'cyan'},
"Date": {'color': 'green'}
}
table_show_manager.add_column(column_info)
# Populate the table with title information
for i, media in enumerate(media_search_manager.media_list):
table_show_manager.add_tv_show({
'Index': str(i),
'Name': media.name,
'Type': media.type,
'Score': media.score,
'Date': media.last_air_date
})
# Run the table and handle user input
last_command = table_show_manager.run(force_int_input=True, max_int_input=len(media_search_manager.media_list))
# Handle user's quit command
if last_command == "q":
console.print("\n[red]Quit [white]...")
sys.exit(0)
# Check if the selected index is within range
if 0 <= int(last_command) <= len(media_search_manager.media_list):
return media_search_manager.get(int(last_command))
else:
console.print("\n[red]Wrong index")
sys.exit(0)

View File

@ -1,226 +0,0 @@
# 3.12.23 -> 10.12.23
# General import
import binascii
import json
import os
import re
import requests
import sys
import urllib
from bs4 import BeautifulSoup
# Class import
from Src.Lib.FFmpeg.my_m3u8 import download_m3u8
from Src.Lib.FFmpeg.util import audio_extractor_m3u8
from Src.Util.config import config
from Src.Util.console import console, msg
from Src.Util.headers import get_headers
def get_info_tv(id_film, title_name, site_version, domain):
req = requests.get(f"https://streamingcommunity.{domain}/titles/{id_film}-{title_name}", headers={
'user-agent': get_headers(),
'X-Inertia': 'true',
'X-Inertia-Version': site_version,
})
if req.ok:
return req.json()['props']['title']['seasons_count']
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
def get_info_season(tv_id, tv_name, domain, version, n_season):
req = requests.get(
f'https://streamingcommunity.{domain}/titles/{tv_id}-{tv_name}/stagione-{n_season}',
headers={
'user-agent': get_headers(),
'x-inertia': 'true',
'x-inertia-version': version,
})
if req.ok:
return [{'id': ep['id'], 'n': ep['number'], 'name': ep['name'] if ep['name'] is not None else ""} for ep in
req.json()['props']['loadedSeason']['episodes']]
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
def get_iframe(tv_id, ep_id, domain):
req = requests.get(f'https://streamingcommunity.{domain}/iframe/{tv_id}',
params={'episode_id': ep_id, 'next_episode': '1'},
headers={'user-agent': get_headers()}
)
if req.ok:
try:
url_embed = BeautifulSoup(req.text, "lxml").find("iframe").get("src")
req_embed = requests.get(url_embed, headers={"User-agent": get_headers()}).text
return BeautifulSoup(req_embed, "lxml").find("body").find("script").text
except:
console.log("[red]Error with episode. Skipping...")
return None
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
def select_quality(json_win_param):
if json_win_param['token1080p']:
return "1080p"
elif json_win_param['token720p']:
return "720p"
elif json_win_param['token480p']:
return "480p"
else:
return "360p"
def parse_content(embed_content):
# Parse parameter from req embed content
win_video = re.search(r"window.video = {.*}", str(embed_content)).group()
win_param = re.search(r"params: {[\s\S]*}", str(embed_content)).group()
# Parse parameter to make read for json
json_win_video = "{" + win_video.split("{")[1].split("}")[0] + "}"
json_win_param = "{" + win_param.split("{")[1].split("}")[0].replace("\n", "").replace(" ", "") + "}"
json_win_param = json_win_param.replace(",}", "}").replace("'", '"')
return json.loads(json_win_video), json.loads(json_win_param), select_quality(json.loads(json_win_param))
def get_playlist(json_win_video, json_win_param, render_quality):
token_render = f"token{render_quality}"
return f"https://vixcloud.co/playlist/{json_win_video['id']}?token={json_win_param['token']}&{token_render}={json_win_param[token_render]}&expires={json_win_param['expires']}"
def get_m3u8_url(json_win_video, json_win_param, render_quality):
token_render = f"token{render_quality}"
return f"https://vixcloud.co/playlist/{json_win_video['id']}?type=video&rendition={render_quality}&token={json_win_param[token_render]}&expires={json_win_param['expires']}"
def get_m3u8_key_ep():
response = requests.get('https://vixcloud.co/storage/enc.key')
if response.ok:
return binascii.hexlify(response.content).decode('utf-8')
else:
console.log(f"[red]Error: {response.status_code}")
sys.exit(0)
def get_m3u8_audio(json_win_video, json_win_param, tv_name, n_season, n_ep, ep_title, token_render):
req = requests.get(f'https://vixcloud.co/playlist/{json_win_video["id"]}',
params={'token': json_win_param['token'],
'expires': json_win_param["expires"]},
headers={
'referer': f'https://vixcloud.co/embed/{json_win_video["id"]}?token={json_win_param[token_render]}&title={tv_name}&referer=1&expires={json_win_param["expires"]}&description=S{n_season}%3AE{n_ep}+{ep_title}&nextEpisode=1'
})
if req.ok:
result = audio_extractor_m3u8(req)
return result
else:
console.log(f"[red]Error: {req.status_code}")
sys.exit(0)
# [func \ main]
def dw_single_ep(tv_id, eps, index_ep_select, domain, tv_name, season_select):
encoded_name = urllib.parse.quote(eps[index_ep_select]['name'])
console.print(
f"[green]Downloading episode: [blue]{eps[index_ep_select]['n']} [green]=> [purple]{eps[index_ep_select]['name']}")
embed_content = get_iframe(tv_id, eps[index_ep_select]['id'], domain)
if embed_content is None:
return
json_win_video, json_win_param, render_quality = parse_content(embed_content)
token_render = f"token{render_quality}"
console.print(f"[blue]Selected quality => [red]{render_quality}")
m3u8_playlist = get_playlist(json_win_video, json_win_param, render_quality)
m3u8_url = get_m3u8_url(json_win_video, json_win_param, render_quality)
m3u8_key = get_m3u8_key_ep()
mp4_name = f"S{str(season_select).zfill(2)}E{str(index_ep_select + 1).zfill(2)}"
mp4_format = f"{mp4_name}.mp4"
season = mp4_name.rsplit("E", 1)[0]
mp4_path = os.path.join(config['root_path'], config['series_folder_name'], mp4_format)
m3u8_url_audio = get_m3u8_audio(json_win_video, json_win_param, tv_name, season_select, index_ep_select + 1,
encoded_name, token_render)
if m3u8_url_audio is not None:
console.print("[blue]Using m3u8 audio => [red]True")
subtitle_path = os.path.join(config['root_path'], config['series_folder_name'], tv_name, season)
download_m3u8(
m3u8_index = m3u8_url,
m3u8_audio = m3u8_url_audio,
m3u8_subtitle = m3u8_playlist,
key = m3u8_key,
output_filename = mp4_path,
subtitle_folder = subtitle_path,
content_name = mp4_name
)
def main_dw_tv(tv_id, tv_name, version, domain):
num_season_find = get_info_tv(tv_id, tv_name, version, domain)
console.print(
"\n[green]Insert season [red]number[green], or [red](*) [green]to download all seasons, or [red][1-2] [green]for a range of seasons")
console.print(f"\n[blue]Season(s) found: [red]{num_season_find}")
season_select = str(msg.ask("\n[green]Insert which season(s) number you'd like to download"))
if "[" in season_select:
start, end = map(int, season_select[1:-1].split('-'))
result = list(range(start, end + 1))
for n_season in result:
eps = get_info_season(tv_id, tv_name, domain, version, n_season)
for ep in eps:
dw_single_ep(tv_id, eps, int(ep['n']) - 1, domain, tv_name, n_season)
print("\n")
elif season_select != "*":
season_select = int(season_select)
if 1 <= season_select <= num_season_find:
eps = get_info_season(tv_id, tv_name, domain, version, season_select)
for ep in eps:
console.print(f"[green]Episode: [blue]{ep['n']} [green]=> [purple]{ep['name']}")
index_ep_select = str(msg.ask(
"\n[green]Insert episode [blue]number[green], or [red](*) [green]to download all episodes, or [red][1-2] [green]for a range of episodes"))
# Download range []
if "[" in index_ep_select:
start, end = map(int, index_ep_select[1:-1].split('-'))
result = list(range(start, end + 1))
for n_range_ep in result:
# index_ep_select = int(n_range_ep) # Unused
dw_single_ep(tv_id, eps, n_range_ep - 1, domain, tv_name, season_select)
# Download single ep
elif index_ep_select != "*":
if 1 <= int(index_ep_select) <= len(eps):
index_ep_select = int(index_ep_select) - 1
dw_single_ep(tv_id, eps, index_ep_select, domain, tv_name, season_select)
else:
console.print("[red]Wrong [yellow]INDEX [red]for the selected Episode")
# Download all
else:
for ep in eps:
dw_single_ep(tv_id, eps, int(ep['n']) - 1, domain, tv_name, season_select)
print("\n")
else:
console.print("[red]Wrong [yellow]INDEX for the selected Season")
else:
for n_season in range(1, num_season_find + 1):
eps = get_info_season(tv_id, tv_name, domain, version, n_season)
for ep in eps:
dw_single_ep(tv_id, eps, int(ep['n']) - 1, domain, tv_name, n_season)
print("\n")

View File

@ -1,65 +0,0 @@
# 24.01.2023
# Class
from Src.Util.console import console
# Import
import subprocess, os, requests, zipfile, sys, ctypes, os, sys
# Variable
# [ func ]
def isAdmin():
try:
is_admin = (os.getuid() == 0)
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
return is_admin
def download_ffmpeg():
# Specify the URL for the FFmpeg binary zip file for Windows
ffmpeg_url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z"
# Name of the directory where FFmpeg will be extracted
ffmpeg_dir = "ffmpeg"
console.print("[yellow]Downloading FFmpeg...[/yellow]")
# Download the FFmpeg zip file
response = requests.get(ffmpeg_url)
os.makedirs(ffmpeg_dir, exist_ok=True)
# Save the zip file
zip_file_path = os.path.join(ffmpeg_dir, "ffmpeg.zip")
with open(zip_file_path, "wb") as zip_file:
zip_file.write(response.content)
with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
zip_ref.extractall(ffmpeg_dir)
# Add the FFmpeg directory to the system PATH
ffmpeg_bin_dir = os.path.join(os.getcwd(), ffmpeg_dir, "bin")
os.environ["PATH"] += os.pathsep + ffmpeg_bin_dir
os.remove(zip_file_path)
def check_ffmpeg():
console.print("[green]Checking ffmpeg ...")
try:
subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
console.print("[blue]FFmpeg [white]=> [red]Find")
except:
try:
console.print("[cyan]FFmpeg is not in the PATH. Downloading and adding to the PATH...[/cyan]")
if not isAdmin():
console.log("[red]You need admin privileges to proceed!")
sys.exit(0)
download_ffmpeg()
sys.exit(0)
except:
console.print("[red]Unable to download or add FFmpeg to the PATH.[/red]")
sys.exit(0)

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +0,0 @@
# 31.01.24
# Class
from Src.Util.console import console
# Import
import ffmpeg
# Variable
# [ func ]
def get_video_duration(file_path):
try:
probe = ffmpeg.probe(file_path)
duration = float(probe['format']['duration'])
return duration
except ffmpeg.Error as e:
print(f"Error: {e.stderr}")
return None
def format_duration(seconds):
hours, remainder = divmod(seconds, 3600)
minutes, seconds = divmod(remainder, 60)
return int(hours), int(minutes), int(seconds)
def print_duration_table(file_path):
video_duration = get_video_duration(file_path)
if video_duration is not None:
hours, minutes, seconds = format_duration(video_duration)
console.log(
f"[cyan]Info [green]'{file_path}': [purple]{int(hours)}[red]h [purple]{int(minutes)}[red]m [purple]{int(seconds)}[red]s")
def audio_extractor_m3u8(req):
m3u8_cont = req.text.split()
m3u8_cont_arr = []
for row in m3u8_cont:
if "audio" in str(row):
lang = None
default = False
for field in row.split(","):
if "NAME" in field:
lang = field.split('"')[-2]
if "DEFAULT" in field:
default_str = field.split('=')[1]
default = default_str.strip() == "YES"
audioobj = {"url": row.split(",")[-1].split('"')[-2], "lang": lang, "default": default}
if audioobj['lang'] is None:
continue
m3u8_cont_arr.append(audioobj)
return m3u8_cont_arr or None

3
Src/Upload/__init__.py Normal file
View File

@ -0,0 +1,3 @@
# 01.03.24
from .update import update

View File

@ -1,5 +1,5 @@
__title__ = 'Streaming_community'
__version__ = 'v0.9.2'
__version__ = 'v1.0.0'
__author__ = 'Ghost6446'
__description__ = 'A command-line program to download film'
__license__ = 'MIT License'

View File

@ -1,47 +1,73 @@
# 13.09.2023
# 01.03.2023
# Class import
from Src.Util.console import console
# General import
import os, requests, time
import os
import requests
import time
# Variable
repo_name = "StreamingCommunity_api"
repo_user = "ghost6446"
main = os.path.abspath(os.path.dirname(__file__))
def get_install_version():
"""
Get the installed version from the '__version__.py' file.
"""
about = {}
with open(os.path.join(main, '__version__.py'), 'r', encoding='utf-8') as f:
version_file_path = os.path.join(main, '__version__.py')
with open(version_file_path, 'r', encoding='utf-8') as f:
exec(f.read(), about)
return about['__version__']
def main_update():
console.print("[green]Checking GitHub version ...")
def update():
"""
Check for updates on GitHub and display relevant information.
"""
json = requests.get(f"https://api.github.com/repos/{repo_user}/{repo_name}/releases").json()[0]
stargazers_count = requests.get(f"https://api.github.com/repos/{repo_user}/{repo_name}").json()['stargazers_count']
console.print("[green]Checking GitHub version [white]...")
last_version = json['name']
down_count = json['assets'][0]['download_count']
# Make the GitHub API requests and handle potential errors
try:
repo_info = requests.get(f"https://api.github.com/repos/{repo_user}/{repo_name}").json()
release_info = requests.get(f"https://api.github.com/repos/{repo_user}/{repo_name}/releases").json()[0]
except requests.RequestException as e:
console.print(f"[red]Error accessing GitHub API: {e}")
return
if down_count > 0 and stargazers_count > 0:
# Get start of the reposity
stargazers_count = repo_info['stargazers_count']
# Find info about latest versione deploy and the donwload count
last_version = release_info['name']
down_count = release_info['assets'][0]['download_count']
# Calculate percentual of start base on download count
if down_count > 0 and stargazers_count > 0:
percentual_stars = round(stargazers_count / down_count * 100, 2)
else:
else:
percentual_stars = 0
if get_install_version() != last_version:
console.print(f"[red]=> A new version is available: [green]{json['zipball_url']}")
console.print(f"[red]=> New Version: [yellow]{json['name']}")
else:
console.print(f"[red]=> Everything is up to date")
console.print(f"[red]=> You're on Version: [yellow]{json['name']}")
installed_version = get_install_version()
# Check installed version
if installed_version != last_version:
console.print(f"[red]Version: [yellow]{last_version}")
else:
console.print(f"[red]Everything up to date")
print("\n")
console.print(f"[red]{repo_name} was downloaded [yellow]{down_count} [red]times, but only [yellow]{percentual_stars}% [red]of You(!!) have starred it. \n\
console.print(f"[red]{repo_name} was downloaded [yellow]{down_count} [red]times, but only [yellow]{percentual_stars}% [red]of You(!!) have starred it.\n\
[cyan]Help the repository grow today, by leaving a [yellow]star [cyan]and [yellow]sharing [cyan]it to others online!")
time.sleep(3)
print("\n")
print("\n")

View File

@ -1,22 +1,171 @@
import json, os
# 29.01.24
import json
import os
from typing import Any, List
class ConfigManager:
def __init__(self, file_path):
def __init__(self, file_path: str = 'config.json') -> None:
"""Initialize the ConfigManager.
Args:
file_path (str, optional): The path to the configuration file. Default is 'config.json'.
"""
self.file_path = file_path
self.config = {}
self.cache = {}
def load_config(self):
with open(self.file_path, 'r') as file:
config_file = json.load(file)
return config_file
def read_config(self) -> None:
"""Read the configuration file."""
try:
if os.path.exists(self.file_path):
with open(self.file_path, 'r') as f:
self.config = json.load(f)
except Exception as e:
print(f"Error reading configuration file: {e}")
def update_config(self, key, new_value):
config = self.load_config()
config[key] = new_value
with open(self.file_path, 'w') as file:
json.dump(config, file, indent=4)
def read_key(self, section: str, key: str, data_type: type = str) -> Any:
"""Read a key from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
data_type (type, optional): The expected data type of the key's value. Default is str.
Returns:
The value of the key converted to the specified data type.
"""
cache_key = f"{section}.{key}"
if cache_key in self.cache:
return self.cache[cache_key]
if section in self.config and key in self.config[section]:
value = self.config[section][key]
else:
raise ValueError(f"Key '{key}' not found in section '{section}'")
value = self._convert_to_data_type(value, data_type)
self.cache[cache_key] = value
return value
def _convert_to_data_type(self, value: str, data_type: type) -> Any:
"""Convert the value to the specified data type.
Args:
value (str): The value to be converted.
data_type (type): The expected data type.
Returns:
The value converted to the specified data type.
"""
if data_type == int:
return int(value)
elif data_type == bool:
return bool(value)
elif data_type == list:
return value if isinstance(value, list) else [item.strip() for item in value.split(',')]
elif data_type == type(None):
return None
else:
return value
def get(self, section: str, key: str) -> Any:
"""Read a value from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
Returns:
The value associated with the key.
"""
return self.read_key(section, key)
def get_int(self, section: str, key: str) -> int:
"""Read an integer value from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
Returns:
int: The integer value.
"""
return self.read_key(section, key, int)
def get_float(self, section: str, key: str) -> int:
"""Read an float value from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
Returns:
float: The float value.
"""
return self.read_key(section, key, float)
def get_bool(self, section: str, key: str) -> bool:
"""Read a boolean value from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
Returns:
bool: The boolean value.
"""
return self.read_key(section, key, bool)
def get_list(self, section: str, key: str) -> List[str]:
"""Read a list value from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
Returns:
list: The list value.
"""
return self.read_key(section, key, list)
def get_dict(self, section: str, key: str) -> dict:
"""Read a dictionary value from the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be read.
Returns:
dict: The dictionary value.
"""
return self.read_key(section, key, dict)
def set_key(self, section: str, key: str, value: Any) -> None:
"""Set a key in the configuration file.
Args:
section (str): The section in the configuration file.
key (str): The key to be set.
value (Any): The value to be associated with the key.
"""
try:
if section not in self.config:
self.config[section] = {}
self.config[section][key] = value
cache_key = f"{section}.{key}"
self.cache[cache_key] = value
self.write_config()
except Exception as e:
print(f"Error setting key '{key}' in section '{section}': {e}")
def write_config(self) -> None:
"""Write the configuration to the file."""
try:
with open(self.file_path, 'w') as f:
json.dump(self.config, f, indent=4)
except Exception as e:
print(f"Error writing configuration file: {e}")
# Example usage:
config_path = os.path.join('config.json')
config_manager = ConfigManager(config_path)
config = config_manager.load_config()
# Initialize
config_manager = ConfigManager()
config_manager.read_config()

View File

@ -1,10 +1,9 @@
# 17.09.2023 -> 3.12.23
# 24.02.24
# Import
from rich.console import Console
from rich.prompt import Prompt
import logging
# Variable
msg = Prompt()
console = Console()
console = Console()

View File

@ -1,4 +1,4 @@
# 3.12.23 -> 10.12.23
# 3.12.23 -> 10.12.23 -> 20.03.24
# Import
import fake_useragent
@ -6,5 +6,13 @@ import fake_useragent
# Variable
useragent = fake_useragent.UserAgent(use_external_data=True)
def get_headers():
def get_headers() -> str:
"""
Generate a random user agent to use in HTTP requests.
Returns:
- str: A random user agent string.
"""
# Get a random user agent string from the user agent rotator
return useragent.firefox

52
Src/Util/logger.py Normal file
View File

@ -0,0 +1,52 @@
# 26.03.24
# Class import
from Src.Util.config import config_manager
# Import
import logging
from logging.handlers import RotatingFileHandler
class Logger:
def __init__(self):
"""
Initialize the Logger class.
"""
# Fetching configuration values
self.DEBUG_MODE = config_manager.get_bool("DEFAULT", "debug")
self.log_to_file = config_manager.get_bool("DEFAULT", "log_to_file")
self.log_file = config_manager.get("DEFAULT", "log_file") if self.log_to_file else None
# Setting logging level based on DEBUG_MODE
if self.DEBUG_MODE:
self.level = logging.DEBUG
# Configure file logging if debug mode and logging to file are both enabled
if self.log_to_file:
self.configure_file_logging()
else:
# If DEBUG_MODE is False, set logging level to ERROR
self.level = logging.ERROR
# Configure console logging
self.configure_logging()
def configure_logging(self):
"""
Configure console logging.
"""
logging.basicConfig(level=self.level, format='[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
def configure_file_logging(self):
"""
Configure file logging if enabled.
"""
file_handler = RotatingFileHandler(self.log_file, maxBytes=10*1024*1024, backupCount=5)
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logging.getLogger('').addHandler(file_handler)

View File

@ -1,9 +1,30 @@
# 3.12.23
# 3.12.23 -> 19.07.24
# Import
# Class import
from .config import config_manager
from Src.Util.console import console
def msg_start():
# Import
import os
import platform
# Variable
CLEAN = config_manager.get_bool('DEFAULT', 'clean_console')
SHOW = config_manager.get_bool('DEFAULT', 'show_message')
def get_os_system():
"""
This function returns the name of the operating system.
"""
os_system = platform.system()
return os_system
def start_message():
"""
Display a start message.
This function prints a formatted start message, including a title and creator information.
"""
msg = """
@ -18,4 +39,15 @@ def msg_start():
"""
console.print(f"[purple]{msg}")
if CLEAN:
if get_os_system() == 'Windows':
os.system("cls")
else:
os.system("clear")
if SHOW:
console.print(f"[bold yellow]{msg}")
console.print(f"[magenta]Created by: Ghost6446\n")
row = "-" * console.width
console.print(f"[yellow]{row} \n")

View File

@ -1,17 +1,35 @@
# 24.01.24
# Import
import shutil, os, time
import shutil
import os
import time
import json
import hashlib
import logging
def remove_folder(folder_path: str) -> None:
"""
Remove a folder if it exists.
Parameters:
- folder_path (str): The path to the folder to be removed.
"""
def remove_folder(folder_path):
if os.path.exists(folder_path):
try:
shutil.rmtree(folder_path)
except OSError as e:
print(f"Error removing folder '{folder_path}': {e}")
def remove_file(file_path: str) -> None:
"""
Remove a file if it exists
def remove_file(file_path):
Parameters:
- file_path (str): The path to the file to be removed.
"""
if os.path.exists(file_path):
time.sleep(1)
@ -19,5 +37,160 @@ def remove_file(file_path):
os.remove(file_path)
except OSError as e:
print(f"Error removing file '{file_path}': {e}")
else:
print(f"File '{file_path}' does not exist.")
#else:
# print(f"File '{file_path}' does not exist.")
def move_file_one_folder_up(file_path):
"""
Move a file one folder up from its current location.
Args:
file_path (str): Path to the file to be moved.
"""
# Get the directory of the file
file_directory = os.path.dirname(file_path)
# Get the parent directory
parent_directory = os.path.dirname(file_directory)
# Get the filename
filename = os.path.basename(file_path)
# New path for the file one folder up
new_path = os.path.join(parent_directory, filename)
# Move the file
os.rename(file_path, new_path)
def read_json(path: str):
"""Reads JSON file and returns its content.
Args:
path (str): The file path of the JSON file to read.
Returns:
variable: The content of the JSON file as a dictionary.
"""
with open(path, "r") as file:
config = json.load(file)
return config
def save_json(json_obj, path: str) -> (None):
"""Saves JSON object to the specified file path.
Args:
json_obj (Dict[str, Any]): The JSON object to be saved.
path (str): The file path where the JSON object will be saved.
"""
with open(path, 'w') as file:
json.dump(json_obj, file, indent=4) # Adjust the indentation as needed
def clean_json(path: str) -> (None):
"""Reads JSON data from the file, cleans it, and saves it back.
Args:
path (str): The file path of the JSON file to clean.
"""
data = read_json(path)
# Recursively replace all values with an empty string
def recursive_empty_string(obj):
if isinstance(obj, dict):
return {key: recursive_empty_string(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [recursive_empty_string(item) for item in obj]
else:
return ""
modified_data = recursive_empty_string(data)
# Save the modified JSON data back to the file
save_json(modified_data, path)
def format_size(size_bytes: float):
"""
Format the size in bytes into a human-readable format.
Args:
size_bytes (float): The size in bytes to be formatted.
Returns:
str: The formatted size.
"""
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 compute_sha1_hash(input_string: str) -> (str):
"""
Computes the SHA-1 hash of the input string.
Args:
input_string (str): The string to be hashed.
Returns:
str: The SHA-1 hash of the input string.
"""
# Compute the SHA-1 hash
hashed_string = hashlib.sha1(input_string.encode()).hexdigest()
# Return the hashed string
return hashed_string
def decode_bytes(bytes_data: bytes, encodings_to_try: list[str] = None) -> (str):
"""
Decode a byte sequence using a list of encodings and return the decoded string.
Args:
bytes_data (bytes): The byte sequence to decode.
encodings_to_try (List[str], optional): A list of encoding names to try for decoding.
If None, defaults to ['utf-8', 'latin-1', 'ascii'].
Returns:
str or None: The decoded string if successful, None if decoding fails.
"""
if encodings_to_try is None:
encodings_to_try = ['utf-8', 'latin-1', 'ascii']
for encoding in encodings_to_try:
try:
# Attempt decoding with the current encoding
string_data = bytes_data.decode(encoding)
logging.info("Decoded successfully with encoding: %s", encoding)
logging.info("Decoded string: %s", string_data)
return string_data
except UnicodeDecodeError:
continue # Try the next encoding if decoding fails
# If none of the encodings work, treat it as raw bytes
logging.warning("Unable to decode the data as text. Treating it as raw bytes.")
logging.info("Raw byte data: %s", bytes_data)
return None
def convert_to_hex(bytes_data: bytes) -> str:
"""
Convert a byte sequence to its hexadecimal representation.
Args:
bytes_data (bytes): The byte sequence to convert.
Returns:
str: The hexadecimal representation of the byte sequence.
"""
hex_data = ''.join(['{:02x}'.format(char) for char in bytes_data])
logging.info("Hexadecimal representation of the data: %s", hex_data)
return hex_data

149
Src/Util/table.py Normal file
View File

@ -0,0 +1,149 @@
# 03.03.24
# Class import
from .message import start_message
# Import
from rich.console import Console
from rich.table import Table
from rich.text import Text
from rich.prompt import Prompt
from rich.style import Style
from typing import Dict, List, Any
class TVShowManager:
def __init__(self):
"""
Initialize TVShowManager with provided column information.
Args:
column_info (Dict[str, Dict[str, str]]): Dictionary containing column names, their colors, and justification.
"""
self.console = Console()
self.tv_shows: List[Dict[str, Any]] = [] # List to store TV show data as dictionaries
self.slice_start: int = 0
self.slice_end: int = 5
self.step: int = self.slice_end
self.column_info = []
def set_slice_end(self, new_slice: int) -> None:
"""
Set the end of the slice for displaying TV shows.
Args:
new_slice (int): The new value for the slice end.
"""
self.slice_end = new_slice
self.step = new_slice
def add_column(self, column_info: Dict[str, Dict[str, str]]) -> None:
"""
Add column information.
Args:
column_info (Dict[str, Dict[str, str]]): Dictionary containing column names, their colors, and justification.
"""
self.column_info = column_info
def add_tv_show(self, tv_show: Dict[str, Any]):
"""
Add a TV show to the list of TV shows.
Args:
tv_show (Dict[str, Any]): Dictionary containing TV show details.
"""
self.tv_shows.append(tv_show)
def display_data(self, data_slice: List[Dict[str, Any]]):
"""
Display TV show data in a tabular format.
Args:
data_slice (List[Dict[str, Any]]): List of dictionaries containing TV show details to display.
"""
table = Table(title=Text("Show Details", justify="center", style="bold magenta"), border_style="white")
# Add columns dynamically based on provided column information
for col_name, col_style in self.column_info.items():
color = col_style.get("color", None)
if color:
style = Style(color=color)
else:
style = None
table.add_column(col_name, style=style, justify='center')
# Add rows dynamically based on available TV show data
for entry in data_slice:
row_data = [entry[col_name] for col_name in self.column_info.keys()]
table.add_row(*row_data)
self.console.print(table) # Use self.console.print instead of print
def run(self, force_int_input: bool = False, max_int_input: int = 0) -> str:
"""
Run the TV show manager application.
Args:
- force_int_input(bool): If True, only accept integer inputs from 0 to max_int_input
- max_int_input (int):
Returns:
- str: Last command executed before breaking out of the loop.
"""
total_items = len(self.tv_shows)
last_command = "" # Variable to store the last command executed
while True:
start_message()
# Display table
self.display_data(self.tv_shows[self.slice_start:self.slice_end])
# Handling user input for loading more items or quitting
if self.slice_end < total_items:
self.console.print(f"\n\n[yellow][INFO] [green]Press [red]Enter [green]to restart, or [red]'q' [green]to quit.")
if not force_int_input:
key = Prompt.ask("[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]for a range of media")
else:
choices = [str(i) for i in range(0, max_int_input)]
choices.append("")
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
last_command = key
if key.lower() == "q":
break
elif key == "":
self.slice_start += self.slice_end
self.slice_end += self.slice_end
if self.slice_end > total_items:
self.slice_end = total_items
else:
break
else:
self.console.print(f"\n\n[yellow][INFO] [red]You've reached the end. [green]Press [red]Enter [green]to restart, or [red]'q' [green]to quit.")
if not force_int_input:
key = Prompt.ask("[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]for a range of media")
else:
choices = [str(i) for i in range(0, max_int_input)]
choices.append("")
key = Prompt.ask("[cyan]Insert media [red]index", choices=choices, show_choices=False)
last_command = key
if key.lower() == "q":
break
elif key == "":
self.slice_start = 0
self.slice_end = self.step
else:
break
return last_command

View File

@ -1,10 +1,55 @@
{
"root_path": "videos",
"movies_folder_name": "Movies",
"series_folder_name": "Series",
"download_subtitles": true,
"download_default_language": true,
"selected_language": "English",
"max_worker": 20,
"domain": "forum"
"DEFAULT": {
"debug": false,
"log_file": "debug.txt",
"log_to_file": true,
"get_info": false,
"show_message": true,
"clean_console": true,
"bypass_ffmpeg": true,
"bypass_github": true,
"get_moment_title": false,
"root_path": "Video",
"movies_folder_name": "Movies",
"series_folder_name": "Series",
"anime_folder_name": "Anime",
"not_close": false,
"swith_anime": false
},
"SITE": {
"streaming_site_name": "streamingcommunity",
"streaming_domain": "forum",
"anime_site_name": "animeunity",
"anime_domain": "to"
},
"M3U8": {
"tdqm_workers": 20,
"tqdm_progress_timeout": 10,
"minium_ts_files_in_folder": 15,
"donwload_percentage": 1,
"requests_timeout": 5,
"enable_time_quit": false,
"tqdm_show_progress": false,
"cleanup_tmp_folder": true
},
"M3U8_OPTIONS": {
"download_audio": true,
"download_subtitles": true,
"specific_list_audio": [
"ita"
],
"specific_list_subtitles": [
"eng"
],
"request": {
"index": {
"authority": "vixcloud.co",
"user-agent": ""
},
"segments": {
"Origin": "https://vixcloud.co",
"user-agent": ""
}
}
}
}

Binary file not shown.

215
run.py
View File

@ -1,131 +1,166 @@
# 10.12.23 -> 1.02.24
# 10.12.23 -> 31.01.24
# Class
import Src.Api.page as Page
from Src.Api.film import main_dw_film as download_film
from Src.Api.tv import main_dw_tv as download_tv
from Src.Util.message import msg_start
from Src.Api import (
get_version_and_domain,
download_series,
download_film,
search,
anime_search,
anime_download_series,
anime_download_film,
get_select_title
)
from Src.Util.message import start_message
from Src.Util.console import console, msg
from Src.Util.os import remove_folder
from Src.Upload.update import main_update
from Src.Lib.FFmpeg.installer import check_ffmpeg
from Src.Util.config import config_manager
from Src.Util.os import remove_folder, remove_file
from Src.Upload.update import update as git_update
from Src.Lib.FFmpeg import check_ffmpeg
from Src.Util.logger import Logger
# Import
import sys, platform
import sys
import logging
import platform
# Variable
DEBUG_MODE = config_manager.get_bool("DEFAULT", "debug")
DEBUG_GET_ALL_INFO = config_manager.get_bool('DEFAULT', 'get_info')
SWITCH_TO = config_manager.get_bool('DEFAULT', 'swith_anime')
CLOSE_CONSOLE = config_manager.get_bool('DEFAULT', 'not_close')
# [ main ]
def initialize():
"""
Initializes the application by performing necessary setup tasks.
Initialize the application.
Checks Python version, removes temporary folder, and displays start message.
"""
# Get system where script is run
run_system = platform.system()
# Checking Python version
# Enable debug with info
if DEBUG_MODE:
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('root').setLevel(logging.INFO)
else:
logging.basicConfig(level=logging.ERROR)
logging.getLogger('root').setLevel(logging.ERROR)
if sys.version_info < (3, 11):
console.log("Install python version > 3.11")
sys.exit(0)
# Removing temporary folder
remove_folder("tmp")
msg_start()
remove_file("debug.log")
start_message()
# Attempting GitHub update
try:
# Updating application
main_update()
git_update()
except Exception as e:
console.print(f"[blue]Request GitHub [white]=> [red]Failed: {e}")
console.print(f"[blue]Req github [white]=> [red]Failed: {e}")
# Checking FFmpeg installation
if run_system != 'Windows':
# Checking ffmpeg availability ( only win )
if run_system == 'Windows':
check_ffmpeg()
print("\n")
def main():
"""
Main function to execute the application logic.
Main function of the application.
"""
# Initializing the application
# Get site domain and version
initialize()
site_version, domain = get_version_and_domain()
# Make request to site to get content that corrsisponde to that string
film_search = msg.ask("\n[cyan]Insert word to search in all site: ").strip()
len_database = search(film_search, domain)
if len_database != 0:
# Select title from list
select_title = get_select_title()
# For series
if select_title.type == 'tv':
download_series(
tv_id=select_title.id,
tv_name=select_title.slug,
version=site_version,
domain=domain
)
# For film
else:
download_film(
id_film=select_title.id,
title_name=select_title.slug,
domain=domain
)
# If no media find
else:
console.print("[red]Cant find a single element")
# End
console.print("\n[red]Done")
def main_switch():
"""
Main function for anime unity
"""
# Get site domain and version
initialize()
# Retrieving domain and site version
domain, site_version = Page.domain_version()
# Make request to site to get content that corrsisponde to that string
film_search = msg.ask("\n[cyan]Insert word to search in all site: ").strip()
len_database = anime_search(film_search)
# Searching for movie or TV series title
film_search = msg.ask("\n[blue]Search for any Movie or TV Series title").strip()
db_title = Page.search(film_search, domain)
Page.display_search_results(db_title)
if len_database != 0:
if db_title:
# Select title from list
select_title = get_select_title()
# For series
if select_title.type == 'TV':
anime_download_series(
tv_id=select_title.id,
tv_name=select_title.slug
)
# For film
else:
anime_download_film(
id_film=select_title.id,
title_name=select_title.slug
)
# Displaying total results
console.print(f"\n[blue]Total result: {len(db_title)}")
# Asking user to select title(s) to download
console.print(
"\n[green]Insert [yellow]INDEX [red]number[green], or [red][1-2] [green]for a range of movies/tv series, or [red][1,3,5] [green]to select discontinued movie/tv series"
)
console.print("\n[red]In case of a TV Series you will also choose seasons and episodes to download")
index_select = str(msg.ask("\n[blue]Select [yellow]INDEX [blue]to download")).strip()
# For only number ( to fix )
if index_select.isnumeric():
index_select = int(index_select)
if 0 <= index_select <= len(db_title) - 1:
selected_title = db_title[index_select]
if selected_title['type'] == "movie":
console.print(f"[green]\nSelected Movie: {selected_title['name']}")
download_film(selected_title['id'], selected_title['slug'], domain)
else:
console.print(f"[green]\nSelected TV Series: {selected_title['name']}")
download_tv(selected_title['id'], selected_title['slug'], site_version, domain)
else:
console.print("[red]Wrong INDEX for selection")
# For range like [5-15] ( to fix )
elif "[" in index_select:
if "-" in index_select:
start, end = map(int, index_select[1:-1].split('-'))
result = list(range(start, end + 1))
for n in result:
selected_title = db_title[n]
if selected_title['type'] == "movie":
console.print(f"[green]\nSelected Movie: {selected_title['name']}")
download_film(selected_title['id'], selected_title['slug'], domain)
else:
console.print(f"[green]\nSelected TV Series: {selected_title['name']}")
download_tv(selected_title['id'], selected_title['slug'], site_version, domain)
# For a list of specific ( to fix )
elif "," in index_select:
result = list(map(int, index_select[1:-1].split(',')))
for n in result:
selected_title = db_title[n]
if selected_title['type'] == "movie":
console.print(f"[green]\nSelected Movie: {selected_title['name']}")
download_film(selected_title['id'], selected_title['slug'], domain)
else:
console.print(f"[green]\nSelected TV Series: {selected_title['name']}")
download_tv(selected_title['id'], selected_title['slug'], site_version, domain)
else:
console.print("[red]Wrong INDEX for selection")
# If no media find
else:
console.print("[red]Couldn't find any entries for the selected title")
console.print("[red]Cant find a single element")
console.print("[red]Done!")
if __name__ == '__main__':
main()
logger = Logger()
while 1:
cmd_insert = str(msg.ask("[red]Quit the script ? [red][[yellow]yes[red] / [yellow]no[red]]"))
if cmd_insert in ['y', 'yes', 'ye']:
break
else:
if not SWITCH_TO:
if not CLOSE_CONSOLE:
main()
else:
while 1:
main()
else:
if not CLOSE_CONSOLE:
main_switch()
else:
while 1:
main_switch()

135
update.py
View File

@ -1,6 +1,9 @@
# 10.12.24
import requests, os, shutil
# General imports
import requests
import os
import shutil
from zipfile import ZipFile
from io import BytesIO
from rich.console import Console
@ -9,86 +12,70 @@ from rich.console import Console
console = Console()
local_path = os.path.join(".")
def move_content(source: str, destination: str) -> None:
def move_content(source: str, destination: str) :
"""
Recursively moves content from source directory to destination directory.
Move all content from the source folder to the destination folder.
Args:
source (str): Path to the source directory.
destination (str): Path to the destination directory.
Returns:
None
source (str): The path to the source folder.
destination (str): The path to the destination folder.
"""
os.makedirs(destination, exist_ok=True)
# Iterate through all elements in the source folder
for element in os.listdir(source):
source_path = os.path.join(source, element)
destination_path = os.path.join(destination, element)
# If it's a directory, recursively call the function
if os.path.isdir(source_path):
move_content(source_path, destination_path)
# Otherwise, move the file, replacing if it already exists
else:
shutil.move(source_path, destination_path)
def delete_files_folders(main_directory_path: str, folders_to_exclude: list = [], files_to_exclude: list = []) -> None:
def keep_specific_items(directory: str, keep_folder: str, keep_file: str):
"""
Deletes files and folders from the specified directory except those specified.
Delete all items in the directory except for the specified folder and file.
Args:
main_directory_path (str): Path to the main directory.
folders_to_exclude (list): List of folder names to exclude from deletion.
files_to_exclude (list): List of file names to exclude from deletion.
Returns:
None
directory (str): The path to the directory.
keep_folder (str): The name of the folder to keep.
keep_file (str): The name of the file to keep.
"""
for root, dirs, files in os.walk(main_directory_path, topdown=False):
for name in files:
file_path = os.path.join(root, name)
if name not in files_to_exclude:
try:
os.remove(file_path)
except:
pass
for name in dirs:
dir_path = os.path.join(root, name)
if name not in folders_to_exclude:
try:
os.rmdir(dir_path)
except:
pass
def list_files_and_folders(directory: str, files_to_remove: list = []) -> None:
"""
Lists files and folders in the specified directory and removes those specified.
Args:
directory (str): Path to the directory to list files and folders.
files_to_remove (list): List of file names to remove.
Returns:
None
"""
try:
for root, dirs, files in os.walk(directory):
for file_name in files:
file_path = os.path.join(root, file_name)
if file_name in files_to_remove:
os.remove(file_path)
except Exception as e:
print(f"Error occurred: {e}")
if not os.path.exists(directory) or not os.path.isdir(directory):
raise ValueError(f"Error: '{directory}' is not a valid directory.")
def download_and_extract_latest_commit(author: str, repo_name: str, exclude_files: list) -> None:
# Iterate through items in the directory
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
# Check if the item is the specified folder or file
if os.path.isdir(item_path) and item != keep_folder:
shutil.rmtree(item_path)
elif os.path.isfile(item_path) and item != keep_file:
os.remove(item_path)
except PermissionError as pe:
print(f"PermissionError: {pe}. Check permissions and try running the script with admin privileges.")
except Exception as e:
print(f"Error: {e}")
def download_and_extract_latest_commit(author: str, repo_name: str):
"""
Downloads and extracts the latest commit from a GitHub repository.
Download and extract the latest commit from a GitHub repository.
Args:
author (str): The username of the repository owner.
repo_name (str): The name of the repository.
exclude_files (list): List of file names to exclude from extraction.
Returns:
None
author (str): The owner of the GitHub repository.
repo_name (str): The name of the GitHub repository.
"""
# Get the latest commit information using GitHub API
api_url = f'https://api.github.com/repos/{author}/{repo_name}/commits?per_page=1'
response = requests.get(api_url)
console.log("[green]Making a request to GitHub repository...")
@ -98,48 +85,54 @@ def download_and_extract_latest_commit(author: str, repo_name: str, exclude_file
commit_sha = commit_info['sha']
zipball_url = f'https://github.com/{author}/{repo_name}/archive/{commit_sha}.zip'
console.log("[green]Getting zip file from repository...")
# Download the zipball
response = requests.get(zipball_url)
# Extract the content of the zipball into a temporary folder
temp_path = os.path.join(os.path.dirname(os.getcwd()), 'temp_extracted')
with ZipFile(BytesIO(response.content)) as zip_ref:
zip_ref.extractall(temp_path)
console.log("[green]Extracting file ...")
list_files_and_folders(temp_path, exclude_files)
# Move files from the temporary folder to the current folder
for item in os.listdir(temp_path):
item_path = os.path.join(temp_path, item)
destination_path = os.path.join(local_path, item)
shutil.move(item_path, destination_path)
# Remove the temporary folder
shutil.rmtree(temp_path)
# Move all folder to main folder
new_folder_name = f"{repo_name}-{commit_sha}"
move_content(new_folder_name, ".")
# Remove old temp folder
shutil.rmtree(new_folder_name)
console.log(f"[cyan]Latest commit downloaded and extracted successfully.")
else:
console.log(f"[red]Failed to fetch commit information. Status code: {response.status_code}")
def main_upload() -> None:
def main_upload():
"""
Main function to upload the latest commit of a GitHub repository.
"""
Main function to upload the latest changes from a GitHub repository.
Returns:
None
"""
repository_owner = 'Ghost6446'
repository_name = 'StreamingCommunity_api'
cmd_insert = input("Are you sure you want to delete all files? (Only videos folder will remain) [yes/no]: ")
if cmd_insert.lower() == "yes" or cmd_insert.lower() == "y":
delete_files_folders(
main_directory_path=".",
folders_to_exclude=["videos"],
files_to_exclude=["upload.py", "config.json"]
)
download_and_extract_latest_commit(repository_owner, repository_name, ["config.json"])
if cmd_insert == "yes":
# Remove all old file
keep_specific_items(".", "videos", "upload.py")
download_and_extract_latest_commit(repository_owner, repository_name)
main_upload()
# win
# pyinstaller --onefile --add-data "./Src/upload/__version__.py;Src/upload" run.py