mirror of
https://github.com/Arrowar/StreamingCommunity.git
synced 2025-06-06 11:35:29 +00:00
Un giorno ...
This commit is contained in:
parent
d82133f381
commit
cd3001f6b2
137
.gitignore
vendored
137
.gitignore
vendored
@ -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
131
README.md
@ -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.
|
||||
|
60
Src/Api/Class/EpisodeType.py
Normal file
60
Src/Api/Class/EpisodeType.py
Normal 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)
|
80
Src/Api/Class/SearchType.py
Normal file
80
Src/Api/Class/SearchType.py
Normal 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)
|
||||
|
||||
|
62
Src/Api/Class/SeriesType.py
Normal file
62
Src/Api/Class/SeriesType.py
Normal 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
326
Src/Api/Class/Video.py
Normal 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
|
42
Src/Api/Class/WindowType.py
Normal file
42
Src/Api/Class/WindowType.py
Normal 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', '')
|
4
Src/Api/Class/__init__.py
Normal file
4
Src/Api/Class/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# 03.03.24
|
||||
|
||||
from .Video import VideoSource
|
||||
from .SearchType import MediaManager, MediaItem
|
6
Src/Api/__init__.py
Normal file
6
Src/Api/__init__.py
Normal 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
244
Src/Api/anime.py
Normal 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)
|
||||
|
162
Src/Api/film.py
162
Src/Api/film.py
@ -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
|
||||
|
@ -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
209
Src/Api/series.py
Normal 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
369
Src/Api/site.py
Normal 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)
|
226
Src/Api/tv.py
226
Src/Api/tv.py
@ -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")
|
@ -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
@ -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
3
Src/Upload/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# 01.03.24
|
||||
|
||||
from .update import update
|
@ -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'
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
@ -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
52
Src/Util/logger.py
Normal 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)
|
@ -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")
|
183
Src/Util/os.py
183
Src/Util/os.py
@ -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
149
Src/Util/table.py
Normal 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
|
61
config.json
61
config.json
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
215
run.py
215
run.py
@ -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
135
update.py
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user