Merge branch 'dev' into fix-codec

This commit is contained in:
chu23465 2025-04-18 23:15:55 +05:30 committed by GitHub
commit 22c0489a47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 80 additions and 32 deletions

View File

@ -24,12 +24,11 @@ Support for Sport replays or live streams is not planned. It's a whole thing wit
## Features
- Progress Bars for decryption ([mp4decrypt](https://github.com/chu23465/bentoOldFork), Shaka)
- Refresh Token fixed for Amazon service
- Reprovision .prd automatically after 2 days
- ISM manifest support (Microsoft Smooth Streaming) (WIP/Experimental)
- N_m3u8DL-RE downloader support (Experimental)
- Atmos audio with ISM manifest (Amazon) is Fixed
- Resume failed download has been sort of implemented. If a track has been successfully downloaded previously and exists in `Temp` directory, VT will not download said track again and will resume download.
- Resume failed download has been implemented. If a track has been successfully downloaded previously and exists in `Temp` directory (encrypted or decrypted), VT will not download said track again.
## Usage
@ -230,6 +229,11 @@ If you are getting an `AssertionError` with Amazon, then try reprovisioning the
- From my testing, when using with VPN, it causes lots of issues, mainly needing to clear `Cache` folder and login repeatedly. Use residential proxies if available. Don't hammer service. Try waiting a minute or two before logging in again.
- If you are getting `No 2160p track found` error for a title you know has 4k, then try passing `-r DV` or `-r HDR`. Make sure your account can access highest qualities.
### Hulu
- Authorization: cookies saved to `vinetrimmer/Cookies/Hulu/default.txt`
- Windscribe VPN sometimes fails
### Example Command
Amazon Example:
@ -352,6 +356,7 @@ Tested so far on Amazon, AppleTVPlus, Max, and DisneyPlus.
- `--standalone` will give a folder of compiled pythonic objects. Zip it to distribute. This is recommended.
- If you don't want to carry around/deal with a zip, instead use `--onefile`. This has the drawback of setting the default folders to the temp folder in whatever OS you are using. This could be fixed with some extra code but that is currently not implemented.
- Refer to [link](https://nuitka.net/user-documentation/user-manual.html) if anything errors out.
## Broken / To-Do (Descending order of priority)
@ -385,6 +390,7 @@ Tested so far on Amazon, AppleTVPlus, Max, and DisneyPlus.
### Amazon Specific
- [ ] Refresh Token for Amazon service
- [ ] Pythonic implementation of init.mp4 builder for ism manifest for avc, hvcc, dv, ac3, eac3, eac3-joc codecs
- [ ] Make a pure python requests based downloader for ISM/MSS manifest. Write init.mp4 then download each segment to memory, decrypt in memory and write it to a binary merged file. Download segments in batches. Batch size based on thread count passed to program. Download has to be sequentially written.
- [ ] `--bitrate CVBR+CBR` is currently broken

View File

@ -15,8 +15,6 @@ poetry run vt dl -al en -sl en -q 2160 -r HDR --selected -w S05E08-S05E24 AMZN -
python vinetrimmer1.py dl -al en -sl en -q 2160 -r HDR --selected -w S05E09-S05E24 AMZN -b CBR 0IQZZIJ6W6TT2CXPT6ZOZYX396
poetry run vt dl -al en -sl en --selected -q 2160 -r HDR -w S01E18-S01E25 AMZN -b CBR --ism 0IQZZIJ6W6TT2CXPT6ZOZYX396
Atmos audio download AMZN to fix --> poetry run vt dl -al en -aa -sl en --selected --debug -w S01E01 -A AMZN -b CBR --ism 0HAQAA7JM43QWX0H6GUD3IOF70
http://ABHIRCQAAAAAAAAMCX3W7WLVKL54A.s3-bom-ww.cf.smooth.row.aiv-cdn.net/e5b0/2fe1/032c/4fae-b896-aca9d8bef3d4/170b36b1-856d-4c69-bbf6-feb6c979185a.ism/manifest
poetry run vt dl -al en -sl en -r HDR -w S01E01 --list -q 2160 AMZN https://www.primevideo.com/detail/0HU52DR3U1R0FGI3KSUL00XYY7
https://www.primevideo.com/detail/0HU52DR3U1R0FGI3KSUL00XYY7/

View File

@ -22,4 +22,5 @@ cp ffprobe ./binaries/
cp ffplay ./binaries/
cp mkvtoolnix ./binaries/
cd ./binaries/
find . -type f -print0 | xargs -0 chmod -x
find . -type f -print0 | xargs -0 chmod +x
chmod +x /home/hidden/VT/VT-PR/binaries/*

12
poetry.lock generated
View File

@ -2344,6 +2344,16 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "ushlex"
version = "0.99.1"
description = "Replacement for shlex (that works with unicode) for Python 2.X."
optional = false
python-versions = "*"
files = [
{file = "ushlex-0.99.1.tar.gz", hash = "sha256:6d681561545a9781430d5254eab9a648bade78c82ffd127d56c9228ae8887d46"},
]
[[package]]
name = "validators"
version = "0.18.2"
@ -2537,4 +2547,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.13"
content-hash = "f840ef73c0dc08490a0894e655df27f34ab60e5a5f8c4e3ae8e23ec15f548dad"
content-hash = "a6d1d8597c66d0b914da73da39508244167ff2ea215b36569f40e0d0c909b74a"

View File

@ -51,6 +51,7 @@ validators = "^0.18.2"
websocket-client = "^1.1.0"
xmltodict = "^0.14.2"
yt-dlp = "^2024.11.11"
ushlex = "^0.99.1"
[tool.poetry.group.dev.dependencies]
flake8 = "^3.8.4"

View File

@ -168,6 +168,17 @@ def get_cookie_jar(service, profile):
return cookie_jar
return None
def save_cookies(service, profile):
"""Save cookies from service session to profile's cookies."""
cookie_file = os.path.join(directories.cookies, service.lower(), f"{profile}.txt")
if not os.path.isfile(cookie_file):
cookie_file = os.path.join(directories.cookies, service, f"{profile}.txt")
if os.path.isfile(cookie_file):
cookie_jar = MozillaCookieJar(cookie_file)
for cookie in service.session.cookies:
cookie_jar.set_cookie(cookie)
cookie_jar.save(ignore_discard=False, ignore_expires=False)
def get_credentials(service, profile="default"):
"""Get the profile's credentials if available."""
@ -310,7 +321,7 @@ def dl(ctx, profile, cdm, *_, **__):
@dl.result_callback()
@click.pass_context
def result(ctx, service, quality, closest_resolution, range_, wanted, alang, slang, acodec, audio_only, subs_only, chapters_only, audio_description,
list_, keys, cache, no_cache, no_subs, no_audio, no_video, no_chapters, atmos, vbitrate: int, abitrate: int, no_mux, mux, selected, latest_episode, strip_sdh, *_, **__):
list_, keys, cache, no_cache, no_subs, no_audio, no_video, no_chapters, atmos, vbitrate, abitrate: int, no_mux, mux, selected, latest_episode, strip_sdh, *_, **__):
def ccextractor():
log.info("Extracting EIA-608 captions from stream with CCExtractor")
track_id = f"ccextractor-{track.id}"
@ -332,13 +343,12 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
else:
log.info(" + No captions found")
global content_keys
log = service.log
service_name = service.__class__.__name__
if service_name in ["DisneyPlus", "Hulu"]: # Always retrieve fresh keys for DSNP so that content_keys variable has 2 kid:key pairs
if service_name in ["DisneyPlus", "Hulu"]: # Always retrieve fresh keys for DSNP so that content_keys variable has 2 kid:key pairs, change this to fetch all keys for title from cache
global content_keys
no_cache = True
log.info("Retrieving Titles")
@ -412,8 +422,6 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
title.tracks.select_videos(by_quality=quality, by_vbitrate=vbitrate, by_range=range_, one_only=True)
title.tracks.select_audios(by_language=alang, by_bitrate=abitrate, with_descriptive=audio_description, by_codec=acodec)
title.tracks.select_subtitles(by_language=slang, with_forced=True)
vbitrate = "min"
except ValueError as e:
log.error(f" - {e}")
continue
@ -479,9 +487,11 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
proxy = next(iter(service.session.proxies.values()), None)
else:
proxy = None
if service:
save_cookies(service, ctx.obj.profile)
track.download(directories.temp, headers=service.session.headers, proxy=proxy)
log.info(" + Downloaded")
# To-Do: For DSNP KID add -> mp4info (Bento4) with --verbose option show default kid from init.mp4 file
if isinstance(track, VideoTrack) and track.needs_ccextractor_first and not no_subs:
ccextractor()
if track.encrypted:
@ -489,7 +499,7 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
if track.key:
log.info(f" + KEY: {track.key} (Static)")
elif not no_cache:
track.key, vault_used = ctx.obj.vaults.get(track.kid, title.id)
track.key, vault_used = ctx.obj.vaults.get(track.kid, title.id) #To-Do return all keys for title.id for DSNP, HULU
if track.key:
log.info(f" + KEY: {track.key} (From {vault_used.name} {vault_used.type.name} Key Vault)")
for vault in ctx.obj.vaults.vaults:
@ -551,6 +561,9 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
)
else:
raise log.exit("Unable to license")
if service:
save_cookies(service, ctx.obj.profile)
content_keys = [
(str(x.kid).replace("-", ""), x.key.hex()) for x in ctx.obj.cdm.get_keys(session_id) if x.type == "CONTENT"
] if "common_privacy_cert" in dir(ctx.obj.cdm) else [
@ -605,12 +618,12 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
executable = next((x for x in (shutil.which(x) for x in names) if x), None)
if not executable:
raise log.exit(" - Unable to find packager binary")
dec = os.path.splitext(track.locate())[0] + ".dec.mp4"
dec = os.path.splitext(track.locate())[0].replace("enc", "dec.mp4")
os.makedirs(directories.temp, exist_ok=True)
try:
os.makedirs(directories.temp, exist_ok=True)
subprocess.run([
args = [
executable,
"input={},stream={},output={}".format(
track.locate(),
@ -635,7 +648,8 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
]
),
"--temp_dir", directories.temp
], check=True)
]
subprocess.run(args, check=True)
except subprocess.CalledProcessError:
raise log.exit(" - Failed!")
@ -643,7 +657,7 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
executable = shutil.which("mp4decrypt")
if not executable:
raise log.exit(" - Unable to find mp4decrypt binary")
dec = os.path.splitext(track.locate())[0] + ".dec.mp4"
dec = os.path.splitext(track.locate())[0].replace("enc", "dec.mp4")
os.makedirs(directories.temp, exist_ok=True)
try:
os.makedirs(directories.temp, exist_ok=True)
@ -677,7 +691,7 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
try:
os.makedirs(directories.temp, exist_ok=True)
exec_string = f"{executable} -i {track.locate()} -hide_banner -loglevel error -map 0 -c:a copy {eac3}"
subprocess.run(exec_string)
subprocess.run(exec_string, check=True)
except subprocess.CalledProcessError:
raise log.exit(" - Failed!")
track.swap(eac3)
@ -762,7 +776,6 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
os.path.join(final_file_path, f"{title.parse_filename(media_info=media_info)}.{extension}")
)
log.info("Processed all titles!")

Binary file not shown.

View File

@ -337,13 +337,21 @@ class Track:
)
self.url = segments
if (Path(save_path).is_file() or Path(os.path.splitext(save_path)[0] + ".dec.mp4").is_file()) and not (os.stat(save_path).st_size <= 3):
if (
Path(save_path).is_file() and not
(os.stat(save_path).st_size <= 3)
) or (
Path(save_path.replace("enc", "dec")).is_file() and not
(os.stat(save_path.replace("enc", "dec")).st_size <= 3)
):
log = logging.getLogger("Tracks")
log.info("File already exists, assuming it's from previous unfinished download")
if Path(os.path.splitext(save_path)[0] + ".dec.mp4").is_file():
if Path(save_path.replace("enc", "dec")).is_file():
self.encrypted = False
self._location = save_path
self._location = save_path.replace("enc", "dec")
else:
self._location = save_path
return save_path
if self.source == "CORE":
@ -432,7 +440,10 @@ class Track:
if not os.path.exists(target) or not self._location:
return False
os.unlink(self._location)
os.rename(target, self._location)
if "dec.mp4" in target:
self._location = target
else:
os.rename(target, self._location)
return True
@staticmethod
@ -1226,7 +1237,7 @@ class Tracks:
if not with_forced:
self.subtitles = [x for x in self.subtitles if not x.forced]
if by_language:
self.subtitles = list(self.select_by_language(by_language, self.subtitles, one_per_lang=True))
self.subtitles = list(self.select_by_language(by_language, self.subtitles, one_per_lang=False))
def export_chapters(self, to_file=None):
"""Export all chapters in order to a string or file."""

View File

@ -16,7 +16,10 @@ class Hulu(BaseService):
\b
Authorization: Cookies
Security: UHD@L3
Security:
Hulu original show/movies 4K SDR: L3/SL2000
Hulu original show/movies 720/1080/4K HDR/DV:L1/SL3000
Licensed show/movies 4K SDR 720/1080/4K HDR/DV: L1/SL3000
"""
ALIASES = ["HULU"]

View File

@ -209,7 +209,6 @@ class Jio(BaseService):
self.log.warning('No mpd found')
for track in tracks:
#track.language = Language.get('ta')
track.needs_proxy = True
return tracks
@ -261,7 +260,7 @@ class Jio(BaseService):
def login(self):
self.log.info(' + Logging into JioCinema')
if not self.PHONE_NUMBER:
self.log.exit('Please provide Jiocinema registered Phone number....')
self.PHONE_NUMBER = input('Please provide Jiocinema registered Phone number with country code: ')
guest = self.session.post(
url="https://auth-jiocinema.voot.com/tokenservice/apis/v4/guest",
json={

View File

@ -17,6 +17,9 @@ from vinetrimmer.utils.collections import as_list
from sys import platform
if "win" not in platform:
import shlex
def load_yaml(path):
if not os.path.isfile(path):
#print(f"Service config does not exist -> {path}")
@ -241,11 +244,11 @@ async def saldl(uri, out, headers=None, proxy=None):
async def m3u8dl(uri, out, track, headers=None, proxy=None):
executable = shutil.which("N_m3u8DL-RE") or shutil.which("m3u8DL") or "/usr/bin/N_m3u8DL-RE"
executable = shutil.which("N_m3u8DL-RE") or shutil.which("m3u8DL")
if not executable:
raise EnvironmentError("N_m3u8DL-RE executable not found...")
ffmpeg_binary = shutil.which("ffmpeg") or "/usr/bin/ffmpeg"
ffmpeg_binary = shutil.which("ffmpeg")
arguments = [
executable,
track.original_url or uri,
@ -263,7 +266,7 @@ async def m3u8dl(uri, out, track, headers=None, proxy=None):
arguments.extend(["--header", f'"Cookie:{headers["cookie"].replace(" ", "")}"'])
#for k,v in headers.items():
if "akamai" in uri:
if "akamai" in uri and track.source == "HS":
arguments.append("--max-speed")
arguments.append("12M")
@ -309,6 +312,9 @@ async def m3u8dl(uri, out, track, headers=None, proxy=None):
try:
arg_str = " ".join(arguments)
#print(arg_str)
p = subprocess.run(arg_str, check=True)
if "win" in platform:
p = subprocess.run(arg_str, check=True)
else:
p = subprocess.run(shlex.split(arg_str), check=True)
except subprocess.CalledProcessError:
raise ValueError("N_m3u8DL-RE failed too many times, aborting")