示例#1
0
 def _handle_api_exception(self, api_data, logger, start) -> Optional[Step]:
     """Return a step if an error in API response was detected. Else return None."""
     error_message: Optional[str] = None
     if "API Timeout" in api_data.values():
         error_message = "API Timeout"
     elif "API rate limit exceeded" in api_data.values():
         error_message = "Radio France API rate limit exceeded"
     elif api_data.get("data") is None:
         error_message = "No data provided by Radio France API"
     elif not api_data["data"]["grid"]:
         error_message = "Grid provided by Radio France API is empty"
     if error_message:
         logger.error(error_message)
     return Step.empty_until(start, start +
                             90, self) if error_message else None
示例#2
0
    def get_step(self, logger: Logger, dt: datetime, channel) -> UpdateInfo:
        """Returns mapping containing info about current song.

        If music: {"type": BroadcastType.MUSIC, "artist": artist, "title": title}
        If ads: "type": BroadcastType.ADS
        Else: "type": BroadcastType.NONE

        Moreover, returns other metadata for postprocessing.
        end datetime object

        To sum up, here are the keys of returned mapping:
        - type: BroadcastType object
        - end: timestamp in sec
        - artist: str (optional)
        - title: str (optional)
        - thumbnail_src: url to thumbnail
        """
        start = int(dt.timestamp())
        try:
            fetched_data = self._fetch_song_metadata()
        except requests.exceptions.Timeout:
            return self._update_info(Step.empty_until(start, start + 90, self))
        end = fetched_data["end"]
        if start > end:
            if not self._get_show_metadata(dt):
                return self._update_info(Step.empty(start, self))
            end = 0
            broadcast_data = {
                "thumbnail_src": self.station_thumbnail,
                "type": BroadcastType.PROGRAMME,
                "title": self.station_slogan,
            }
        else:
            title = fetched_data['title']
            artist = fetched_data['singer']
            thumbnail = fetched_data.get("cover") or self.station_thumbnail
            broadcast_data = {
                "title": f"{artist} • {title}",
                "type": BroadcastType.MUSIC,
                "thumbnail_src": thumbnail,
                "metadata": SongPayload(title=title, artist=artist)
            }
        broadcast_data.update(station=self.station_info,
                              **self._get_show_metadata(dt))
        return self._update_info(
            Step(start=start, end=end, broadcast=Broadcast(**broadcast_data)))
示例#3
0
class RadioFranceStation(URLStation):
    API_RATE_LIMIT_EXCEEDED = 1
    _station_api_name: str
    _grid_template = RADIO_FRANCE_GRID_TEMPLATE

    @property
    def token(self):
        if os.getenv("TOKEN") is None:  # in case of development server
            load_dotenv()
            if os.getenv("TOKEN") is None:
                raise RuntimeError("No token for Radio France API found.")
        return os.getenv("TOKEN")

    @staticmethod
    def _notifying_update_info(step):
        return UpdateInfo(should_notify_update=True, step=step)

    def _fetch_metadata(self,
                        start: datetime,
                        end: datetime,
                        retry=0,
                        current=0,
                        raise_exc=False) -> Dict[Any, Any]:
        """Fetch metadata from radiofrance open API."""
        query = self._grid_template.format(start=int(start.timestamp()),
                                           end=int(end.timestamp()),
                                           station=self._station_api_name)
        try:
            rep = requests.post(
                url="https://openapi.radiofrance.fr/v1/graphql?x-token={}".
                format(self.token),
                json={"query": query},
                timeout=4,
            )
        except requests.exceptions.Timeout:
            if current < retry:
                return self._fetch_metadata(start, end, retry, current + 1,
                                            raise_exc)
            if raise_exc:
                raise TimeoutError("Radio France API Timeout")
            return {"message": "API Timeout"}
        data = json.loads(rep.text)
        return data

    @staticmethod
    def _find_current_child_show(children: List[Any], parent: Dict[str, Any],
                                 start: int) -> Tuple[Dict, int, bool]:
        """Return current show among children and its end timestamp.

        Sometimes, current timestamp is between 2 children. In this case,
        return parent show and next child start as end.

        Parameters:
        - children: list of dict representing radiofrance steps
        - parent: dict representing radiofrance step
        - start: datetime object representing asked timestamp

        Return a tuple containing:
        - dict representing a step
        - end timestamp
        - True if the current show is child or not
        """

        # on enlève les enfants vides (les TrackStep que l'on ne prend pas en compte)
        children = filter(bool, children)
        # on trie dans l'ordre inverse
        children = sorted(children, key=lambda x: x.get("start"), reverse=True)

        # on initialise l'enfant suivant (par défaut le dernier)
        next_child = children[0]
        # et on parcourt la liste des enfants à l'envers
        for child in children:
            # dans certains cas, le type de step ne nous intéresse pas
            # et est donc vide, on passe directement au suivant
            # (c'est le cas des TrackSteps)
            if child.get("start") is None:
                continue

            # si le début du programme est à venir, on passe au précédent
            if child["start"] > start:
                next_child = child
                continue

            # au premier programme dont le début est avant la date courante
            # on sait qu'on est potentiellement dans le programme courant.
            # Il faut vérifier que l'on est encore dedans en vérifiant :
            if child["end"] > start:
                return child, int(child["end"]), True

            # sinon, on est dans un "trou" : on utilise donc le parent
            # et le début de l'enfant suivant. Cas particulier : si on est
            # entre la fin du dernier enfant et la fin du parent (càd l'enfant
            # suivant est égal à l'enfant courant), on prend la fin du parent.
            elif next_child == child:
                return parent, int(parent["end"]), False
            else:
                return parent, int(next_child["start"]), False
        else:
            # si on est ici, c'est que la boucle a parcouru tous les enfants
            # sans valider child["start"] < now. Autrement dit, le premier
            # enfant n'a pas encore commencé. On renvoie donc le parent et le
            # début du premier enfant (stocké dans next_child) comme end
            return parent, int(next_child.get("start")) or parent["end"], False

    def _handle_api_exception(self, api_data, logger, start) -> Optional[Step]:
        """Return a step if an error in API response was detected. Else return None."""
        error_message: Optional[str] = None
        if "API Timeout" in api_data.values():
            error_message = "API Timeout"
        elif "API rate limit exceeded" in api_data.values():
            error_message = "Radio France API rate limit exceeded"
        elif api_data.get("data") is None:
            error_message = "No data provided by Radio France API"
        elif not api_data["data"]["grid"]:
            error_message = "Grid provided by Radio France API is empty"
        if error_message:
            logger.error(error_message)
        return Step.empty_until(start, start +
                                90, self) if error_message else None

    @staticmethod
    def _get_detailed_metadata(metadata: dict, parent: dict, child: dict,
                               is_child: bool) -> dict:
        """Alter (add detailed information to) a copy of metadata and return it

        :param metadata: metadata to update (this method creates a copy and alter it)
        :param parent: parent broadcast
        :param child: child broadcast (may be identical to parent)
        :param is_child: if True, add parent_show_title and parent_show_link info
        :return: updated copy of metadata input
        """
        detailed_metadata = metadata.copy()
        diffusion = child.get("diffusion") or {}
        show = diffusion.get("show") or {}
        if not is_child:
            diffusion_summary = diffusion.get("standFirst", "") or ""
            if len(diffusion_summary.strip()) == 1:
                diffusion_summary = ""
            detailed_metadata.update({
                "show_link": show.get("url", ""),
                "link": diffusion.get("url", ""),
                "summary": diffusion_summary.strip(),
            })
            return detailed_metadata
        parent_diffusion = parent.get("diffusion") or {}
        parent_show = parent_diffusion.get("show") or {}
        diffusion_summary = diffusion.get(
            "standFirst", "") or parent_diffusion.get("standFirst", "") or ""
        child_show_link = diffusion.get("url", "") or parent_diffusion.get(
            "url", "")
        parent_show_link = parent_show.get("url") or ""
        parent_show_title = parent_show.get("title") or parent["title"]
        # on vérifie que les infos parents ne sont pas redondantes avec les infos enfantes
        if (parent_show_link == child_show_link
                or parent_show_title.upper() in (metadata.get(
                    "title", "").upper(), metadata.get("show_title",
                                                       "").upper())):
            parent_show_link = ""
            parent_show_title = ""
        if len(diffusion_summary.strip()) == 1:
            diffusion_summary = ""
        detailed_metadata.update({
            "show_link":
            show.get("url", ""),
            "link":
            diffusion.get("url", "") or parent_diffusion.get("url", ""),
            "summary":
            diffusion_summary.strip(),
            "parent_show_title":
            parent_show_title,
            "parent_show_link":
            parent_show_link,
        })
        return detailed_metadata

    def _get_radiofrance_step(self, api_data: dict, dt: datetime,
                              child_precision: bool, detailed: bool) -> Step:
        """Return a track step or a programme step from Radio France depending of contained metadata"""
        return (self._get_radiofrance_track_step(api_data, dt)
                if api_data.get("track")
                else self._get_radiofrance_programme_step(
                    api_data, dt, child_precision, detailed))

    def _get_radiofrance_programme_step(self, api_data: dict, dt: datetime,
                                        child_precision: bool, detailed: bool):
        """Return radio france step starting at dt.

        Parameters:
        child_precision: bool -- if True, search current child if current broadcast contains any
        detailed: bool -- if True, return more info in step such as summary, external links, parent broadcast
        """
        start = max(int(api_data["start"]), int(dt.timestamp()))
        metadata = {
            "station": self.station_info,
            "type": BroadcastType.PROGRAMME
        }
        children = (api_data.get("children") or []) if child_precision else []
        broadcast, broadcast_end, is_child = (self._find_current_child_show(
            children, api_data, start) if any(children) else
                                              (api_data, int(api_data["end"]),
                                               False))
        diffusion = broadcast.get("diffusion")
        if diffusion is None:
            title = broadcast["title"]
            show_title = ""
            thumbnail_src = self.station_thumbnail
        else:
            show = diffusion.get("show", {}) or {}
            title = diffusion.get("title") or show.get("title", "")
            show_title = show.get("title",
                                  "") if title != show.get("title", "") else ""
            podcast_link = (show.get("podcast") or {}).get("itunes")
            thumbnail_src = music.fetch_apple_podcast_cover(
                podcast_link, self.station_thumbnail)
        metadata.update({
            "title": title,
            "show_title": show_title,
            "thumbnail_src": thumbnail_src,
        })
        if detailed:
            metadata = self._get_detailed_metadata(metadata, api_data,
                                                   broadcast, is_child)
        return Step(start=start,
                    end=broadcast_end,
                    broadcast=Broadcast(**metadata))

    def _get_radiofrance_track_step(self, api_data: dict, dt: datetime):
        start = max(int(api_data["start"]), int(dt.timestamp()))
        track_data = api_data["track"]
        artists = ", ".join(
            track_data.get("mainArtists") or track_data.get("performers"))
        cover_link, deezer_link = music.fetch_cover_and_link_on_deezer(
            self.station_thumbnail, artists, track_data.get("albumTitle"))
        return Step(start=start,
                    end=api_data["end"],
                    broadcast=Broadcast(
                        title=f"{artists} • {track_data['title']}",
                        type=BroadcastType.MUSIC,
                        station=self.station_info,
                        link=deezer_link,
                        summary=self.station_slogan,
                        thumbnail_src=cover_link,
                        metadata=SongPayload(
                            title=track_data["title"],
                            artist=artists,
                            album=track_data.get("album", ""),
                            base64_cover_art=url_to_base64(cover_link))))

    def get_step(self, logger: Logger, dt: datetime, channel) -> UpdateInfo:
        start = int(dt.timestamp())
        fetched_data = self._fetch_metadata(dt, dt + timedelta(minutes=120))
        if (error_step := self._handle_api_exception(fetched_data, logger,
                                                     start)) is not None:
            return self._notifying_update_info(error_step)
        try:
            # on récupère la première émission trouvée
            first_show_in_grid = fetched_data["data"]["grid"][0]
            # si celle-ci est terminée et la suivante n'a pas encore démarrée
            # alors on RENVOIE une métadonnées neutre jusqu'au démarrage de l'émission
            # suivante
            if first_show_in_grid["end"] < start:
                next_show = fetched_data["data"]["grid"][1]
                return self._notifying_update_info(
                    Step.empty_until(start, int(next_show["start"]), self))
            # si l'émission n'est pas encore démarrée, on RENVOIE une métaonnée neutre
            # jusqu'au démarrage de celle-ci
            if first_show_in_grid["start"] > dt.timestamp():
                return self._notifying_update_info(
                    Step.empty_until(start, int(first_show_in_grid['start']),
                                     self))
            # cas où on est dans un programme, on RENVOIE les métadonnées de ce programme
            return self._notifying_update_info(
                self._get_radiofrance_step(first_show_in_grid,
                                           dt,
                                           child_precision=True,
                                           detailed=True))

        except Exception as err:
            logger.error(traceback.format_exc())
            logger.error("Données récupérées avant l'exception : {}".format(
                fetched_data))
            return self._notifying_update_info(
                Step.empty_until(start, start + 90, self))
示例#4
0
        if (error_step :=
                self._handle_api_exception(api_data, logger,
                                           int(dt.timestamp()))) is not None:
            return error_step
        try:
            first_show_in_grid = api_data["data"]["grid"][0]
            return self._get_radiofrance_step(first_show_in_grid,
                                              dt,
                                              child_precision=True,
                                              detailed=False)
        except Exception as err:
            start = int(dt.timestamp())
            logger.error(traceback.format_exc())
            logger.error(
                "Données récupérées avant l'exception : {}".format(api_data))
            return Step.empty_until(start, start + 90, self)

    def get_schedule(self, logger: Logger, start: datetime,
                     end: datetime) -> List[Step]:
        api_data = self._fetch_metadata(start, end, retry=5, raise_exc=True)
        temp_end, end = start, end
        steps = []
        grid = api_data["data"]["grid"]
        while grid:
            step_data = grid.pop(0)
            new_step = self._get_radiofrance_programme_step(
                step_data, temp_end, child_precision=False, detailed=False)
            steps.append(new_step)
            temp_end = datetime.fromtimestamp(new_step.end)
        return steps