Unter der Haube des Yandex.Music Bot-Clients

Einführung


Hallo Habr! Wieder bin ich mit dem zweiten Artikel, der die Yandex.Music-API betrifft. Der Fall wird im ersten Artikel geplant und erwähnt .

Hände erreicht, fertig. Heute werde ich über interessante Momente sprechen, die meiner Meinung nach in der Codebasis meines Telegramm-Bots vorhanden sind, der sich als vollwertiger Kunde von Musik positioniert. Wir werden auch die Yandex-Musikerkennungs-API berühren.

Bevor Sie mit der Punkt-für-Punkt-Geschichte der Implementierung einer bestimmten Sache fortfahren, sollten Sie sich ein Bild über den Bot selbst und seine funktionalen Fähigkeiten machen.

Client-Video-Demo


Im Hauptteil werde ich über Folgendes sprechen:

  1. Melden Sie sich über die Website auf GitHub-Seiten in Ihrem Konto an (warum und warum).
  2. Das Datenformat, seine Verpackung und Verwendung in Schaltflächendaten.
  3. Aktualisieren Sie das Routing, die Datenversionierung und den Kontext, der in Handler geworfen wird.
  4. Dienstleistungen:
    • Service-Reload-Track im Telegramm.
    • Der Dienst der "Abonnements", um einen Titel mit dem Senden des Status des Downloads zu erhalten.
  5. Die einfachste und eleganteste Implementierung für das Zwischenspeichern von Abfragen.
  6. Erkennung eines Titels durch Sprachnachricht und wie er im Bot allgemein angezeigt wird.
  7. Kleine Notizen.

Wenn Sie an mindestens einem Punkt interessiert sind - willkommen bei cat.
Der Bot arbeitet mit Benutzern, die sich in ihrem Konto angemeldet haben, aber nein. Alles dreht sich um vier Hauptdienstklassen: Album, Künstler, Wiedergabeliste und Titel. Alle Entitätsdaten werden unterstützt, es gibt eine Paginierung mit Seitenauswahl. Um mit ihnen zu interagieren, kann ein autorisierter Benutzer sein persönliches Menü verwenden, das Folgendes umfasst: intelligente Wiedergabelisten, die Wiedergabeliste „Ich mag“ und eigene Wiedergabelisten. Für nicht autorisierte Benutzer ist eine Suche verfügbar, bei der eine Suchanfrage als reguläre Nachricht gesendet wird. Jetzt gibt es nur noch eine trockene Liste dessen, was es sonst noch gibt: Empfangen von Titeln und anderen Objekten über einen direkten Link, Empfangen von Texten, die Möglichkeit, den Titel und den Künstler zu mögen / nicht zu mögen, ähnliche Titel anzuzeigen, den Titel anhand einer Sprachnachricht zu erkennen und vieles mehr.

Hauptteil


1. Melden Sie sich bei Ihrem Konto an


Ursprünglich war nicht geplant, dass der Bot ohne Autorisierung funktioniert. Daher wurde auch während des Entwurfs entschieden, wie die Autorisierung erfolgen soll - über seine Website . Die Hauptargumente waren Sicherheit und Transparenz. In keinem Fall wollte Benutzeranmeldungen und Passwörter in klaren Nachrichten nicht akzeptieren. Die Autorisierung vom Computer des Benutzers ist elementar. Daher wurde eine Site zur Autorisierung von React geschrieben . Es ist eine Form der Autorisierung mit einer Weiterleitung zurück zum Bot. Ein Stück mit Autorisierung und Verarbeitung von Captcha wurde aus der Bibliothek in Python entnommen und in JavaScript neu geschrieben .

Zur Autorisierung wird das empfangene OAuth-Token verwendet, das über Deep Linking zurück an den Bot übertragen wird.

Der letzte Link für die Umleitung sieht folgendermaßen aus: t.me/music_yandex_bot?start={oauth_token}

Ich wollte es besser, aber die Benutzer hatten kein Vertrauen. Nur jeder zehnte Benutzer wurde autorisiert (zu dem Zeitpunkt, als dies obligatorisch war). Daher musste ich mit den folgenden Updates erklären, warum Sie vertrauen sollten. Alle Artikel zu HTTPS , ausschließlich zu Anfragen von Seiten des Clients und Open Source Code, wurden auf der Site zur Autorisierung veröffentlicht und in einer Begrüßungsnachricht im Bot dupliziert. Wurde besser.

Unter den Faktoren geringer Autorisierung erwies sich die Unzugänglichkeit von Spiegeln für t.me in Russland als Unterstützung nur für Benutzer mit einem Abonnement (mehr dazu im Anhang, Absatz 7).

Das erste Element wurde mithilfe des URI ( tg: // ) aufgelöst. Zur Not gibt es bereits einen Link zum Spiegel (wenn die automatische Umleitung nicht funktioniert hat und die Schaltfläche nicht geholfen hat), und das zweite Element befindet sich wieder in Element 7.


2. Datenformat


Für diesen Bot habe ich mich zum ersten Mal für NoSQL DB - MongoDB entschieden . Verstanden: Wenn ein Dokument eingebettet ist , wenn nicht. Arbeitete gern mit Mongoengine. Aber ich wollte wirklich nicht alle Schaltflächendaten zu Hause speichern, und der Client hat nur die Datensatz- ID in der Datenbank. Ich hatte Angst vor der Datenmenge, wollte aber einen kostenlosen Mongo-Server mit einem Limit von 512 MB für die Datenspeicherung verwenden. Es ist viel schwieriger, eine schöne Wiederverwendung von Datensätzen zu finden, wenn die Daten in mehreren Schaltflächen übereinstimmen, und veraltete zu bereinigen, als alles in den Schaltflächen selbst zu speichern. Nachdem ich die Größe der zu speichernden Daten analysiert hatte, kam ich zu dem Schluss, dass sie leicht passen.

Zuerst habe ich nur JSON verwendet, aber er lehnte es sehr schnell ab, als er an ein Limit stieß. In Telegramm darf der Inhalt der Schaltflächendaten in UTF-8 nur 64 Byte betragen .

Daher begann ich mit der Aufforderung eines Freundes, das Paket aus dem Strukturmodul zu betrachten . So wurden die Arten von Abfragen geboren, das primitive Format, das Verpacken und Auspacken. Jetzt wird es im Bot absolut überall eingesetzt.

Das Format ist sehr einfach. Das erste Byte ist der Typ, das zweite die Version. Alles andere sind Daten für einen bestimmten Typ. Typen werden als Enum gespeichert und haben eine ID , die das erste Byte ist. Zusätzlich zur ID verfügt jeder Typ über ein Format zum Packen und Entpacken von Daten. Beispiel: Geben Sie SHOW_TRACK_MENU einWessen Format hat den Wert "s?", wobei "s" die eindeutige Kennung der Spur ist und "?" - Hat der Track Text?

Bei Tracks wird ein String-Typ verwendet, weil: Erstens kann die ID des Tracks eine Verkettung von ID und Album- ID des Tracks durch den Doppelpunkt sein, und zweitens kann es sich um die UUID handeln . Tracks mit UUID - selbst geladene Tracks, die nur dem Benutzer zur Verfügung stehen, der sie heruntergeladen hat.

Da die Daten beispielsweise nicht immer dem Format entsprechen, kann dieselbe Spur- ID einfach durch eine Zahl dargestellt werden. Vor dem Packen muss sie in einen Typ für das Format umgewandelt werden. In diesem Fall s. Daher gibt es in der Klasse eine Methode, die die übertragenen Daten für das Packen normalisiert, um dies beim Übergeben an den Konstruktor nicht selbst zu tun.

Saiten sind autark und können ihre Länge beim Verpacken angeben und diese Länge beim Auspacken berücksichtigen.

Die Unterstützung älterer Versionen war nicht geplant, daher wird eine Ausnahme ausgelöst, wenn die Versionen nicht übereinstimmen. Bei der Verarbeitung von Updates, auf die ich im nächsten Absatz eingehen werde, wird die erforderliche Logik aufgerufen.

Da Telegram ausschließlich UTF-8 isst , werden die gepackten Daten in base85 codiert . Ja, ich verliere hier an Geschwindigkeit und speichere die kleinste Größe ohne Verwendung von base64 , aber angesichts der kleinen Daten halte ich die Verwendung von base85 für angemessen.

Quelle. Datei 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. Routing von Updates und Kontext


Das Projekt verwendet die Python-Telegramm-Bot- Bibliothek , um mit der Telegramm-Bot-API zu arbeiten . Es gibt bereits ein System zum Registrieren von Handlern für bestimmte eingetroffene Aktualisierungstypen, Filter für reguläre Ausdrücke, Befehle usw. Aufgrund meines eigenen Datenformats und meiner Typen musste ich jedoch von TelegramHandler erben und meinen Handler implementieren .

Update und Kontext werden über Argumente an jeden Handler übergeben. In diesem Fall habe ich meinen eigenen Kontext und er befindet sich in Handler'eEs wird gebildet, und dies ist: Empfangen und / oder Hinzufügen eines Benutzers zur Datenbank, Überprüfen der Relevanz des Tokens für den Zugriff auf Musik, Initialisieren des Yandex.Music-Clients, abhängig vom Autorisierungsstatus und der Verfügbarkeit eines Abonnements.

Weiter von meinem Handler'a gibt es spezifischere Handler, zum Beispiel CallbackQueryHandler . Damit wird ein Handler für eine bestimmte Art von Aktualisierung registriert (mein Typ, mit einem Datenformat). Um zu überprüfen, ob dieses Update für den aktuellen Handler geeignet ist, werden nicht alle Daten entpackt, sondern nur die ersten beiden Bytes. Zu diesem Zeitpunkt wird überprüft, ob ein Rückruf gestartet werden muss. Nur wenn das Starten des Rückrufs erforderlich ist - werden die Daten vollständig entpackt und als kwargs übertragenzum letzten Handler. Ab sofort werden analytische Daten an ChatBase gesendet .

Die Registrierung erfolgt nacheinander, und die Priorität ist höher für diejenigen, die früher registriert werden (tatsächlich wie beim Django- Routing und in anderen Projekten). Daher ist die Registrierung eines Handlers für eine veraltete Version die erste unter den CallBackQuery-Handlern .

Die Logik der Verarbeitung der veralteten Version ist einfach - informieren Sie den Benutzer darüber und senden Sie nach Möglichkeit aktualisierte Daten.

4. Dienstleistungen


Alle Dienste werden initialisiert, wenn der Bot in einer Steuerungsklasse gestartet wird, die dann überall im Bot ( DJ ) universell verwendet wird .

Jeder Dienst verfügt über einen eigenen ThreadPoolExecutor mit einer bestimmten Anzahl von Mitarbeitern, an die Aufgaben gesendet werden.

Einen Track in Telegram neu laden


Derzeit wurde dieser Dienst nicht in User Bot umgeschrieben, um die Beschränkung der Größe der heruntergeladenen Datei in Telegram zu umgehen. Wie sich herausstellte, gibt es in Yandex.Music Dateien, die größer als 50 MB sind - Podcasts.

Der Dienst überprüft die Dateigröße und gibt im Falle eines Überschusses eine Warnung an den Benutzer aus. Dank des in Absatz 5 beschriebenen Caching-Systems wird die Verfügbarkeit und der Empfang der Texte überprüft. Tracks werden ebenfalls zwischengespeichert. Der Hash der Datei wird in der Datenbank gespeichert. Wenn es einen gibt, wird Audio mit einem bekannten Cache gesendet.

Wenn keine Datei in der Datenbank vorhanden ist, wird ein direkter Link von Yandex.Music empfangen. Im Moment haben Benutzer zwar nicht die Möglichkeit, die Qualitätseinstellungen zu ändern, aber alle sind auf Standardwerte eingestellt. Die Datei wird in den Benutzereinstellungen nach Bitrate und Codec gesucht.

Die Datei und ihr Cover werden als tempfile.TemporaryFile () heruntergeladen und anschließend in Telegram hochgeladen. Es ist erwähnenswert, dass TG die Dauer des Tracks nicht immer richtig erkennt, aber ich schweige im Allgemeinen über den Künstler und den Titel. Daher stammen diese Daten von Yandex, zum Glück ist es möglich, sie in den Warenkorb zu übertragen.

Wenn eine Audiodatei von diesem Dienst gesendet wird, wird finish_callback () aufgerufen , um den Dienst per Abonnement gegen Ende des Downloads zu signalisieren.

Abonnement-Service zum Empfangen von Titeln und zum Senden des Download-Status


Tracks werden nicht sofort geladen. Es ist möglich, dass mehrere Benutzer denselben Titel angefordert haben. In diesem Fall ist der Benutzer, der den Track zuerst angefordert hat, der Initiator und Eigentümer des Tracks. Wenn das Laden länger als eine Sekunde dauert, beginnt der Download-Status: "Der Benutzer sendet eine Sprachnachricht". Andere Benutzer, die denselben Titel angefordert haben, sind reguläre Abonnenten. Sie erhalten wie der Eigentümer alle ~ 4 Sekunden den Download-Status, damit die Nachricht nicht unterbrochen wird (der Status bleibt 5 Sekunden lang hängen). Sobald der Download des Titels für den Eigentümer abgeschlossen ist, wird finish_callback () vom obigen Dienst aufgerufen . Danach werden alle Abonnenten aus der Statusliste gelöscht und erhalten den heruntergeladenen Titel.

In der Architekturlösung ist der Eigentümer des Tracks auch ein Abonnent, jedoch mit einer bestimmten Markierung, da die Arten des Sendens des Tracks unterschiedlich sind.

5. Abfrage-Caching


Wie wir uns aus meinem letzten Artikel erinnern, sind die Anfragen an die Yandex.Music-API sehr hoch. Die Liste der Tracks kann 3 oder 5 MB groß sein. Darüber hinaus gibt es nur viele Anfragen. Bei jeder Aktualisierungsverarbeitung werden mindestens zwei Anforderungen an Yandex gesendet: zum Initialisieren des Clients und für eine bestimmte Aktion. Um genügend Informationen zu sammeln (z. B. für eine Wiedergabeliste), müssen Sie an einigen Stellen eine Wiedergabeliste anfordern, ihre Titel empfangen, Informationen von der Zielseite abrufen (wenn es sich um eine intelligente Wiedergabeliste handelt) und die Initialisierung des Clients nicht vergessen. Im Allgemeinen leiser Horror in Bezug auf die Anzahl der Anfragen.

Ich wollte etwas sehr Universelles und keine Speicherung für bestimmte Objekte, die gleichen Kunden.

Da Sie in der Bibliothek zusätzlich zu den Anforderungen Ihre eigene Instanz für die Ausführung von Abfragen angeben können, dann habe ich das ausgenutzt.

Der Punkt ist einfach. Die Cache-Klasse selbst ist ein Singleton. Es gibt nur zwei Parameter: Cache-Lebensdauer, Größe. Wenn die Anforderung ausgeführt wird, wird der Wrapper aufgerufen. Er wird überschrieben. Das Überprüfen des Caches erfolgt durch Hash von eingefrorenen Args und Quarts. Der Cache hat Zeit zum Hinzufügen. Bei der Überprüfung der Notwendigkeit der Aktualisierung der Daten werden entweder Daten von LimitedSizeDict abgerufen oder eine echte Anforderung gestellt und dem Cache hinzugefügt.

Einige Anforderungen können nicht zwischengespeichert werden, z. B. das Festlegen eines "Gefällt mir" / "Gefällt mir nicht". Wenn der Benutzer die folgende Sequenz drückt: "Gefällt mir", "Gefällt mir nicht", "Gefällt mir", wird am Ende das Gleiche nicht geliefert. In solchen Fällen müssen Sie beim Senden einer Anforderung das Argument use_cache mit einem Wert gleich False übergeben. Dies ist der einzige Ort, an dem der Cache nicht verwendet wird.

Dank dessen mache ich die kühnsten Anfragen, damit sie zwischengespeichert werden. Ich versuche nicht, es in kleine zu unterteilen, die nur für die aktuelle Seite benötigt werden. Ich nehme alles auf einmal und beim Wechseln zwischen Seiten habe ich eine enorme Umschaltgeschwindigkeit (im Vergleich zum alten Ansatz).

Die zwischengespeicherte Anforderungsklasse hat sich für mich als wunderschön herausgestellt und wurde einfach integriert.

Quelle
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. Verfolgen Sie die Erkennung per Sprachnachricht


Am Anfang gab es keinen Gedanken, dies dem Bot hinzuzufügen, aber es passierte eine interessante Situation. Der Bot hat einen Chat (dies wird in der Beschreibung des Bots angegeben). Nach einiger Zeit bemerkte ich, dass Leute hineingehen und Sprachnachrichten mit Musik senden. Zuerst dachte ich, es sei eine neue Art von Spam, so dass jemand einen Bottom machte und Spaß machte. Aber als es bereits 10 solcher Leute gab und alle das Gleiche taten, schlug mein Freund (derselbe, der von struct vorgeschlagen wurde ) vor, dass Benutzer Yandex.Music auf der Suche nach einem offiziellen Bot fahren sollten, um Musik von Yandex zu erkennen. Sie sehen dort einen Chatraum und senden ihm in aller Ernsthaftigkeit Sprachnachrichten! Es war nur eine brillante und wahre Annahme. Dann sagte ich scherzhaft, dass es Zeit sei, Anerkennung zu finden und dem Chat einen Bot hinzuzufügen. Zum Spaß ... Nach einer Weile war das erledigt!

Nun zur API . In letzter Zeit hat Yandex zunehmend Web-Sockets verwendet. Ich habe ihre Verwendung bei der Verwaltung eines i.module und einer i.station kennengelernt. Der Musikerkennungsdienst arbeitet auch daran. Ich habe die minimale Arbeitslösung in meinen Bot geworfen, aber die Implementierung nicht zur Bibliothek hinzugefügt.

WS befindet sich unter der folgenden Adresse: wss: //voiceservices.yandex.net/uni.ws
Wir benötigen nur zwei Nachrichten - Autorisierung und eine Anforderung zur Erkennung .
Yandex selbst sendet in seiner offiziellen Anwendung kurze Dateien in ein oder drei Sekunden. Als Antwort können Sie die Notwendigkeit erhalten, mehr Daten oder das Ergebnis zu senden - gefunden oder nicht. Wenn das Ergebnis gefunden wird, wird die Track-ID zurückgegeben. .Ogg- Dateien werden mit

gesendetENCODER = SpeechKit Mobile SDK v3.28.0 . Ich habe nicht überprüft, wie es mit anderen Encodern funktioniert, ich ändere es einfach in der von Telegram aufgezeichneten Datei.

Beim Rückwärtsfahren mit einem Web-Socket passierte manchmal Magie. Manchmal konnte ich den Titel nicht finden, aber als ich die Sprache in der Nachricht mit der Erkennungsanforderung änderte, tat ich es. Oder zuerst wurde es gefunden und dann gestoppt, obwohl die Datei dieselbe ist. Ich dachte, dass die Sprache des Tracks durch ihr SpeechKit auf dem Client festgelegt wird. Da ich nicht die Möglichkeit habe, es selbst zu tun, mache ich eine Brute-Force-Suche.

Meine kniegefertigte Implementierung der Spracherkennung durch Sprachnachrichten von Telegramm
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. Kleine Notizen


Anfänglich konnten nur Benutzer mit einem Abonnement den Bot verwenden, da der Dienst in einer begrenzten Anzahl von Ländern (ohne Abonnement) verwendet werden kann und sich der Server mit dem Bot in Europa befindet. Das Problem wird gelöst, indem ein Proxy verwendet wird, um Anforderungen von Benutzern ohne Abonnement auszuführen. Der Proxyserver befindet sich in Moskau.

Es gibt eine Auswahl an Seiten, diese ist jedoch auf 100 begrenzt (es können keine weiteren Schaltflächen hinzugefügt werden, Telegrammbeschränkung). Einige häufig verwendete Seitenanforderungen haben viel mehr Seiten.

Bei der Yandex-Suche wird die Anzahl der Elemente auf der Seite fest codiert. Wie viele Titel, wie viele Wiedergabelisten. Manchmal entspricht dies nicht einmal der Datenmenge, die auf der Vorderseite angezeigt wird. Es gibt einen Seitenwechsel, die Anzahl der Elemente schwebt. Daher springt es im Bot auch, was nicht sehr schön ist. Und ihren Paginator mit seinem eigenen zu kombinieren - so etwas. An anderen Stellen, an denen eine bestimmte Anzahl von Elementen pro Seite angefordert werden kann, ist alles perfekt. Die Sprachnachricht nach der Implementierung der Suche im Bot lautet t.me/MarshalC/416 .

Wenn Audio in Telegramm weitergeleitet wird, geht der Autor verloren und wird demjenigen zugewiesen, der die Weiterleitung vorgenommen hat. Daher werden alle Tracks mit der Signatur des Bot-Benutzernamens gesendet. Sprachnachricht

mit allem, was ich nach der Implementierung des Radios in der Bibliothek getroffen habe - t.me/MarshalC/422(über die Kette von Tracks, die mit dem Senden eines Haufens von Feedback, batch_id , durchlaufen werden ).

Fazit


Trotz der Tatsache, dass dies ein weiterer Artikel über den Telegramm-Bot ist, haben Sie ihn bis hierher gelesen, höchstwahrscheinlich, weil Sie an einem der Artikel im Schnitt interessiert waren und dies ist wunderbar, vielen Dank !

Leider habe ich den Quellcode des Bots nicht vollständig geöffnet (da ich an einigen Stellen eine Umgestaltung vornehmen muss). In diesem Artikel wurde viel beschrieben, aber einige Aspekte, beispielsweise bei virtuellen Tastaturen und deren Generierung, sind nicht betroffen. Zum größten Teil ist das, was nicht in dem Artikel steht, nur die Arbeit mit meiner Bibliothek, nichts Interessantes.

Die Klassen, um die sich alles dreht, habe ich in der Form gezeigt, in der sie jetzt sind. Ich gebe zu, dass es Fehler gibt, aber alles funktioniert schon lange. An einigen Stellen mag ich meinen Code, an einigen Stellen nicht - und das ist normal. Vergessen Sie nicht, dass die Arbeit mit WS zur Erkennung eine Lösung für das Knie ist. Bereit, begründete Kritik in den Kommentaren zu lesen.

Obwohl der Bot schon geplant war, als ich anfing, die Bibliothek zu schreiben, lehnte ich diese Idee ab, aber anscheinend kehrte ich zurück (es war langweilig).

Yandex.Music Bot - ein Projekt, das die Eignung der Verwendung der Bibliothek für die Arbeit mit der API in Projekten belegt.

Vielen Dank an Mama, Yana, Sana'a, Ruhm. Jemand zum Korrekturlesen von Fehlern, einige für Hinweise, ohne die einige Punkte in diesem Artikel möglicherweise nicht vorhanden wären, und für einige lediglich zum Bewerten des Artikels vor der Veröffentlichung. Arthur für picci für den Artikel, Lyod für das Logo.

PS Jetzt habe ich ein akutes Problem mit der Verteilung nach dem Studium. Wenn Sie bereit sind, mich anzurufen - sagen Sie mir, wohin ich den Lebenslauf senden soll, interviewen Sie mich bitte. Alle meine Kontakte sind im Profil. Das Timing brennt, ich hoffe für dich. Nach dem Gesetz kann ich nur in der Republik Belarus trainieren.

PPS Dies war der zweite von drei Artikeln, aber ohne die geringste Ahnung werde ich Zeit haben, ein weiteres Projekt zu diesem Thema durchzuführen und ob es benötigt wird. Ich werde sein Thema öffentlich bekannt geben - den plattformübergreifenden Kunden Yandex.Music.

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


All Articles