Sous le capot du client bot Yandex.Music

introduction


Bonjour, Habr! Encore une fois, je suis avec le deuxième article concernant l'API Yandex.Music. L'affaire est prévue et mentionnée dans le premier article .

Les mains tendues, c'est fait. Aujourd'hui, je vais parler de moments intéressants, à mon avis, qui sont présents dans la base de code de mon bot Telegram, qui se positionne comme un client à part entière de la musique. Nous toucherons également l'API de reconnaissance musicale Yandex.

Avant de passer à l'histoire point par point de la mise en œuvre d'une chose particulière, il serait utile d'avoir une idée du bot lui-même et de ses capacités fonctionnelles.

Démo vidéo client


Dans la partie principale, je parlerai des éléments suivants:

  1. Connectez-vous à votre compte via le site sur les pages GitHub (pourquoi et pourquoi).
  2. Le format des données, leur emballage et leur utilisation dans les données des boutons.
  3. Mettre à jour le routage, le versionnage des données, le lancement de contexte dans les gestionnaires.
  4. Prestations de service:
    • Piste de rechargement de service dans Telegram.
    • Le service des "abonnements" pour recevoir une piste avec l'envoi de l'état du téléchargement.
  5. L'implémentation de mise en cache des requêtes la plus simple et la plus élégante.
  6. Reconnaissance d'une piste par message vocal et comment elle apparaît généralement dans le bot.
  7. Petites notes.

Si vous êtes intéressé par au moins un point - bienvenue au chat.
Le bot fonctionne avec les utilisateurs qui se sont connectés à leur compte, mais non. Tout tourne autour de quatre classes principales de service: album, artiste, playlist et piste. Toutes les données d'entité sont prises en charge, il y a une pagination avec la sélection de page. Pour interagir avec eux, un utilisateur autorisé peut utiliser son menu personnel, qui comprend: des listes de lecture intelligentes, la liste de lecture «J'aime» et leurs propres listes de lecture. Pour les utilisateurs non autorisés, la recherche est disponible - en envoyant une requête de recherche sous forme de message normal. Maintenant, il n'y a qu'une liste sèche de ce qu'il y a d'autre: recevoir des pistes et d'autres objets via un lien direct, recevoir des paroles, la possibilité d'aimer / détester la piste et l'artiste, de visualiser des pistes similaires, de reconnaître la piste par message vocal et plus encore.

Partie principale


1. Connectez-vous à votre compte


Initialement, le bot n'était pas prévu de fonctionner sans autorisation.Par conséquent, même pendant la conception, il a été décidé de la manière dont l'autorisation devait se produire - via son site Web . Les principaux arguments étaient la sécurité et la transparence. En aucun cas, il ne voulait accepter les identifiants et les mots de passe des utilisateurs dans des messages clairs. Et l'autorisation de l'ordinateur de l'utilisateur est élémentaire. Par conséquent, un site a été écrit pour autorisation sur React . Il s'agit d'une forme d'autorisation avec redirection vers le bot. Un morceau avec autorisation et traitement de captcha a été extrait de la bibliothèque en Python et réécrit en JavaScript .

Pour l'autorisation, le jeton OAuth reçu est utilisé, qui est retransféré au bot via une liaison profonde.

Le lien final pour la redirection ressemble à ceci: t.me/music_yandex_bot?start={oauth_token}

Je voulais le meilleur, mais les utilisateurs n'avaient pas confiance. Seul un utilisateur sur dix était autorisé (au moment où il était obligatoire). Par conséquent, avec les mises à jour suivantes, j'ai dû expliquer pourquoi vous devriez faire confiance. Tous les articles sur HTTPS , exclusivement sur les demandes du côté client et le code open source ont été publiés sur le site pour autorisation et dupliqués dans un message de bienvenue dans le bot. Obtenu mieux.

En outre, parmi les facteurs de faible autorisation, l'inaccessibilité des miroirs pour t.me en Russie s'est avérée être et prendre en charge uniquement les utilisateurs abonnés (plus à ce sujet dans les notes, paragraphe 7).

Le premier élément a été résolu à l'aide de l' URI ( tg: // ), et à la rigueur, il existe déjà un lien vers le miroir (si la redirection automatique n'a pas fonctionné et le bouton n'a pas aidé), et le second est à nouveau dans l'élément 7.


2. Format des données


Pour ce bot, j'ai décidé pour la première fois d'utiliser NoSQL DB - MongoDB . Compris: quand incorporer un document, quand non. J'ai aimé travailler avec mongoengine. Mais je ne voulais vraiment pas stocker toutes les données des boutons à la maison, et le client n'a que l' ID d' enregistrement dans la base de données. J'étais terrifié par la quantité de données, mais je voulais prendre un serveur Mongo gratuit avec une limite de 512 Mo pour le stockage de données. Trouver une belle réutilisation des enregistrements si les données correspondent dans plusieurs boutons et nettoyer les fichiers obsolètes est beaucoup plus difficile que de tout stocker dans les boutons eux-mêmes. Après avoir analysé la taille des données à stocker, j'ai conclu qu'elles s'adaptent facilement.

Au début, je viens d'utiliser JSON, mais il l'a très vite refusé quand il s'est heurté à une limite. Dans Telegram, le contenu des données de bouton ne peut pas dépasser 64 octets en UTF-8 .

Par conséquent, avec l'invite d'un ami, j'ai commencé à regarder pack à partir du module struct . Ainsi les types de requêtes sont nés, le format primitif, le packaging et le déballage. Maintenant, il est utilisé partout dans le bot.

Le format est très simple. Le premier octet est le type, le second est la version. Tout le reste est constitué de données pour un type particulier. Les types sont stockés comme Enum, ont un ID , qui est le premier octet. En plus de l' ID , chaque type a un format pour l'emballage et le déballage des données. Par exemple: tapez SHOW_TRACK_MENUdont le format a la valeur "s?", où "s" est l'identifiant unique de la piste et "?" - La piste contient-elle du texte?

A pistes utilisées type chaîne parce que: premièrement, ID de la piste peut être une concaténation d' ID et de l' album ID de la piste à travers le côlon, et, d' autre part , il peut être l'UUID . Pistes avec UUID - pistes auto-chargées, disponibles uniquement pour l'utilisateur qui les a téléchargées.

Étant donné que les données ne correspondent pas toujours au format, par exemple, le même ID de piste peut être représenté simplement par un numéro, avant de le compresser, il doit être converti en un type pour le format. Dans ce cas, s. Par conséquent, dans la classe, il existe une méthode qui normalise les données transférées pour l'empaquetage, afin de ne pas le faire vous-même lors de la transmission au constructeur.

Les cordes sont autosuffisantes et peuvent indiquer leur longueur lors de l'emballage et en tenir compte lors du déballage.

La prise en charge des anciennes versions n'était pas prévue, donc une exception est levée si les versions ne correspondent pas. Lors du traitement des mises à jour, dont je parlerai dans le paragraphe suivant, la logique nécessaire est appelée.

Comme Telegram mange exclusivement UTF-8 , les données compressées sont encodées en base85 . Oui, je perds de la vitesse ici et j'enregistre la plus petite taille sans utiliser la base64 , mais étant donné les petites données, je pense utiliser la base85 appropriée.

La source. Fichier callback_data.py
import struct
import base64

from ext.query_types import QueryType


class BadDataVersion(Exception):
    pass


class CallbackData:
    ACTUAL_VERSION = 7
    BASE_FORMAT = '<BB'

    def __init__(self, type_: QueryType, data=None, version=None):
        self.type = type_
        self.version = version or CallbackData.ACTUAL_VERSION
        self.data = data

        if self.data is not None and not isinstance(self.data, list):
            self.data = [self.data]

        if self.data is not None:
            self.data = self._normalize_data_to_format(self.data)

    def __repr__(self):
        return f'<CallbackData> Type: {self.type} Version: {self.version} Data: {self.data}'

    def _normalize_data_to_format(self, data, bytes_object=False):
        normalized_data = data.copy()
        for i, c in enumerate(self.type.format):
            cast = str
            if c.lower() in 'bhilqn':
                cast = int
            elif c in 'efd':
                cast = float
            elif c == '?':
                cast = bool

            casted = cast(data[i])
            if bytes_object and cast == str:
                casted = casted.encode('utf-8')
            normalized_data[i] = casted

        return normalized_data

    @staticmethod
    def decode_type(callback_data):
        decoded = base64.b85decode(callback_data.encode('utf-8'))
        type_, version = struct.unpack(CallbackData.BASE_FORMAT, decoded[:2])

        if CallbackData.ACTUAL_VERSION != version:
            raise BadDataVersion()

        return QueryType(type_), version, decoded

    @classmethod
    def decode(cls, type_, version, decoded):
        start, data = 2, []

        if start < len(decoded):
            format_iter = iter(type_.format)

            while True:
                if start >= len(decoded):
                    break

                format_ = next(format_iter, type_.format[-1])

                decode_str = format_ in 'ps'
                if decode_str:
                    # struct.calcsize('b') = 1
                    length = list(struct.unpack('b', decoded[start: start + 1]))[0]
                    start += 1

                    format_ = f'{length}{format_}'

                step = struct.calcsize(format_)

                unpacked = list(struct.unpack(f'{format_}', decoded[start: start + step]))
                if decode_str:
                    unpacked[0] = unpacked[0].decode('UTF-8')

                data += unpacked
                start += step

        return cls(type_, data if data else None, version)

    def encode(self):
        encode = struct.pack(self.BASE_FORMAT, self.type.value, self.version)

        if self.data is not None:
            format_iter = iter(self.type.format)
            normalized_data = self._normalize_data_to_format(self.data, bytes_object=True)

            for data in normalized_data:
                format_ = next(format_iter, self.type.format[-1])

                if format_ in 'ps':
                    #        .  'b'  
                    # -      ,    > 36 
                    encode += struct.pack('b', len(data))
                    encode += struct.pack(f'{len(data)}{format_}', data)
                else:
                    encode += struct.pack(f'{format_}', data)

        return base64.b85encode(encode).decode('utf-8')



3. Mises à jour et contexte du routage


Le projet utilise la bibliothèque python-telegram-bot pour fonctionner avec l' API Telegram Bot . Il possède déjà un système d'enregistrement des gestionnaires pour certains types de mises à jour qui sont arrivés, des filtres pour les expressions régulières, les commandes, etc. Mais, étant donné mon propre format de données et mes types, j'ai dû hériter de TelegramHandler et implémenter mon gestionnaire .

La mise à jour et le contexte sont transmis à chaque gestionnaire via des arguments. Dans ce cas, j'ai mon propre contexte et c'est dans Handler'eil est en cours de formation, c'est-à-dire: recevoir et / ou ajouter un utilisateur à la base de données, vérifier la pertinence du token pour accéder à la musique, initialiser le client Yandex.Music, en fonction du statut d'autorisation et de la disponibilité d'un abonnement.

Plus loin de mon Handler'a, il existe des gestionnaires plus spécifiques, par exemple, CallbackQueryHandler . Avec lui, un gestionnaire est enregistré pour un certain type de mise à jour (mon type, avec un format de données). Pour vérifier si cette mise à jour convient au gestionnaire actuel, toutes les données ne sont pas décompressées, mais uniquement les deux premiers octets. À ce stade, la nécessité de lancer un rappel est vérifiée. Ce n'est que si le lancement du rappel est nécessaire - que les données sont complètement décompressées et transférées sous forme de kwargsau gestionnaire final. Immédiatement, il y a un envoi de données analytiques à ChatBase .

L'enregistrement se fait séquentiellement, et la priorité est plus élevée pour qui sera enregistré plus tôt (en fait, comme dans le routage Django et dans d'autres projets). Par conséquent, l'enregistrement d'un gestionnaire pour une version obsolète est le premier parmi les gestionnaires CallBackQuery .

La logique de traitement de la version obsolète est simple - informez l'utilisateur à ce sujet et envoyez des données mises à jour, si possible.

4. Services


Tous les services sont initialisés lorsque le bot est lancé dans une classe de contrôle, qui est ensuite universellement utilisée n'importe où dans le bot ( DJ ).

Chaque service a son propre ThreadPoolExecutor avec un certain nombre de travailleurs dans lesquels les tâches sont soumises.

Recharger une piste dans Telegram


Pour le moment, ce service n'a pas été réécrit dans User Bot pour contourner la limite de la taille du fichier téléchargé dans Telegram. Il s'est avéré que Yandex.Music contient des fichiers de plus de 50 Mo - des podcasts.

Le service vérifie la taille du fichier et, en cas d'excès, lance une alerte à l'utilisateur. Grâce au système de mise en cache décrit au paragraphe 5, cela vérifie la disponibilité et la réception des paroles. Les pistes sont également mises en cache. Le hachage du fichier est stocké dans la base de données. S'il y en a un, l'audio avec un cache connu est envoyé.

En l'absence de fichier dans la base de données, un lien direct est reçu de Yandex.Music. Bien que pour le moment, les utilisateurs n'ont pas la possibilité de modifier les paramètres de qualité, mais tous sont définis sur des valeurs standard. Le fichier est recherché par bitrate et codec à partir des paramètres utilisateur.

Le fichier et sa couverture sont téléchargés en tant que tempfile.TemporaryFile () , après quoi ils sont téléchargés sur Telegram. Il est à noter que TG ne reconnaît pas toujours correctement la durée du morceau, mais je me tais généralement sur l'artiste et le titre. Par conséquent, ces données sont extraites de Yandex, heureusement, il est possible de les transmettre au panier.

Lorsqu'un fichier audio est envoyé par ce service, finish_callback () est appelé , signalant le service par abonnement à la fin du téléchargement.

Service d'abonnement pour recevoir des pistes et envoyer l'état du téléchargement


Les pistes ne sont pas chargées instantanément. Il est possible que plusieurs utilisateurs aient demandé la même piste. Dans ce cas, l'utilisateur qui a demandé la piste en premier est l'initiateur et le propriétaire de la piste. Lors du rechargement de plus d'une seconde, l'état de téléchargement démarre: «L'utilisateur envoie un message vocal». Les autres utilisateurs qui ont demandé la même piste sont des abonnés réguliers. Ils, comme le propriétaire, reçoivent l'état de téléchargement une fois toutes les ~ 4 secondes afin que le message ne s'interrompt pas (l'état se bloque pendant 5 secondes). Dès que le téléchargement de la piste pour le propriétaire est terminé, done_callback () est appelé à partir du service ci-dessus. Après cela, tous les abonnés sont supprimés de la liste d'état et reçoivent la piste téléchargée.

Dans la solution architecturale, le propriétaire de la piste est également abonné, mais avec une certaine marque, car les modalités d'envoi de la piste sont différentes.

5. Mise en cache des requêtes


Comme nous nous souvenons de mon dernier article, les demandes à l'API Yandex.Music sont très lourdes. La liste des pistes peut être de 3 ou 5 Mo. De plus, il y a juste beaucoup de demandes. A chaque traitement de mise à jour, au moins 2 requêtes sont envoyées à Yandex: pour initialiser le client et pour une action spécifique. Dans certains endroits, pour collecter suffisamment d'informations (par exemple, pour une liste de lecture), vous devez faire une demande de liste de lecture, pour recevoir ses pistes, pour obtenir des informations de la page de destination (s'il s'agit d'une liste de lecture intelligente), et n'oubliez pas l'initialisation du client. En général, horreur tranquille en termes de nombre de demandes.

Je voulais quelque chose de très universel et de ne pas faire de stockage pour certains objets, les mêmes clients.

Étant donné que la bibliothèque vous permet de spécifier votre propre instance pour l'exécution des requêtes, en plus des demandes, alors j'en ai profité.

Le point est simple. La classe de cache elle-même est un singleton. Il n'a que deux paramètres: durée de vie du cache, taille. Lorsque la demande est exécutée, l'encapsuleur est appelé. Il est outrepassé. La vérification du cache se produit par le hachage d'arguments et de quarts gelés. Le cache a le temps d'ajouter. Lors de la vérification de la nécessité de mettre à jour les données, soit les données de LimitedSizeDict sont obtenues, soit une demande réelle est effectuée et ajoutée au cache.

Certaines demandes ne peuvent pas être mises en cache, par exemple, en définissant un J'aime / Je n'aime pas. Si l'utilisateur appuie sur la séquence suivante: j'aime, je n'aime pas, j'aime, alors finalement, il ne sera pas livré. Dans de tels cas, lors de l'envoi d'une demande, vous devez passer l'argument use_cache avec une valeur égale à False. En fait, c'est le seul endroit où le cache n'est pas utilisé.

Grâce à cela, je fais les demandes les plus audacieuses pour qu'elles soient mises en cache. Je n'essaye pas de le diviser en petits et je n'ai besoin que de la page actuelle. Je prends tout en même temps, et lorsque je passe d'une page à l'autre, j'ai une vitesse de commutation énorme (par rapport à l'ancienne approche).

Quant à moi, la classe de demande en cache s'est avérée magnifiquement et a été simplement intégrée.

La source
import copy
import time

from typing import Union
from collections import OrderedDict

import requests

from yandex_music.utils.request import Request as YandexMusicRequest


class LimitedSizeDict(OrderedDict):
    def __init__(self, *args, **kwargs):
        self.size_limit = kwargs.pop("size_limit", None)
        OrderedDict.__init__(self, *args, **kwargs)
        self._check_size_limit()

    def __setitem__(self, key, value):
        OrderedDict.__setitem__(self, key, value)
        self._check_size_limit()

    def _check_size_limit(self):
        if self.size_limit is not None:
            while len(self) > self.size_limit:
                self.popitem(last=False)


class CachedItem:
    def __init__(self, response: requests.Response):
        self.timestamp = time.time()
        self.response = response


class Cache:
    __singleton: 'Cache' = None

    def __init__(self, lifetime: Union[str, int], size: Union[str, int]):
        Cache.__singleton = self

        self.lifetime = int(lifetime) * 60
        self.size = int(size)

        self.storage: LimitedSizeDict = LimitedSizeDict(size_limit=int(size))

    def get(self, *args, **kwargs):
        hash_ = Cache.get_hash(*args, **kwargs)

        return self.storage[hash_].response

    def update(self, response: requests.Response, *args, **kwargs):
        hash_ = Cache.get_hash(*args, **kwargs)
        self.storage.update({hash_: CachedItem(response)})

    def need_to_fetch(self, *args, **kwargs):
        hash_ = Cache.get_hash(*args, **kwargs)
        cached_item = self.storage.get(hash_)

        if not cached_item:
            return True

        if time.time() - cached_item.timestamp > self.lifetime:
            return True

        return False

    @classmethod
    def get_instance(cls) -> 'Cache':
        if cls.__singleton is not None:
            return cls.__singleton
        else:
            raise RuntimeError(f'{cls.__name__} not initialized')

    @staticmethod
    def freeze_dict(d: dict) -> Union[frozenset, tuple, dict]:
        if isinstance(d, dict):
            return frozenset((key, Cache.freeze_dict(value)) for key, value in d.items())
        elif isinstance(d, list):
            return tuple(Cache.freeze_dict(value) for value in d)

        return d

    @staticmethod
    def get_hash(*args, **kwargs):
        return hash((args, Cache.freeze_dict(kwargs)))


class Request(YandexMusicRequest):
    def _request_wrapper(self, *args, **kwargs):
        use_cache = kwargs.get('use_cache', True)

        if 'use_cache' in kwargs:
            kwargs.pop('use_cache')

        if not use_cache:
            response = super()._request_wrapper(*args, **copy.deepcopy(kwargs))
        elif use_cache and Cache.get_instance().need_to_fetch(*args, **kwargs):
            response = super()._request_wrapper(*args, **copy.deepcopy(kwargs))
            Cache.get_instance().update(response, *args, **kwargs)
        else:
            response = Cache.get_instance().get(*args, **kwargs)

        return response



6. Suivi de la reconnaissance par message vocal


Au début, il n'était pas question d'ajouter cela au bot, mais une situation intéressante s'est produite. Le bot a un chat (cela est indiqué dans la description du bot). Après un certain temps, j'ai remarqué que les gens y vont et envoient des messages vocaux avec de la musique. Au début, je pensais que c'était un nouveau type de spam tel que quelqu'un avait mis en bouteille et plaisantait. Mais, lorsque de telles personnes avaient déjà moins de 10 ans et que tout le monde faisait la même chose, mon ami (le même qui a suggéré la structure ) a suggéré que les utilisateurs conduisent Yandex.Music dans la recherche, en attendant que le bot officiel reconnaisse la musique de Yandex, ils y voient une salle de chat et y envoient des messages vocaux avec sérieux! C'était juste une hypothèse brillante et vraie. Puis j'ai plaisanté en disant qu'il était temps de faire la reconnaissance et d'ajouter un bot au chat. Pour le plaisir ... Après un moment, cela a été fait!

Maintenant sur l' API . Récemment, Yandex a de plus en plus utilisé des sockets Web. J'ai rencontré leur utilisation dans la gestion d'un i.module et d'un i.station. Le service de reconnaissance musicale y travaille également. J'ai lancé la solution de travail minimale dans mon bot, mais je n'ai pas ajouté l'implémentation à la bibliothèque.

WS est situé à l'adresse suivante: wss: //voiceservices.yandex.net/uni.ws
Nous n'avons besoin que de deux messages - autorisation et demande de reconnaissance .
Yandex lui-même, dans son application officielle, envoie des fichiers courts en une seconde ou trois. En réponse, vous pouvez avoir besoin d'envoyer plus de données ou le résultat - trouvé ou non. Si le résultat est trouvé, l'ID de piste sera retourné. Les fichiers .gg sont

envoyés avecENCODER = SpeechKit Mobile SDK v3.28.0 . Je n'ai pas vérifié comment cela fonctionne avec les autres encodeurs, je le change juste dans le fichier enregistré par Telegram.

Lors d'une marche arrière avec une prise Web, la magie se produisait parfois. Parfois, je ne pouvais pas trouver la piste, mais quand j'ai changé la langue dans le message avec la demande de reconnaissance, je l'ai fait. Ou au début, il a trouvé, puis il s'est arrêté, bien que le fichier soit le même. Je pensais que la langue de la piste était définie par leur SpeechKit sur le client. N'ayant pas une telle opportunité de le faire moi-même, je fais une recherche par force brute.

Mon implémentation à genoux de la reconnaissance vocale par messages vocaux de Telegram
import uuid

import lomond


def get_auth_data():
    return {
        "event": {
            "header": {
                "messageId": str(uuid.uuid4()),
                "name": "SynchronizeState",
                "namespace": "System"
            },
            "payload": {
                "accept_invalid_auth": True,
                "auth_token": "5983ba91-339e-443c-8452-390fe7d9d308",
                "uuid": str(uuid.uuid4()).replace('-', ''),
            }
        }
    }


def get_asr_data():
    return {
        "event": {
            "header": {
                "messageId": str(uuid.uuid4()),
                "name": "Recognize",
                "namespace": "ASR",
                "streamId": 1
            },
            "payload": {
                "advancedASROptions": {
                    "manual_punctuation": False,
                    "partial_results": False
                },
                "disableAntimatNormalizer": False,
                "format": "audio/opus",
                "music_request2": {
                    "headers": {
                        "Content-Type": "audio/opus"
                    }
                },
                "punctuation": False,
                "tags": "PASS_AUDIO;",
                "topic": "queries"
            }
        }
    }


class Recognition:
    URI = 'wss://voiceservices.yandex.net/uni.ws'
    LANGS = ['', 'ru-RU', 'en-US']
    POLL_DELAY = 0.3

    def __init__(self, binary_data, status_msg):
        self.status_msg = status_msg
        self.websocket = lomond.WebSocket(self.URI)
        self.chunks = self.get_chunks_and_replace_encoder(binary_data)

    def get_track_id(self):
        for lang in Recognition.LANGS:
            asr_data = get_asr_data()
            if lang:
                asr_data['event']['payload'].update({'lang': lang})
                self.status_msg.edit_text(f'     {lang}...')
            else:
                self.status_msg.edit_text(f'      ...')

            for msg in self.websocket.connect(poll=self.POLL_DELAY):
                if msg.name == 'ready':
                    self.websocket.send_json(get_auth_data())
                    self.websocket.send_json(asr_data)

                    for chunk in self.chunks:
                        self.websocket.send_binary(chunk)

                if msg.name == 'text':
                    response = msg.json.get('directive')

                    if self.is_valid_response(response):
                        self.websocket.close()

                        return self.parse_track_id(response)
                    elif self.is_fatal_error(response):
                        self.websocket.close()

                        break

    def is_valid_response(self, response):
        if response.get('header', {}).get('name') == 'MusicResult' and \
                response.get('payload', {}).get('result') == 'success':
            self.status_msg.edit_text(f' ,  !')
            return True
        return False

    @staticmethod
    def is_fatal_error(response):
        if response.get('header', {}).get('name') == 'MusicResult' and \
                response.get('payload', {}).get('result') == 'music':
            return False
        return True

    @staticmethod
    def parse_track_id(response):
        return response['payload']['data']['match']['id']

    @staticmethod
    def get_chunks_and_replace_encoder(binary_data):
        chunks = []

        for chunk in binary_data.split(b'OggS')[1:]:
            if b'OpusTags' in chunk:
                pos = chunk.index(b'OpusTags') + 12
                size = len(chunk)
                chunk = chunk[:pos] + b'#\x00\x00\x00\x00ENCODER=SpeechKit Mobile SDK v3.28.0'
                chunk += b"\x00" * (size - len(chunk))

            chunks.append(b'\x00\x00\x00\x01OggS' + chunk)

        return chunks


, .

7. Petites notes


Initialement, seuls les utilisateurs abonnés pouvaient utiliser le bot car le service peut être utilisé dans un nombre limité de pays (sans abonnement), et le serveur avec le bot est situé en Europe. Le problème est résolu en utilisant un proxy pour exécuter les demandes des utilisateurs sans abonnement. Le serveur proxy est situé à Moscou.

Il y a un choix de pages, mais il est limité à 100 (plus aucun bouton ne peut être ajouté, restriction de télégramme). Certaines demandes de page courantes ont beaucoup plus de pages.

Dans la recherche Yandex, le nombre d'éléments sur la page est codé en dur. Combien de pistes, combien de listes de lecture. Parfois, cela ne correspond même pas à la quantité de données affichées sur le devant. Il y a un changement de page, le nombre d'éléments est flottant. Par conséquent, dans le bot, il saute également, ce qui n'est pas très beau. Et pour combiner leur paginateur avec le sien - quelque chose comme ça. Dans d'autres endroits où il est possible de demander un certain nombre d'éléments par page, tout est parfait. Le message vocal après avoir implémenté la recherche dans le bot est t.me/MarshalC/416 .

Lors du transfert audio dans Telegram, l'auteur est perdu et affecté à celui qui a fait le transfert. Par conséquent, toutes les pistes sont envoyées avec la signature du nom d'utilisateur du bot.

Message vocal avec tout ce que j'ai rencontré après la mise en place de la radio dans la bibliothèque - t.me/MarshalC/422(à propos de la chaîne de pistes, en passant par l'envoi d'un tas de commentaires, batch_id ).

Conclusion


Malgré le fait qu'il s'agisse d'un autre article sur le bot Telegram, vous l'avez lu jusqu'ici, probablement parce que vous étiez intéressé par l'un des points de la coupe et c'est merveilleux, merci beaucoup !

Malheureusement, je n'ai pas entièrement ouvert le code source du bot (car dans certains endroits j'ai besoin de refactoriser). Beaucoup a été décrit dans cet article, mais certains aspects, par exemple, avec les claviers virtuels et leur génération ne sont pas affectés. Pour l'essentiel, ce qui n'est pas dans l'article fonctionne simplement avec ma bibliothèque, rien d'intéressant.

Les classes autour desquelles tout tourne, je les ai montrées sous la forme dans laquelle elles se trouvent maintenant. J'avoue qu'il y a des bugs, mais tout fonctionne depuis longtemps. À certains endroits, j'aime mon code, à certains endroits, je n'aime pas - et c'est normal. N'oubliez pas que travailler avec WS pour la reconnaissance est une solution sur le genou. Prêt à lire des critiques motivées dans les commentaires.

Bien que le bot ait été planifié même lorsque j'ai commencé à écrire la bibliothèque, j'ai renié cette idée, mais, apparemment, je suis revenu (c'était ennuyeux).

Yandex.Music Bot - un projet qui prouve l'utilité d'utiliser la bibliothèque pour travailler avec l'API dans les projets.

Un grand merci à maman, Yana, Sana'a, Glory. Quelqu'un pour les erreurs de relecture, certains pour les conseils, sans lesquels certains points de cet article pourraient ne pas avoir existé, et pour certains simplement pour évaluer l'article avant la publication. Arthur pour picci pour l'article, Lyod pour le logo.

PS Maintenant, j'ai un problème aigu avec la distribution après étude. Si vous êtes prêt à me téléphoner - dites-moi où envoyer le CV, interviewez-moi, s'il vous plaît. Tous mes contacts sont dans le profil. Le temps presse, j'espère pour vous. En vertu de la loi, je ne peux m'entraîner qu'en République du Bélarus.

PPS C'était le deuxième article de trois, mais sans la moindre idée, j'aurai le temps de faire un autre projet sur ce sujet et si c'est nécessaire. Je vais dévoiler publiquement son sujet - le client multiplateforme Yandex.Music.

Source: https://habr.com/ru/post/undefined/


All Articles