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:
parent
7f37dc9571
commit
c1d517f18c
@ -15,7 +15,7 @@ from devine.core.manifests import DASH, HLS
|
|||||||
from devine.core.search_result import SearchResult
|
from devine.core.search_result import SearchResult
|
||||||
from devine.core.service import Service
|
from devine.core.service import Service
|
||||||
from devine.core.titles import Episode, Movie, Movies, Series
|
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.collections import as_list
|
||||||
from devine.core.utils.sslciphers import SSLCiphers
|
from devine.core.utils.sslciphers import SSLCiphers
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ class iP(Service):
|
|||||||
iP:
|
iP:
|
||||||
cert: path/to/cert
|
cert: path/to/cert
|
||||||
\b
|
\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:
|
- See which titles are available in UHD:
|
||||||
https://www.bbc.co.uk/iplayer/help/questions/programme-availability/uhd-content
|
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):
|
def __init__(self, ctx: Context, title: str):
|
||||||
self.title = title
|
self.title = title
|
||||||
self.vcodec = ctx.parent.params.get("vcodec")
|
self.vcodec = ctx.parent.params.get("vcodec")
|
||||||
|
self.range = ctx.parent.params.get("range_")
|
||||||
super().__init__(ctx)
|
super().__init__(ctx)
|
||||||
|
|
||||||
if self.vcodec == "H.265" and not self.config.get("cert"):
|
if self.range[0].name == "HLG" and not self.config.get("cert"):
|
||||||
self.log.error("H.265 cannot be selected without a certificate")
|
self.log.error("UHD tracks cannot be selected without an SSL certificate")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
quality = ctx.parent.params.get("quality")
|
elif self.range[0].name == "HLG" and self.config.get("cert"):
|
||||||
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")
|
|
||||||
self.vcodec = "H.265"
|
self.vcodec = "H.265"
|
||||||
|
|
||||||
def search(self) -> Generator[SearchResult, None, None]:
|
def search(self) -> Generator[SearchResult, None, None]:
|
||||||
@ -97,15 +96,13 @@ class iP(Service):
|
|||||||
def get_titles(self) -> Union[Movies, Series]:
|
def get_titles(self) -> Union[Movies, Series]:
|
||||||
kind, pid = (re.match(self.TITLE_RE, self.title).group(i) for i in ("kind", "id"))
|
kind, pid = (re.match(self.TITLE_RE, self.title).group(i) for i in ("kind", "id"))
|
||||||
if not pid:
|
if not pid:
|
||||||
self.log.error("Unable to parse title ID - is the URL or id correct?")
|
raise ValueError("Unable to parse title ID - is the URL or id correct?")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
data = self.get_data(pid, slice_id=None)
|
data = self.get_data(pid, slice_id=None)
|
||||||
if data is None and kind == "episode":
|
if data is None and kind == "episode":
|
||||||
return self.get_single_episode(self.title)
|
return self.get_single_episode(pid)
|
||||||
elif data is None:
|
elif data is None:
|
||||||
self.log.error("Metadata was not found - if %s is an episode, use full URL as input", pid)
|
raise ValueError(f"Metadata was not found - if {pid} is an episode, use full URL as input")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if "Film" in data["labels"]["category"]:
|
if "Film" in data["labels"]["category"]:
|
||||||
return Movies(
|
return Movies(
|
||||||
@ -122,7 +119,9 @@ class iP(Service):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
seasons = [self.get_data(pid, x["id"]) for x in data["slices"] or [{"id": None}]]
|
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)
|
return Series(episodes)
|
||||||
|
|
||||||
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
|
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
|
||||||
@ -133,27 +132,32 @@ class iP(Service):
|
|||||||
|
|
||||||
versions = r.json().get("allAvailableVersions")
|
versions = r.json().get("allAvailableVersions")
|
||||||
if not versions:
|
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))
|
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)
|
redux = re.search("window.__IPLAYER_REDUX_STATE__ = (.*?);</script>", r.text).group(1)
|
||||||
data = json.loads(redux)
|
data = json.loads(redux)
|
||||||
versions = [
|
versions = [{"pid": x.get("id") for x in data["versions"] if not x.get("kind") == "audio-described"}]
|
||||||
{"pid": x.get("id") for x in data["versions"] if not x.get("kind") == "audio-described"}
|
|
||||||
]
|
|
||||||
|
|
||||||
quality = [
|
quality = [
|
||||||
connection.get("height")
|
connection.get("height")
|
||||||
for i in (
|
for i in (self.check_all_versions(version) for version in (x.get("pid") for x in versions))
|
||||||
self.check_all_versions(version)
|
|
||||||
for version in (x.get("pid") for x in versions)
|
|
||||||
)
|
|
||||||
for connection in i
|
for connection in i
|
||||||
if connection.get("height")
|
if connection.get("height")
|
||||||
]
|
]
|
||||||
max_quality = max((h for h in quality if h < "1080"), default=None)
|
max_quality = max((h for h in quality if h < "1080"), default=None)
|
||||||
|
|
||||||
media = next((i for i in (self.check_all_versions(version)
|
media = next(
|
||||||
for version in (x.get("pid") for x in versions))
|
(
|
||||||
if any(connection.get("height") == max_quality for connection in i)), None)
|
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 = {}
|
connection = {}
|
||||||
for video in [x for x in media if x["kind"] == "video"]:
|
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}")
|
raise ValueError(f"Unsupported video media transfer format {connection['transferFormat']!r}")
|
||||||
|
|
||||||
for video in tracks.videos:
|
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)):
|
if any(re.search(r"-audio_\w+=\d+", x) for x in as_list(video.url)):
|
||||||
# create audio stream from the video stream
|
# 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
|
# use audio_url not video url, as to ignore video bitrate in ID
|
||||||
id_=hashlib.md5(audio_url.encode()).hexdigest()[0:7],
|
id_=hashlib.md5(audio_url.encode()).hexdigest()[0:7],
|
||||||
url=audio_url,
|
url=audio_url,
|
||||||
codec=Audio.Codec.from_codecs("mp4a"),
|
codec=Audio.Codec.from_codecs(video.data["hls"]["playlist"].stream_info.codecs),
|
||||||
language=[v.language for v in video.data["hls"]["playlist"].media][0],
|
language=video.data["hls"]["playlist"].media[0].language,
|
||||||
bitrate=int(self.find(r"-audio_\w+=(\d+)", as_list(video.url)[0]) or 0),
|
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
|
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):
|
if not tracks.exists(by_id=audio.id):
|
||||||
# some video streams use the same audio, so natural dupes exist
|
# some video streams use the same audio, so natural dupes exist
|
||||||
@ -313,13 +321,12 @@ class iP(Service):
|
|||||||
data=episode,
|
data=episode,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_single_episode(self, url: str) -> Series:
|
def get_single_episode(self, pid: str) -> Series:
|
||||||
r = self.session.get(url)
|
r = self.session.get(self.config["endpoints"]["episodes"].format(pid=pid))
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
redux = re.search("window.__IPLAYER_REDUX_STATE__ = (.*?);</script>", r.text).group(1)
|
data = json.loads(r.content)
|
||||||
data = json.loads(redux)
|
subtitle = data["episodes"][0].get("subtitle")
|
||||||
subtitle = data["episode"].get("subtitle")
|
|
||||||
|
|
||||||
if subtitle is not None:
|
if subtitle is not None:
|
||||||
season_match = re.search(r"Series (\d+):", subtitle)
|
season_match = re.search(r"Series (\d+):", subtitle)
|
||||||
@ -338,9 +345,9 @@ class iP(Service):
|
|||||||
return Series(
|
return Series(
|
||||||
[
|
[
|
||||||
Episode(
|
Episode(
|
||||||
id_=data["episode"]["id"],
|
id_=data["episodes"][0]["id"],
|
||||||
service=self.__class__,
|
service=self.__class__,
|
||||||
title=data["episode"]["title"],
|
title=data["episodes"][0]["title"],
|
||||||
season=season if subtitle else 0,
|
season=season if subtitle else 0,
|
||||||
number=number if subtitle else 0,
|
number=number if subtitle else 0,
|
||||||
name=name if subtitle else "",
|
name=name if subtitle else "",
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
base_url: https://www.bbc.co.uk/iplayer/{type}/{pid}
|
base_url: https://www.bbc.co.uk/iplayer/{type}/{pid}
|
||||||
user_agent: 'smarttv_AFTMM_Build_0003255372676_Chromium_41.0.2250.2'
|
user_agent: smarttv_AFTMM_Build_0003255372676_Chromium_41.0.2250.2
|
||||||
api_key: 'D2FgtcTxGqqIgLsfBWTJdrQh2tVdeaAp'
|
api_key: D2FgtcTxGqqIgLsfBWTJdrQh2tVdeaAp
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
metadata: 'https://graph.ibl.api.bbc.co.uk/'
|
episodes: https://ibl.api.bbci.co.uk/ibl/v1/episodes/{pid}?rights=mobile&availability=available
|
||||||
playlist: 'https://www.bbc.co.uk/programmes/{pid}/playlist.json'
|
metadata: https://graph.ibl.api.bbc.co.uk/
|
||||||
manifest: "https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/{mediaset}/vpid/{vpid}/"
|
playlist: https://www.bbc.co.uk/programmes/{pid}/playlist.json
|
||||||
manifest_: 'https://securegate.iplayer.bbc.co.uk/mediaselector/6/select/version/2.0/vpid/{vpid}/format/json/mediaset/{mediaset}/proto/https'
|
manifest: https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/{mediaset}/vpid/{vpid}/
|
||||||
search: "https://search.api.bbci.co.uk/formula/iplayer-ibl-root"
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user