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.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 "",
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user