Changes
Implemented track download skip if file already exists. A few Linux support changes. Implemented caching cookies to profile cookies path.
This commit is contained in:
parent
bffc9b0d7a
commit
dd71f707f6
10
README.md
10
README.md
@ -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
|
||||
|
||||
@ -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
12
poetry.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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, 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")
|
||||
@ -477,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:
|
||||
@ -487,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:
|
||||
@ -549,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 [
|
||||
@ -603,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(),
|
||||
@ -633,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!")
|
||||
|
||||
@ -641,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)
|
||||
@ -675,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)
|
||||
@ -759,7 +775,6 @@ def result(ctx, service, quality, closest_resolution, range_, wanted, alang, sla
|
||||
muxed_location,
|
||||
os.path.join(final_file_path, f"{title.parse_filename(media_info=media_info)}.{extension}")
|
||||
)
|
||||
|
||||
|
||||
log.info("Processed all titles!")
|
||||
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
@ -1225,7 +1236,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."""
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -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,
|
||||
@ -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")
|
||||
Loading…
Reference in New Issue
Block a user