Advertisement
Croftie

Untitled

Jun 12th, 2025
5
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.89 KB | None | 0 0
  1. """
  2. ITV
  3. Author: stabbedbybrick
  4.  
  5. Info:
  6. ITV L3 is 720p, AAC 2.0 max
  7.  
  8. """
  9. from __future__ import annotations
  10.  
  11. import json
  12. import subprocess
  13. import time
  14. from collections import Counter
  15. from pathlib import Path
  16.  
  17. import click
  18. import requests
  19. from bs4 import BeautifulSoup
  20.  
  21. from utils.args import get_args
  22. from utils.cdm import LocalCDM
  23. from utils.config import Config
  24. from utils.options import get_downloads
  25. from utils.titles import Episode, Movie, Movies, Series
  26. from utils.utilities import (
  27. append_id,
  28. construct_pssh,
  29. convert_subtitles,
  30. force_numbering,
  31. get_wvd,
  32. in_cache,
  33. set_filename,
  34. set_save_path,
  35. string_cleaning,
  36. update_cache,
  37. )
  38.  
  39.  
  40. class ITV(Config):
  41. def __init__(self, config, **kwargs):
  42. super().__init__(config, **kwargs)
  43.  
  44. with self.config["download_cache"].open("r") as file:
  45. self.cache = json.load(file)
  46.  
  47. self.client.headers = {
  48. 'accept': '*/*',
  49. 'accept-encoding': 'gzip, deflate',
  50. 'accept-language': 'en-US,en;q=0.9',
  51. 'sec-fetch-dest': 'document',
  52. 'sec-fetch-mode': 'navigate',
  53. 'sec-fetch-site': 'none',
  54. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  55. 'Connection': 'keep-alive'
  56. }
  57.  
  58. self.get_options()
  59.  
  60. def get_license(self, challenge: bytes, lic_url: str) -> bytes:
  61. r = self.client.post(url=lic_url, data=challenge)
  62. r.raise_for_status()
  63. return r.content
  64.  
  65. def get_keys(self, pssh: str, lic_url: str) -> bytes:
  66. wvd = get_wvd(Path.cwd())
  67. widevine = LocalCDM(wvd)
  68. challenge = widevine.challenge(pssh)
  69. response = self.get_license(challenge, lic_url)
  70. return widevine.parse(response)
  71.  
  72. def get_data(self, url: str) -> dict:
  73. r = self.client.get(url)
  74. r.raise_for_status()
  75.  
  76. soup = BeautifulSoup(r.text, "html.parser")
  77. props = soup.select_one("#__NEXT_DATA__").text
  78. data = json.loads(props)
  79. return data["props"]["pageProps"]
  80.  
  81. def get_series(self, url: str) -> Series:
  82. data = self.get_data(url)
  83.  
  84. return Series(
  85. [
  86. Episode(
  87. id_=episode["episodeId"],
  88. service="ITV",
  89. title=data["programme"]["title"],
  90. season=episode.get("series")
  91. if isinstance(episode.get("series"), int)
  92. else 0,
  93. number=episode.get("episode")
  94. if isinstance(episode.get("episode"), int)
  95. else 0,
  96. name=episode["episodeTitle"],
  97. year=None,
  98. data=episode["playlistUrl"],
  99. description=episode.get("description"),
  100. )
  101. for series in data["seriesList"]
  102. if "Latest episodes" not in series["seriesLabel"]
  103. for episode in series["titles"]
  104. ]
  105. )
  106.  
  107. def get_movies(self, url: str) -> Movies:
  108. data = self.get_data(url)
  109.  
  110. return Movies(
  111. [
  112. Movie(
  113. id_=movie["episodeId"],
  114. service="ITV",
  115. title=data["programme"]["title"],
  116. year=movie.get("productionYear"),
  117. name=data["programme"]["title"],
  118. data=movie["playlistUrl"],
  119. synopsis=movie.get("description"),
  120. )
  121. for movies in data["seriesList"]
  122. for movie in movies["titles"]
  123. ]
  124. )
  125.  
  126. def get_playlist(self, playlist: str) -> tuple:
  127. headers = {
  128. "Accept": "application/vnd.itv.vod.playlist.v4+json",
  129. "Accept-Language": "en-US,en;q=0.9,da;q=0.8",
  130. "Connection": "keep-alive",
  131. "Content-Type": "application/json",
  132. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
  133. }
  134. payload = {
  135. "client": {"version":"4.1","id":"browser","supportsAdPods":True,"service":"itv.x","appversion":"2.320.5"},
  136. "device": {"manufacturer":"Chrome","model":"122.0.0.0","os":{"name":"Windows","version":"10","type":"desktop"},"deviceGroup":"dotcom"},
  137. "user": {},
  138. "variantAvailability": {"player":"dash","featureset": ["mpeg-dash", "widevine", "outband-webvtt", "hd", "single-track"], "platformTag": "dotcom","drm":{"system":"widevine","maxSupported":"L3"}},
  139. }
  140.  
  141. r = self.client.post(playlist, headers=headers, json=payload)
  142. r.raise_for_status()
  143.  
  144. data = r.json()
  145.  
  146. video = data["Playlist"]["Video"]
  147. media = video["MediaFiles"]
  148. mpd_url = f"{media[0].get('Href')}"
  149. lic_url = f"{media[0].get('KeyServiceUrl')}"
  150. subtitle = video.get("Subtitles")
  151. subtitle = f"{subtitle[0].get('Href')}" if subtitle else None
  152.  
  153. return mpd_url, lic_url, subtitle
  154.  
  155. def get_mediainfo(self, manifest: str, quality: str) -> str:
  156. r = requests.get(manifest)
  157. r.raise_for_status()
  158.  
  159. self.soup = BeautifulSoup(r.content, "xml")
  160. elements = self.soup.find_all("Representation")
  161. heights = sorted(
  162. [int(x.attrs["height"]) for x in elements if x.attrs.get("height")],
  163. reverse=True,
  164. )
  165.  
  166. new_base, params = manifest.split(".mpd")
  167. new_base += "dash/"
  168. self.soup.select_one("BaseURL").string = new_base
  169.  
  170. segments = self.soup.find_all("SegmentTemplate")
  171. for segment in segments:
  172. segment["media"] += params
  173. segment["initialization"] += params
  174.  
  175. with open(self.tmp / "manifest.mpd", "w") as f:
  176. f.write(str(self.soup.prettify()))
  177.  
  178. if quality is not None:
  179. if int(quality) in heights:
  180. return quality
  181. else:
  182. closest_match = min(heights, key=lambda x: abs(int(x) - int(quality)))
  183. return closest_match
  184.  
  185. return heights[0]
  186.  
  187. def get_content(self, url: str) -> object:
  188. if self.movie:
  189. with self.console.status("Fetching movie titles..."):
  190. content = self.get_movies(self.url)
  191. title = string_cleaning(str(content))
  192.  
  193. self.log.info(f"{str(content)}\n")
  194.  
  195. else:
  196. with self.console.status("Fetching series titles..."):
  197. content = self.get_series(url)
  198.  
  199. title = string_cleaning(str(content))
  200. seasons = Counter(x.season for x in content)
  201. num_seasons = len(seasons)
  202. num_episodes = sum(seasons.values())
  203.  
  204. if self.force_numbering:
  205. content = force_numbering(content)
  206. if self.append_id:
  207. content = append_id(content)
  208.  
  209. self.log.info(
  210. f"{str(content)}: {num_seasons} Season(s), {num_episodes} Episode(s)\n"
  211. )
  212.  
  213. return content, title
  214.  
  215. def get_episode_from_url(self, url: str):
  216. with self.console.status("Getting episode from URL..."):
  217. data = self.get_data(url)
  218.  
  219. episode = Series(
  220. [
  221. Episode(
  222. id_=data["episode"]["episodeId"],
  223. service="ITV",
  224. title=data["programme"]["title"],
  225. season=data["episode"].get("series")
  226. if isinstance(data["episode"].get("series"), int)
  227. else 0,
  228. number=data["episode"].get("episode")
  229. if isinstance(data["episode"].get("episode"), int)
  230. else 0,
  231. name=data["episode"]["episodeTitle"],
  232. # year=None,
  233. data=data["episode"]["playlistUrl"],
  234. description=data["episode"].get("description"),
  235. )
  236. ]
  237. )
  238.  
  239. title = string_cleaning(str(episode))
  240.  
  241. return [episode[0]], title
  242.  
  243. def get_options(self) -> None:
  244. downloads, title = get_downloads(self)
  245.  
  246. for download in downloads:
  247. if not self.no_cache and in_cache(self.cache, download):
  248. continue
  249.  
  250. if self.slowdown:
  251. with self.console.status(
  252. f"Slowing things down for {self.slowdown} seconds..."
  253. ):
  254. time.sleep(self.slowdown)
  255.  
  256. self.download(download, title)
  257.  
  258. def download(self, stream: object, title: str) -> None:
  259. manifest, lic_url, subtitle = self.get_playlist(stream.data)
  260. self.res = self.get_mediainfo(manifest, self.quality)
  261. pssh = construct_pssh(self.soup)
  262.  
  263. keys = self.get_keys(pssh, lic_url)
  264. with open(self.tmp / "keys.txt", "w") as file:
  265. file.write("\n".join(keys))
  266.  
  267. self.filename = set_filename(self, stream, self.res, audio="AAC2.0")
  268. self.save_path = set_save_path(stream, self, title)
  269. self.manifest = self.tmp / "manifest.mpd"
  270. self.key_file = self.tmp / "keys.txt"
  271. self.sub_path = None
  272.  
  273. self.log.info(f"{str(stream)}")
  274. click.echo("")
  275.  
  276. if subtitle is not None and not self.skip_download:
  277. self.log.info(f"Subtitles: {subtitle}")
  278. try:
  279. sub = self.client.get(subtitle)
  280. sub.raise_for_status()
  281. except requests.exceptions.HTTPError:
  282. self.log.warning(f"Subtitle response {sub.status_code}, skipping")
  283. else:
  284. sub_path = self.tmp / f"{self.filename}.vtt"
  285. with open(sub_path, "wb") as f:
  286. f.write(sub.content)
  287.  
  288. if not self.sub_no_fix:
  289. sub_path = convert_subtitles(self.tmp, self.filename, sub_type="vtt")
  290.  
  291. self.sub_path = sub_path
  292.  
  293. if self.skip_download:
  294. self.log.info(f"Filename: {self.filename}")
  295. self.log.info("Subtitles: Yes\n") if subtitle else self.log.info(
  296. "Subtitles: None\n"
  297. )
  298.  
  299. args, file_path = get_args(self)
  300.  
  301. if not file_path.exists():
  302. try:
  303. subprocess.run(args, check=True)
  304. except Exception as e:
  305. raise ValueError(f"{e}")
  306. else:
  307. self.log.warning(f"{self.filename} already exists. Skipping download...\n")
  308. self.sub_path.unlink() if self.sub_path else None
  309.  
  310. if not self.skip_download and file_path.exists():
  311. update_cache(self.cache, self.config, stream)
  312.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement