fix(iP): Fix failing audio track for Devine v.3.3.3, fix HLG range

- MIssing data for the added audio track in Devine v3.3.3 has been fixed and will now download properly.
- The range for UHD tracks were previously incorrectly listed as SDR but is now listed as HLG. You'll need to use `--range HLG` to request UHD tracks.
- Single episodes are now fetched from API rather than site source code.
This commit is contained in:
stabbedbybrick 2024-05-09 21:10:53 +02:00
parent 7f37dc9571
commit c1d517f18c
2 changed files with 50 additions and 42 deletions

View File

@ -15,7 +15,7 @@ from devine.core.manifests import DASH, HLS
from devine.core.search_result import SearchResult
from devine.core.service import Service
from devine.core.titles import Episode, Movie, Movies, Series
from devine.core.tracks import Audio, Chapter, Subtitle, Track, Tracks, Video
from devine.core.tracks import Audio, Chapter, Subtitle, Tracks, Video
from devine.core.utils.collections import as_list
from devine.core.utils.sslciphers import SSLCiphers
@ -45,7 +45,7 @@ class iP(Service):
iP:
cert: path/to/cert
\b
- Use -v H.265 to request UHD tracks
- Use --range HLG to request H.265 UHD tracks
- See which titles are available in UHD:
https://www.bbc.co.uk/iplayer/help/questions/programme-availability/uhd-content
"""
@ -64,15 +64,14 @@ class iP(Service):
def __init__(self, ctx: Context, title: str):
self.title = title
self.vcodec = ctx.parent.params.get("vcodec")
self.range = ctx.parent.params.get("range_")
super().__init__(ctx)
if self.vcodec == "H.265" and not self.config.get("cert"):
self.log.error("H.265 cannot be selected without a certificate")
if self.range[0].name == "HLG" and not self.config.get("cert"):
self.log.error("UHD tracks cannot be selected without an SSL certificate")
sys.exit(1)
quality = ctx.parent.params.get("quality")
if quality and quality[0] > 1080 and self.vcodec != "H.265" and self.config.get("cert"):
self.log.info(" + Switched video codec to H.265 to be able to get 2160p video track")
elif self.range[0].name == "HLG" and self.config.get("cert"):
self.vcodec = "H.265"
def search(self) -> Generator[SearchResult, None, None]:
@ -97,15 +96,13 @@ class iP(Service):
def get_titles(self) -> Union[Movies, Series]:
kind, pid = (re.match(self.TITLE_RE, self.title).group(i) for i in ("kind", "id"))
if not pid:
self.log.error("Unable to parse title ID - is the URL or id correct?")
sys.exit(1)
raise ValueError("Unable to parse title ID - is the URL or id correct?")
data = self.get_data(pid, slice_id=None)
if data is None and kind == "episode":
return self.get_single_episode(self.title)
return self.get_single_episode(pid)
elif data is None:
self.log.error("Metadata was not found - if %s is an episode, use full URL as input", pid)
sys.exit(1)
raise ValueError(f"Metadata was not found - if {pid} is an episode, use full URL as input")
if "Film" in data["labels"]["category"]:
return Movies(
@ -122,7 +119,9 @@ class iP(Service):
)
else:
seasons = [self.get_data(pid, x["id"]) for x in data["slices"] or [{"id": None}]]
episodes = [self.create_episode(episode, data) for season in seasons for episode in season["entities"]["results"]]
episodes = [
self.create_episode(episode, data) for season in seasons for episode in season["entities"]["results"]
]
return Series(episodes)
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
@ -133,27 +132,32 @@ class iP(Service):
versions = r.json().get("allAvailableVersions")
if not versions:
# If API returns no versions, try to fetch from site source code
r = self.session.get(self.config["base_url"].format(type="episode", pid=title.id))
redux = re.search("window.__IPLAYER_REDUX_STATE__ = (.*?);</script>", r.text).group(1)
data = json.loads(redux)
versions = [
{"pid": x.get("id") for x in data["versions"] if not x.get("kind") == "audio-described"}
]
versions = [{"pid": x.get("id") for x in data["versions"] if not x.get("kind") == "audio-described"}]
quality = [
connection.get("height")
for i in (
self.check_all_versions(version)
for version in (x.get("pid") for x in versions)
)
for i in (self.check_all_versions(version) for version in (x.get("pid") for x in versions))
for connection in i
if connection.get("height")
]
max_quality = max((h for h in quality if h < "1080"), default=None)
media = next((i for i in (self.check_all_versions(version)
for version in (x.get("pid") for x in versions))
if any(connection.get("height") == max_quality for connection in i)), None)
media = next(
(
i
for i in (self.check_all_versions(version) for version in (x.get("pid") for x in versions))
if any(connection.get("height") == max_quality for connection in i)
),
None,
)
if not media:
self.log.error("No media found. If you're behind a VPN/proxy, you might be blocked")
sys.exit(1)
connection = {}
for video in [x for x in media if x["kind"] == "video"]:
@ -189,7 +193,9 @@ class iP(Service):
raise ValueError(f"Unsupported video media transfer format {connection['transferFormat']!r}")
for video in tracks.videos:
# TODO: add HLG to UHD tracks
# UHD DASH manifest has no range information, so we add it manually
if video.codec == Video.Codec.HEVC:
video.range = Video.Range.HLG
if any(re.search(r"-audio_\w+=\d+", x) for x in as_list(video.url)):
# create audio stream from the video stream
@ -198,12 +204,14 @@ class iP(Service):
# use audio_url not video url, as to ignore video bitrate in ID
id_=hashlib.md5(audio_url.encode()).hexdigest()[0:7],
url=audio_url,
codec=Audio.Codec.from_codecs("mp4a"),
language=[v.language for v in video.data["hls"]["playlist"].media][0],
codec=Audio.Codec.from_codecs(video.data["hls"]["playlist"].stream_info.codecs),
language=video.data["hls"]["playlist"].media[0].language,
bitrate=int(self.find(r"-audio_\w+=(\d+)", as_list(video.url)[0]) or 0),
channels=[v.channels for v in video.data["hls"]["playlist"].media][0],
channels=video.data["hls"]["playlist"].media[0].channels,
descriptive=False, # Not available
descriptor=Track.Descriptor.HLS,
descriptor=Audio.Descriptor.HLS,
drm=video.drm,
data=video.data,
)
if not tracks.exists(by_id=audio.id):
# some video streams use the same audio, so natural dupes exist
@ -313,13 +321,12 @@ class iP(Service):
data=episode,
)
def get_single_episode(self, url: str) -> Series:
r = self.session.get(url)
def get_single_episode(self, pid: str) -> Series:
r = self.session.get(self.config["endpoints"]["episodes"].format(pid=pid))
r.raise_for_status()
redux = re.search("window.__IPLAYER_REDUX_STATE__ = (.*?);</script>", r.text).group(1)
data = json.loads(redux)
subtitle = data["episode"].get("subtitle")
data = json.loads(r.content)
subtitle = data["episodes"][0].get("subtitle")
if subtitle is not None:
season_match = re.search(r"Series (\d+):", subtitle)
@ -338,9 +345,9 @@ class iP(Service):
return Series(
[
Episode(
id_=data["episode"]["id"],
id_=data["episodes"][0]["id"],
service=self.__class__,
title=data["episode"]["title"],
title=data["episodes"][0]["title"],
season=season if subtitle else 0,
number=number if subtitle else 0,
name=name if subtitle else "",

View File

@ -1,10 +1,11 @@
base_url: https://www.bbc.co.uk/iplayer/{type}/{pid}
user_agent: 'smarttv_AFTMM_Build_0003255372676_Chromium_41.0.2250.2'
api_key: 'D2FgtcTxGqqIgLsfBWTJdrQh2tVdeaAp'
user_agent: smarttv_AFTMM_Build_0003255372676_Chromium_41.0.2250.2
api_key: D2FgtcTxGqqIgLsfBWTJdrQh2tVdeaAp
endpoints:
metadata: 'https://graph.ibl.api.bbc.co.uk/'
playlist: 'https://www.bbc.co.uk/programmes/{pid}/playlist.json'
manifest: "https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/{mediaset}/vpid/{vpid}/"
manifest_: 'https://securegate.iplayer.bbc.co.uk/mediaselector/6/select/version/2.0/vpid/{vpid}/format/json/mediaset/{mediaset}/proto/https'
search: "https://search.api.bbci.co.uk/formula/iplayer-ibl-root"
episodes: https://ibl.api.bbci.co.uk/ibl/v1/episodes/{pid}?rights=mobile&availability=available
metadata: https://graph.ibl.api.bbc.co.uk/
playlist: https://www.bbc.co.uk/programmes/{pid}/playlist.json
manifest: https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/{mediaset}/vpid/{vpid}/
manifest_: https://securegate.iplayer.bbc.co.uk/mediaselector/6/select/version/2.0/vpid/{vpid}/format/json/mediaset/{mediaset}/proto/https
search: https://search.api.bbci.co.uk/formula/iplayer-ibl-root