Fix #38, #57, Implement Config file (#58)

* style: correct some user messages and typos

* feat(config file): implement configuration settings

* style(config): improve config variables readability

* style(config params): config conformity fix

* feat(config): add config feature to variables

* feat(config): add config feature examples to README.md

* style: uniformity

* fix(subtitles): add subtitles to corresponding folder

* fix(tv-series subtitles folder name)
This commit is contained in:
Cohvir 2024-03-04 22:34:36 +01:00 committed by GitHub
parent 0094458c3d
commit 9ee9c91a2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 203 additions and 101 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

14
.idea/StreamingCommunity_api.iml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/StreamingCommunity_api.iml" filepath="$PROJECT_DIR$/.idea/StreamingCommunity_api.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -24,13 +24,13 @@ pip install -r requirements.txt
## Usage ## Usage
Run the script with the following command: Run the script with the following command:
```python ```powershell
python run.py python run.py
``` ```
## Auto Update ## Auto Update
Keep your script up to date with the latest features by running: Keep your script up to date with the latest features by running:
```python ```powershell
python update.py python update.py
``` ```
@ -39,10 +39,32 @@ python update.py
- 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 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, navigate to ".\Src\Lib\FFmpeg\my_m3u8" and change 'DOWNLOAD_SUB' to False in the configuration file.) - 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. - 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": "Film",
"series_folder_name": "Serie",
"download_subtitles": true,
"download_default_language": false
}
```
#### Options
| Key | Default Value | Description | Value Example |
|---------------------------|---------------|------------------------------------------------------------------------------------|--------------------------|
| root_path | videos | Path where the script will add movies and tv series. Do not put trailing slash. | media/streamingcommunity |
| movies_folder_name | Film | The folder name where all the movies will be placed. Do not put trailing slash. | downloaded-movies |
| series_folder_name | Serie | 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 | false | Whether or not you want to download only the default Italian audio language. | true |
## Tutorial ## Tutorial
For a detailed walkthrough, refer to the [video tutorial](https://www.youtube.com/watch?v=Ok7hQCgxqLg&ab_channel=Nothing) For a detailed walkthrough, refer to the [video tutorial](https://www.youtube.com/watch?v=Ok7hQCgxqLg&ab_channel=Nothing)

View File

@ -3,12 +3,14 @@
# Class import # Class import
from Src.Util.headers import get_headers from Src.Util.headers import get_headers
from Src.Util.console import console from Src.Util.console import console
from Src.Util.config import config
from Src.Lib.FFmpeg.my_m3u8 import download_m3u8 from Src.Lib.FFmpeg.my_m3u8 import download_m3u8
# General import # General import
import requests, os, re, json, sys, binascii import requests, os, re, json, sys, binascii
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
# [func] # [func]
def get_iframe(id_title, domain): def get_iframe(id_title, domain):
req = requests.get(url = f"https://streamingcommunity.{domain}/iframe/{id_title}", headers = { req = requests.get(url = f"https://streamingcommunity.{domain}/iframe/{id_title}", headers = {
@ -22,13 +24,14 @@ def get_iframe(id_title, domain):
try: try:
return BeautifulSoup(req_embed, "lxml").find("body").find("script").text return BeautifulSoup(req_embed, "lxml").find("body").find("script").text
except: except:
console.log("[red]Cant play this video, (video not available)") console.log("[red]Couldn't play this video file (video not available)")
sys.exit(0) sys.exit(0)
else: else:
console.log(f"[red]Error: {req.status_code}") console.log(f"[red]Error: {req.status_code}")
sys.exit(0) sys.exit(0)
def select_quality(json_win_param): def select_quality(json_win_param):
if json_win_param['token1080p']: if json_win_param['token1080p']:
@ -40,6 +43,7 @@ def select_quality(json_win_param):
else: else:
return "360p" return "360p"
def parse_content(embed_content): def parse_content(embed_content):
# Parse parameter from req embed content # Parse parameter from req embed content
@ -52,10 +56,12 @@ def parse_content(embed_content):
json_win_param = json_win_param.replace(",}", "}").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)) 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): def get_m3u8_url(json_win_video, json_win_param, render_quality):
token_render = f"token{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']}" 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(json_win_video, json_win_param, title_name, token_render): def get_m3u8_key(json_win_video, json_win_param, title_name, token_render):
response = requests.get('https://vixcloud.co/storage/enc.key', headers={ response = requests.get('https://vixcloud.co/storage/enc.key', 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"]}', '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"]}',
@ -67,6 +73,7 @@ def get_m3u8_key(json_win_video, json_win_param, title_name, token_render):
console.log(f"[red]Error: {response.status_code}") console.log(f"[red]Error: {response.status_code}")
sys.exit(0) sys.exit(0)
def get_m3u8_audio(json_win_video, json_win_param, title_name, token_render): 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={ 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"]}' '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"]}'
@ -89,18 +96,18 @@ def main_dw_film(id_film, title_name, domain):
json_win_video, json_win_param, render_quality = parse_content(embed_content) json_win_video, json_win_param, render_quality = parse_content(embed_content)
token_render = f"token{render_quality}" token_render = f"token{render_quality}"
console.print(f"[blue]Quality select => [red]{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_url = get_m3u8_url(json_win_video, json_win_param, render_quality)
m3u8_key = get_m3u8_key(json_win_video, json_win_param, title_name, token_render) m3u8_key = get_m3u8_key(json_win_video, json_win_param, title_name, token_render)
mp4_name = title_name.replace("+", " ").replace(",", "").replace("-", "_") mp4_name = title_name.replace("+", " ").replace(",", "").replace("-", "_")
mp4_format = mp4_name + ".mp4" mp4_format = mp4_name + ".mp4"
mp4_path = os.path.join("videos", mp4_format) 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) m3u8_url_audio = get_m3u8_audio(json_win_video, json_win_param, title_name, token_render)
if m3u8_url_audio != None: if m3u8_url_audio is not None:
console.print("[blue]Use m3u8 audio => [red]True") console.print("[blue]Using m3u8 audio => [red]True")
subtitle_path = os.path.join(config['root_path'], config['series_folder_name'], mp4_name)
download_m3u8(m3u8_index=m3u8_url, m3u8_audio=m3u8_url_audio, m3u8_subtitle=m3u8_url, key=m3u8_key, output_filename=mp4_path) download_m3u8(m3u8_index=m3u8_url, m3u8_audio=m3u8_url_audio, m3u8_subtitle=m3u8_url, key=m3u8_key, output_filename=mp4_path, subtitle_folder=subtitle_path, content_name=mp4_name)

View File

@ -26,7 +26,7 @@ def domain_version():
return domain, version return domain, version
except Exception as e: except Exception as e:
console.log("[red]Cant get version, problem with domain. Try again.") console.log("[red]Couldn't get the version, there's a problem with the domain. Try again.")
sys.exit(0) sys.exit(0)

View File

@ -3,6 +3,7 @@
# Class import # Class import
from Src.Util.headers import get_headers from Src.Util.headers import get_headers
from Src.Util.console import console, msg from Src.Util.console import console, msg
from Src.Util.config import config
from Src.Lib.FFmpeg.my_m3u8 import download_m3u8 from Src.Lib.FFmpeg.my_m3u8 import download_m3u8
# General import # General import
@ -16,6 +17,7 @@ def get_token(id_tv, domain):
session.get(f"https://streamingcommunity.{domain}/watch/{id_tv}") session.get(f"https://streamingcommunity.{domain}/watch/{id_tv}")
return session.cookies['XSRF-TOKEN'] return session.cookies['XSRF-TOKEN']
def get_info_tv(id_film, title_name, site_version, domain): def get_info_tv(id_film, title_name, site_version, domain):
req = requests.get(f"https://streamingcommunity.{domain}/titles/{id_film}-{title_name}", headers={ req = requests.get(f"https://streamingcommunity.{domain}/titles/{id_film}-{title_name}", headers={
@ -30,6 +32,7 @@ def get_info_tv(id_film, title_name, site_version, domain):
console.log(f"[red]Error: {req.status_code}") console.log(f"[red]Error: {req.status_code}")
sys.exit(0) sys.exit(0)
def get_info_season(tv_id, tv_name, domain, version, token, n_stagione): def get_info_season(tv_id, tv_name, domain, version, token, n_stagione):
req = requests.get(f'https://streamingcommunity.{domain}/titles/{tv_id}-{tv_name}/stagione-{n_stagione}', headers={ req = requests.get(f'https://streamingcommunity.{domain}/titles/{tv_id}-{tv_name}/stagione-{n_stagione}', headers={
'authority': f'streamingcommunity.{domain}', 'referer': f'https://streamingcommunity.broker/titles/{tv_id}-{tv_name}', 'authority': f'streamingcommunity.{domain}', 'referer': f'https://streamingcommunity.broker/titles/{tv_id}-{tv_name}',
@ -42,6 +45,7 @@ def get_info_season(tv_id, tv_name, domain, version, token, n_stagione):
console.log(f"[red]Error: {req.status_code}") console.log(f"[red]Error: {req.status_code}")
sys.exit(0) sys.exit(0)
def get_iframe(tv_id, ep_id, domain, token): def get_iframe(tv_id, ep_id, domain, token):
req = requests.get(f'https://streamingcommunity.{domain}/iframe/{tv_id}', params={'episode_id': ep_id, 'next_episode': '1'}, cookies={'XSRF-TOKEN': token}, headers={ req = requests.get(f'https://streamingcommunity.{domain}/iframe/{tv_id}', params={'episode_id': ep_id, 'next_episode': '1'}, cookies={'XSRF-TOKEN': token}, headers={
'referer': f'https://streamingcommunity.{domain}/watch/{tv_id}?e={ep_id}', 'referer': f'https://streamingcommunity.{domain}/watch/{tv_id}?e={ep_id}',
@ -85,14 +89,17 @@ def parse_content(embed_content):
json_win_param = json_win_param.replace(",}", "}").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)) 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): def get_playlist(json_win_video, json_win_param, render_quality):
token_render = f"token{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']}" 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): def get_m3u8_url(json_win_video, json_win_param, render_quality):
token_render = f"token{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']}" 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(json_win_video, json_win_param, tv_name, n_stagione, n_ep, ep_title, token_render): def get_m3u8_key_ep(json_win_video, json_win_param, tv_name, n_stagione, n_ep, ep_title, token_render):
response = requests.get('https://vixcloud.co/storage/enc.key', headers={ response = requests.get('https://vixcloud.co/storage/enc.key', 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_stagione}%3AE{n_ep}+{ep_title}&nextEpisode=1', '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_stagione}%3AE{n_ep}+{ep_title}&nextEpisode=1',
@ -123,38 +130,42 @@ def get_m3u8_playlist(json_win_video, json_win_param, tv_name, n_stagione, n_ep,
# [func \ main] # [func \ main]
def dw_single_ep(tv_id, eps, index_ep_select, domain, token, tv_name, season_select): def dw_single_ep(tv_id, eps, index_ep_select, domain, token, tv_name, season_select):
enccoded_name = urllib.parse.quote(eps[index_ep_select]['name']) encoded_name = urllib.parse.quote(eps[index_ep_select]['name'])
console.print(f"[green]Download ep: [blue]{eps[index_ep_select]['n']} [green]=> [purple]{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, token) embed_content = get_iframe(tv_id, eps[index_ep_select]['id'], domain, token)
json_win_video, json_win_param, render_quality = parse_content(embed_content) json_win_video, json_win_param, render_quality = parse_content(embed_content)
token_render = f"token{render_quality}" token_render = f"token{render_quality}"
console.print(f"[blue]Quality select => [red]{render_quality}") console.print(f"[blue]Selected quality => [red]{render_quality}")
m3u8_playlist = get_playlist(json_win_video, json_win_param, 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_url = get_m3u8_url(json_win_video, json_win_param, render_quality)
m3u8_key = get_m3u8_key_ep(json_win_video, json_win_param, tv_name, season_select, index_ep_select+1, enccoded_name, token_render) m3u8_key = get_m3u8_key_ep(json_win_video, json_win_param, tv_name, season_select, index_ep_select+1, encoded_name, token_render)
mp4_name = f"{tv_name.replace('+', '_')}_S{str(season_select).zfill(2)}E{str(index_ep_select+1).zfill(2)}" mp4_name = f"{tv_name.replace('+', '_')}_S{str(season_select).zfill(2)}E{str(index_ep_select+1).zfill(2)}"
mp4_format = f"{mp4_name}.mp4" mp4_format = f"{mp4_name}.mp4"
mp4_path = os.path.join("videos",tv_name, mp4_format) season = mp4_name.rsplit("E", 1)[0]
mp4_path = os.path.join(config['root_path'], config['series_folder_name'], tv_name, season, mp4_format)
m3u8_url_audio = get_m3u8_playlist(json_win_video, json_win_param, tv_name, season_select, index_ep_select+1, enccoded_name, token_render) m3u8_url_audio = get_m3u8_playlist(json_win_video, json_win_param, tv_name, season_select, index_ep_select+1, encoded_name, token_render)
if m3u8_url_audio != None: if m3u8_url_audio is not None:
console.print("[blue]Use m3u8 audio => [red]True") console.print("[blue]Using m3u8 audio => [red]True")
# Movie_Name.[Language_Code].vtt
# Movie_Name.[Language_Code].forced.vtt
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)
download_m3u8(m3u8_index=m3u8_url, m3u8_audio=m3u8_url_audio, m3u8_subtitle=m3u8_playlist, key=m3u8_key, output_filename=mp4_path)
def main_dw_tv(tv_id, tv_name, version, domain): def main_dw_tv(tv_id, tv_name, version, domain):
token = get_token(tv_id, domain) token = get_token(tv_id, domain)
num_season_find = get_info_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 [yellow]or [red](*) [green]to download all seasons [yellow]or [red][1-2] [green]for a range of season") 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 find: [red]{num_season_find}") console.print(f"\n[blue]Season(s) found: [red]{num_season_find}")
season_select = str(msg.ask("\n[green]Insert season number: ")) season_select = str(msg.ask("\n[green]Insert which season(s) number you'd like to download"))
if "[" in season_select: if "[" in season_select:
start, end = map(int, season_select[1:-1].split('-')) start, end = map(int, season_select[1:-1].split('-'))
result = list(range(start, end + 1)) result = list(range(start, end + 1))
@ -169,8 +180,8 @@ def main_dw_tv(tv_id, tv_name, version, domain):
eps = get_info_season(tv_id, tv_name, domain, version, token, season_select) eps = get_info_season(tv_id, tv_name, domain, version, token, season_select)
for ep in eps: for ep in eps:
console.print(f"[green]Ep: [blue]{ep['n']} [green]=> [purple]{ep['name']}") console.print(f"[green]Episode: [blue]{ep['n']} [green]=> [purple]{ep['name']}")
index_ep_select = str(msg.ask("\n[green]Insert ep [red]number [yellow]or [red](*) [green]to download all ep [yellow]or [red][1-2] [green]for a range of ep: ")) 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 [] # Download range []
if "[" in index_ep_select: if "[" in index_ep_select:
@ -187,7 +198,7 @@ def main_dw_tv(tv_id, tv_name, version, domain):
index_ep_select = int(index_ep_select) - 1 index_ep_select = int(index_ep_select) - 1
dw_single_ep(tv_id, eps, index_ep_select, domain, token, tv_name, season_select) dw_single_ep(tv_id, eps, index_ep_select, domain, token, tv_name, season_select)
else: else:
console.print("[red]Wrong index for ep") console.print("[red]Wrong [yellow]INDEX [red]for the selected Episode")
# Download all # Download all
else: else:
@ -196,7 +207,7 @@ def main_dw_tv(tv_id, tv_name, version, domain):
print("\n") print("\n")
else: else:
console.print("[red]Wrong index for season") console.print("[red]Wrong [yellow]INDEX for the selected Season")
else: else:
for n_season in range(1, num_season_find+1): for n_season in range(1, num_season_find+1):
eps = get_info_season(tv_id, tv_name, domain, version, token, n_season) eps = get_info_season(tv_id, tv_name, domain, version, token, n_season)

View File

@ -55,7 +55,7 @@ def check_ffmpeg():
console.print("[cyan]FFmpeg is not in the PATH. Downloading and adding to the PATH...[/cyan]") console.print("[cyan]FFmpeg is not in the PATH. Downloading and adding to the PATH...[/cyan]")
if not isAdmin(): if not isAdmin():
console.log("[red]You need to be admin to proceed!") console.log("[red]You need admin privileges to proceed!")
sys.exit(0) sys.exit(0)
download_ffmpeg() download_ffmpeg()

View File

@ -3,6 +3,7 @@
# Class import # Class import
from Src.Util.console import console from Src.Util.console import console
from Src.Util.headers import get_headers from Src.Util.headers import get_headers
from Src.Util.config import config
from Src.Lib.FFmpeg.util import print_duration_table from Src.Lib.FFmpeg.util import print_duration_table
# Import # Import
@ -20,8 +21,8 @@ warnings.filterwarnings("ignore", category=UserWarning, module="cryptography")
# Variable # Variable
MAX_WORKER = 20 MAX_WORKER = 20
DONWLOAD_SUB = True DOWNLOAD_SUB = config['download_subtitles']
DOWNLOAD_DEFAULT_LANGUAGE = False DOWNLOAD_DEFAULT_LANGUAGE = config['download_default_language']
failed_segments = [] failed_segments = []
@ -65,7 +66,7 @@ class M3U8_Parser:
}) })
for key in m3u8_obj.keys: for key in m3u8_obj.keys:
if key != None: if key is not None:
self.keys = ({ self.keys = ({
"method": key.method, "method": key.method,
"uri": key.uri, "uri": key.uri,
@ -90,7 +91,6 @@ class M3U8_Parser:
"uri": media.uri "uri": media.uri
}) })
for segment in m3u8_obj.segments: for segment in m3u8_obj.segments:
if "vtt" not in segment.uri: if "vtt" not in segment.uri:
self.segments.append(segment.uri) self.segments.append(segment.uri)
@ -107,13 +107,13 @@ class M3U8_Parser:
if self.video_playlist: if self.video_playlist:
return self.video_playlist[0].get('uri') return self.video_playlist[0].get('uri')
else: else:
print("No video playlist find") print("No video playlist found")
return None return None
def download_subtitle(self): def download_subtitle(self, subtitle_path, content_name):
"""Download all subtitle if present""" """Download all subtitle if present"""
path = os.path.join("videos", "subtitle") path = subtitle_path
if self.subtitle_playlist: if self.subtitle_playlist:
for sub_info in self.subtitle_playlist: for sub_info in self.subtitle_playlist:
@ -123,25 +123,29 @@ class M3U8_Parser:
continue continue
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
console.log(f"[green]Download subtitle: [red]{name_language}") console.log(f"[green]Downloading subtitle: [red]{name_language}")
req_sub_content = requests.get(sub_info.get("uri")) req_sub_content = requests.get(sub_info.get("uri"))
sub_parse = M3U8_Parser() sub_parse = M3U8_Parser()
sub_parse.parse_data(req_sub_content.text) sub_parse.parse_data(req_sub_content.text)
url_subititle = sub_parse.subtitle[0] url_subtitle = sub_parse.subtitle[0]
open(os.path.join(path, name_language + ".vtt"), "wb").write(requests.get(url_subititle).content) # Subtitles convention:
# Movie_Name.[Language_Code].vtt
# Movie_Name.[Language_Code].forced.vtt # If forced
# Implementare "forced"
open(os.path.join(path, content_name + f".{name_language}" + ".vtt"), "wb").write(requests.get(url_subtitle).content)
else: else:
console.log("[red]No subtitle find") console.log("[red]No subtitle found")
def get_track_audio(self, language_name): # Ex. English def get_track_audio(self, language_name): # Ex. English
"""Return url of audio eng audio playlist if present""" """Return url of audio eng audio playlist if present"""
if self.audio_ts: if self.audio_ts:
console.log(f"[cyan]Find {len(self.audio_ts)}, playlist with audio") console.log(f"[cyan]Found {len(self.audio_ts)}, playlist with audio")
if language_name != None: if language_name is not None:
for obj_audio in self.audio_ts: for obj_audio in self.audio_ts:
if obj_audio.get("name") == language_name: if obj_audio.get("name") == language_name:
return obj_audio.get("uri") return obj_audio.get("uri")
@ -149,13 +153,14 @@ class M3U8_Parser:
return None return None
else: else:
console.log("[red]Cant find playlist with audio") console.log("[red]Couldn't find any playlist with audio")
class M3U8_Segments: class M3U8_Segments:
def __init__(self, url, key=None): def __init__(self, url, key=None):
self.url = url self.url = url
self.key = key self.key = key
if key != None: if key is not None:
self.decription = Decryption(key) self.decription = Decryption(key)
self.temp_folder = os.path.join("tmp", "segments") self.temp_folder = os.path.join("tmp", "segments")
@ -171,7 +176,7 @@ class M3U8_Segments:
m3u8_parser.parse_data(m3u8_content) m3u8_parser.parse_data(m3u8_content)
# Add decryption iv if present # Add decryption iv if present
if self.key != None and m3u8_parser.keys: if self.key is not None and m3u8_parser.keys:
self.decription.parse_key(m3u8_parser.keys.get("iv")) self.decription.parse_key(m3u8_parser.keys.get("iv"))
# Add all segments # Add all segments
@ -184,10 +189,10 @@ class M3U8_Segments:
if response.ok: if response.ok:
self.parse_data(response.text) self.parse_data(response.text)
#console.log(f"[red]Ts segments find [white]=> [yellow]{len(self.segments)}") # console.log(f"[red]Ts segments find [white]=> [yellow]{len(self.segments)}")
if len(self.segments) == 0: if len(self.segments) == 0:
console.log("[red]Cant find segments to donwload, retry") console.log("[red]Couldn't find any segments to download, retry")
sys.exit(0) sys.exit(0)
else: else:
@ -213,20 +218,19 @@ class M3U8_Segments:
if response.status_code == 200: if response.status_code == 200:
return response.content return response.content
else: else:
#print(f"Failed to fetch {ts_url}: {response.status_code}") # print(f"Failed to fetch {ts_url}: {response.status_code}")
failed_segments.append(str(url_number)) failed_segments.append(str(url_number))
return None return None
except Exception as e: except Exception as e:
#print(f"Failed to fetch {ts_url}: {str(e)}") # print(f"Failed to fetch {ts_url}: {str(e)}")
failed_segments.append(str(url_number)) failed_segments.append(str(url_number))
return None return None
else: else:
#print("Skip ", ts_url, " arr ", failed_segments) # print("Skip ", ts_url, " arr ", failed_segments)
return None return None
def save_ts(self, index, progress_counter, quit_event): def save_ts(self, index, progress_counter, quit_event):
"""Save ts file and decrypt if there is iv present in decryption class""" """Save ts file and decrypt if there is iv present in decryption class"""
@ -247,7 +251,7 @@ class M3U8_Segments:
progress_counter.update(1) progress_counter.update(1)
def download_ts(self): def download_ts(self):
"""Loop to donwload all segment of playlist m3u8 and break it if there is no progress""" """Loop to download all segment of playlist m3u8 and break it if there is no progress"""
progress_counter = tqdm(total=len(self.segments), unit="bytes", desc="[yellow]Download") progress_counter = tqdm(total=len(self.segments), unit="bytes", desc="[yellow]Download")
@ -311,21 +315,21 @@ class M3U8_Segments:
file_list_path = os.path.join(current_dir, 'file_list.txt') file_list_path = os.path.join(current_dir, 'file_list.txt')
ts_files = [f for f in os.listdir(self.temp_folder) if f.endswith(".ts")] ts_files = [f for f in os.listdir(self.temp_folder) if f.endswith(".ts")]
def extract_number(file_name): def extract_number(file_name):
return int(''.join(filter(str.isdigit, file_name))) return int(''.join(filter(str.isdigit, file_name)))
ts_files.sort(key=extract_number) ts_files.sort(key=extract_number)
if len(ts_files) == 0: if len(ts_files) == 0:
console.log("[red]Cant find segments to join, retry") console.log("[red]Couldn't find any segments to join, retry")
sys.exit(0) sys.exit(0)
with open(file_list_path, 'w') as f: with open(file_list_path, 'w') as f:
for ts_file in ts_files: for ts_file in ts_files:
relative_path = os.path.relpath(os.path.join(self.temp_folder, ts_file), current_dir) relative_path = os.path.relpath(os.path.join(self.temp_folder, ts_file), current_dir)
f.write(f"file '{relative_path}'\n") f.write(f"file '{relative_path}'\n")
console.log("[cyan]Start join all file") console.log("[cyan]Joining all files...")
try: try:
ffmpeg.input(file_list_path, format='concat', safe=0).output(output_filename, c='copy', loglevel='error').run() ffmpeg.input(file_list_path, format='concat', safe=0).output(output_filename, c='copy', loglevel='error').run()
except ffmpeg.Error as e: except ffmpeg.Error as e:
@ -336,6 +340,7 @@ class M3U8_Segments:
os.remove(file_list_path) os.remove(file_list_path)
shutil.rmtree("tmp", ignore_errors=True) shutil.rmtree("tmp", ignore_errors=True)
class M3U8_Downloader: class M3U8_Downloader:
def __init__(self, m3u8_url, m3u8_audio = None, key=None, output_filename="output.mp4"): def __init__(self, m3u8_url, m3u8_audio = None, key=None, output_filename="output.mp4"):
self.m3u8_url = m3u8_url self.m3u8_url = m3u8_url
@ -346,16 +351,16 @@ class M3U8_Downloader:
def start(self): def start(self):
video_m3u8 = M3U8_Segments(self.m3u8_url, self.key) video_m3u8 = M3U8_Segments(self.m3u8_url, self.key)
console.log("[green]Download video ts") console.log("[green]Downloading video ts")
video_m3u8.get_info() video_m3u8.get_info()
video_m3u8.download_ts() video_m3u8.download_ts()
video_m3u8.join(self.video_path) video_m3u8.join(self.video_path)
print_duration_table(self.video_path) print_duration_table(self.video_path)
print("\n") print("\n")
if self.m3u8_audio != None: if self.m3u8_audio is not None:
audio_m3u8 = M3U8_Segments(self.m3u8_audio, self.key) audio_m3u8 = M3U8_Segments(self.m3u8_audio, self.key)
console.log("[green]Download audio ts") console.log("[green]Downloading audio ts")
audio_m3u8.get_info() audio_m3u8.get_info()
audio_m3u8.download_ts() audio_m3u8.download_ts()
audio_m3u8.join(self.audio_path) audio_m3u8.join(self.audio_path)
@ -405,22 +410,24 @@ def df_make_req(url):
console.log(f"[red]Wrong url, error: {response.status_code}") console.log(f"[red]Wrong url, error: {response.status_code}")
sys.exit(0) sys.exit(0)
def download_subtitle(url, name_language): def download_subtitle(url, name_language):
"""Make req to vtt url and save to video subtitle folder""" """Make req to vtt url and save to video subtitle folder"""
path = os.path.join("videos", "subtitle") path = os.path.join("videos", "subtitle")
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
console.log(f"[green]Download subtitle: [red]{name_language}") console.log(f"[green]Downloading subtitle: [red]{name_language}")
open(os.path.join(path, name_language + ".vtt"), "wb").write(requests.get(url).content) open(os.path.join(path, name_language + ".vtt"), "wb").write(requests.get(url).content)
def download_m3u8(m3u8_playlist=None, m3u8_index = None, m3u8_audio=None, m3u8_subtitle=None, key=None, output_filename=os.path.join("videos", "output.mp4"), log=False):
def download_m3u8(m3u8_playlist=None, m3u8_index = None, m3u8_audio=None, m3u8_subtitle=None, key=None, output_filename=os.path.join("videos", "output.mp4"), log=False, subtitle_folder="subtitles", content_name=""):
# Get byte of key # Get byte of key
key = bytes.fromhex(key) if key is not None else key key = bytes.fromhex(key) if key is not None else key
if m3u8_playlist != None: if m3u8_playlist is not None:
console.log(f"[green]Dowload m3u8 from playlist") console.log(f"[green]Downloading m3u8 from playlist")
# Parse m3u8 playlist # Parse m3u8 playlist
parse_class_m3u8 = M3U8_Parser() parse_class_m3u8 = M3U8_Parser()
@ -431,28 +438,25 @@ def download_m3u8(m3u8_playlist=None, m3u8_index = None, m3u8_audio=None, m3u8_s
else: else:
parse_class_m3u8.parse_data(m3u8_playlist) parse_class_m3u8.parse_data(m3u8_playlist)
# Get italian language if present as default
# Get italian language in present as defualt
if DOWNLOAD_DEFAULT_LANGUAGE: if DOWNLOAD_DEFAULT_LANGUAGE:
m3u8_audio = parse_class_m3u8.get_track_audio("Italian") m3u8_audio = parse_class_m3u8.get_track_audio("Italian")
console.log(f"[green]Select language => [purple]{m3u8_audio}") console.log(f"[green]Selected language => [purple]{m3u8_audio}")
# Get best quality # Get best quality
if m3u8_index == None: if m3u8_index is None:
m3u8_index = parse_class_m3u8.get_best_quality() m3u8_index = parse_class_m3u8.get_best_quality()
if "https" in m3u8_index: if "https" in m3u8_index:
if log: console.log(f"[green]Select m3u8 index => [purple]{m3u8_index}") if log: console.log(f"[green]Selected m3u8 index => [purple]{m3u8_index}")
else: else:
console.log("[red]Cant find a valid m3u8 index") console.log("[red]Couldn't find a valid m3u8 index")
sys.exit(0) sys.exit(0)
# Download subtitle if present ( normally in m3u8 playlist )
if DOWNLOAD_SUB:
parse_class_m3u8.download_subtitle(subtitle_path=subtitle_folder, content_name=content_name)
# Download subtitle if present ( normaly in m3u8 playlist ) if m3u8_subtitle is not None:
if DONWLOAD_SUB:
parse_class_m3u8.download_subtitle()
if m3u8_subtitle != None:
parse_class_m3u8_sub = M3U8_Parser() parse_class_m3u8_sub = M3U8_Parser()
@ -462,10 +466,9 @@ def download_m3u8(m3u8_playlist=None, m3u8_index = None, m3u8_audio=None, m3u8_s
else: else:
parse_class_m3u8_sub.parse_data(m3u8_subtitle) parse_class_m3u8_sub.parse_data(m3u8_subtitle)
# Download subtitle if present ( normaly in m3u8 playlist ) # Download subtitle if present ( normally in m3u8 playlist )
if DONWLOAD_SUB: if DOWNLOAD_SUB:
parse_class_m3u8_sub.download_subtitle() parse_class_m3u8_sub.download_subtitle(subtitle_path=subtitle_folder, content_name=content_name)
# Download m3u8 index, with segments # Download m3u8 index, with segments
# os.makedirs("videos", exist_ok=True) # os.makedirs("videos", exist_ok=True)
@ -473,5 +476,5 @@ def download_m3u8(m3u8_playlist=None, m3u8_index = None, m3u8_audio=None, m3u8_s
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
if log: if log:
console.log(f"[green]Dowload m3u8 from index [white]=> [purple]{m3u8_index}") console.log(f"[green]Download m3u8 from index [white]=> [purple]{m3u8_index}")
M3U8_Downloader(m3u8_index, m3u8_audio, key=key, output_filename=output_filename).start() M3U8_Downloader(m3u8_index, m3u8_audio, key=key, output_filename=output_filename).start()

View File

@ -19,7 +19,7 @@ def get_install_version():
return about['__version__'] return about['__version__']
def main_update(): def main_update():
console.print("[green]Checking github version ...") console.print("[green]Checking GitHub version ...")
json = requests.get(f"https://api.github.com/repos/{repo_user}/{repo_name}/releases").json()[0] 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'] stargazers_count = requests.get(f"https://api.github.com/repos/{repo_user}/{repo_name}").json()['stargazers_count']
@ -34,11 +34,11 @@ def main_update():
if get_install_version() != last_version: if get_install_version() != last_version:
console.print(f"[red]=> A new version is available: [green]{json['zipball_url']}") console.print(f"[red]=> A new version is available: [green]{json['zipball_url']}")
console.print(f"[red]=> Versione: [yellow]{json['name']}") console.print(f"[red]=> New Version: [yellow]{json['name']}")
else: else:
console.print(f"[red]=> Everything up to date") console.print(f"[red]=> Everything is up to date")
console.print(f"[red]=> Last version: [yellow]{json['name']}") console.print(f"[red]=> You're on Version: [yellow]{json['name']}")
print("\n") print("\n")

10
Src/Util/config.py Normal file
View File

@ -0,0 +1,10 @@
import json
from pathlib import Path
def load_config(file_path):
with open(file_path, 'r') as file:
config_file = json.load(file)
return config_file
config_path = Path(__file__).parent.parent.parent / 'config.json' # path for config.json (in root directory)
config = load_config(config_path)

7
config.json Normal file
View File

@ -0,0 +1,7 @@
{
"root_path": "videos",
"movies_folder_name": "Film",
"series_folder_name": "Serie",
"download_subtitles": true,
"download_default_language": false
}

28
run.py
View File

@ -36,30 +36,30 @@ def main():
initialize() initialize()
domain, site_version = Page.domain_version() domain, site_version = Page.domain_version()
film_search = msg.ask("\n[blue]Insert word to search in all site: ").strip() film_search = msg.ask("\n[blue]Search for any movie or tv series title").strip()
db_title = Page.search(film_search, domain) db_title = Page.search(film_search, domain)
Page.display_search_results(db_title) Page.display_search_results(db_title)
if len(db_title) != 0: if len(db_title) != 0:
console.print(f"\n[blue]Total result: {len(db_title)}") console.print(f"\n[blue]Total result: {len(db_title)}")
console.print( console.print(
"\n[green]Insert index [red]number [yellow]or [red][1-2] [green]for a range of movies/tv [yellow]or [red][1,3,5] [green]to select discontinued movie/tv" "\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 tv show you will have to choose season and episode to download") 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]Index to download: ")) index_select = str(msg.ask("\n[blue]Select [yellow]INDEX [blue]to download"))
if index_select.isnumeric(): if index_select.isnumeric():
index_select = int(index_select) index_select = int(index_select)
if 0 <= index_select <= len(db_title) - 1: if 0 <= index_select <= len(db_title) - 1:
selected_title = db_title[index_select] selected_title = db_title[index_select]
if selected_title['type'] == "movie": if selected_title['type'] == "movie":
console.print(f"[green]\nMovie select: {selected_title['name']}") console.print(f"[green]\nSelected Movie: {selected_title['name']}")
download_film(selected_title['id'], selected_title['slug'], domain) download_film(selected_title['id'], selected_title['slug'], domain)
else: else:
console.print(f"[green]\nTv select: {selected_title['name']}") console.print(f"[green]\nSelected TV Series: {selected_title['name']}")
download_tv(selected_title['id'], selected_title['slug'], site_version, domain) download_tv(selected_title['id'], selected_title['slug'], site_version, domain)
else: else:
console.print("[red]Wrong index for selection") console.print("[red]Wrong INDEX for selection")
elif "[" in index_select: elif "[" in index_select:
if "-" in index_select: if "-" in index_select:
start, end = map(int, index_select[1:-1].split('-')) start, end = map(int, index_select[1:-1].split('-'))
@ -67,27 +67,27 @@ def main():
for n in result: for n in result:
selected_title = db_title[n] selected_title = db_title[n]
if selected_title['type'] == "movie": if selected_title['type'] == "movie":
console.print(f"[green]\nMovie select: {selected_title['name']}") console.print(f"[green]\nSelected Movie: {selected_title['name']}")
download_film(selected_title['id'], selected_title['slug'], domain) download_film(selected_title['id'], selected_title['slug'], domain)
else: else:
console.print(f"[green]\nTv select: {selected_title['name']}") console.print(f"[green]\nSelected TV Series: {selected_title['name']}")
download_tv(selected_title['id'], selected_title['slug'], site_version, domain) download_tv(selected_title['id'], selected_title['slug'], site_version, domain)
elif "," in index_select: elif "," in index_select:
result = list(map(int, index_select[1:-1].split(','))) result = list(map(int, index_select[1:-1].split(',')))
for n in result: for n in result:
selected_title = db_title[n] selected_title = db_title[n]
if selected_title['type'] == "movie": if selected_title['type'] == "movie":
console.print(f"[green]\nMovie select: {selected_title['name']}") console.print(f"[green]\nSelected Movie: {selected_title['name']}")
download_film(selected_title['id'], selected_title['slug'], domain) download_film(selected_title['id'], selected_title['slug'], domain)
else: else:
console.print(f"[green]\nTv select: {selected_title['name']}") console.print(f"[green]\nSelected TV Series: {selected_title['name']}")
download_tv(selected_title['id'], selected_title['slug'], site_version, domain) download_tv(selected_title['id'], selected_title['slug'], site_version, domain)
else: else:
console.print("[red]Wrong index for selection") console.print("[red]Wrong INDEX for selection")
else: else:
console.print("[red]Cant find a single element") console.print("[red]Couldn't find any entries for the selected title")
console.print("[red]Done") console.print("[red]Done!")
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -66,7 +66,7 @@ def download_and_extract_latest_commit(author, repo_name):
temp_path = os.path.join(os.path.dirname(os.getcwd()), 'temp_extracted') temp_path = os.path.join(os.path.dirname(os.getcwd()), 'temp_extracted')
with ZipFile(BytesIO(response.content)) as zip_ref: with ZipFile(BytesIO(response.content)) as zip_ref:
zip_ref.extractall(temp_path) zip_ref.extractall(temp_path)
console.log("[green]Extract file ...") console.log("[green]Extracting file ...")
# Move files from the temporary folder to the current folder # Move files from the temporary folder to the current folder