Bajo el capó del cliente bot Yandex.Music

Introducción


Hola Habr! Nuevamente, estoy con el segundo artículo que afecta a la API Yandex.Music. El caso está planeado y mencionado en el primer artículo .

Manos extendidas, hechas. Hoy hablaré sobre interesantes, en mi opinión, momentos que están presentes en la base de código de mi bot Telegram, que se posiciona como un cliente de música completo. También tocaremos la API de reconocimiento de música Yandex.

Antes de continuar con la historia punto por punto de la implementación de una cosa en particular, valdría la pena tener una idea sobre el bot en sí y sus capacidades funcionales.

Video demo del cliente


En la parte principal hablaré sobre lo siguiente:

  1. Inicie sesión en su cuenta a través del sitio en las páginas de GitHub (por qué y por qué).
  2. El formato de datos, su empaque y su uso en datos de botones.
  3. Actualización de enrutamiento, control de versiones de datos, lanzamiento de contexto en controladores.
  4. Servicios:
    • Servicio de recarga de pista en Telegram.
    • El servicio de "suscripciones" para recibir una pista con el envío del estado de la descarga.
  5. La implementación de almacenamiento en caché de consultas más simple y elegante.
  6. Reconocimiento de una pista por mensaje de voz y cómo apareció generalmente en el bot.
  7. Pequeñas notas

Si está interesado en al menos un punto, bienvenido a cat.
El bot funciona con usuarios que han iniciado sesión en su cuenta, pero no. Todo gira en torno a cuatro clases principales de servicio: álbum, artista, lista de reproducción y pista. Todos los datos de la entidad son compatibles, hay paginación con la selección de página. Para interactuar con ellos, un usuario autorizado puede usar su menú personal, que incluye: listas de reproducción inteligentes, la lista de reproducción "Me gusta" y sus propias listas de reproducción. Para usuarios no autorizados, la búsqueda está disponible, enviando una consulta de búsqueda como un mensaje regular. Ahora solo hay una lista seca de qué más hay: recibir pistas y otros objetos a través de un enlace directo, recibir letras, la posibilidad de que les guste / no les guste la pista y el artista, ver pistas similares, reconocer la pista por mensaje de voz y más.

Parte principal


1. Inicie sesión en la cuenta


Inicialmente, el bot no estaba planeado para funcionar sin autorización, por lo tanto, incluso durante el diseño, se decidió cómo debería ocurrir la autorización, a través de su sitio web . Los principales argumentos fueron la seguridad y la transparencia. En ningún caso no quería aceptar inicios de sesión de usuario y contraseñas en mensajes claros. Y la autorización de la computadora del usuario es elemental. Por lo tanto, se escribió un sitio para autorización en React . Es una forma de autorización con un redireccionamiento de regreso al bot. Una pieza con autorización y procesamiento de captcha fue tomada de la biblioteca en Python y reescrita en JavaScript .

Para la autorización, se utiliza el token OAuth recibido, que se transfiere de vuelta al bot a través de enlaces profundos.

El enlace final para la redirección se ve así: t.me/music_yandex_bot?start={oauth_token}

Quería lo mejor, pero los usuarios no confiaban. Solo cada décimo usuario estaba autorizado (en el momento en que era obligatorio). Por lo tanto, con las siguientes actualizaciones, tuve que explicar por qué debería confiar. Todos los artículos sobre HTTPS , exclusivamente sobre las solicitudes del lado del cliente y el código fuente abierto se publicaron en el sitio para su autorización y se duplicaron en un mensaje de bienvenida en el bot. Mejoró.

Además, entre los factores de baja autorización, la inaccesibilidad de los espejos para t.me en Rusia resultó ser y soporte solo para usuarios con una suscripción (más sobre esto en las notas, párrafo 7).

El primer elemento se resolvió utilizando el URI ( tg: // ), y en caso de necesidad, ya hay un enlace al espejo (si la redirección automática no funcionó y el botón no ayudó), y el segundo está nuevamente en el elemento 7.


2. Formato de datos


Para este bot, decidí por primera vez usar NoSQL DB - MongoDB . Entendido: cuando incrustar un documento, cuando no. Me gustó trabajar con mongoengine. Pero realmente no quería almacenar todos los datos del botón en casa, y el cliente solo tiene el ID de registro en la base de datos. Estaba aterrorizado por la cantidad de datos, pero quería tomar un servidor Mongo gratuito con un límite de 512 MB para el almacenamiento de datos. Encontrar una hermosa reutilización de registros si los datos coinciden en varios botones y limpiar los obsoletos es mucho más difícil que almacenar todo en los propios botones. Después de analizar el tamaño de los datos que se almacenarán, concluí que se ajusta fácilmente.

Al principio, solo usé JSON, pero lo rechazó rápidamente cuando se topó con un límite. En Telegram, el contenido de los datos del botón solo puede tener un máximo de 64 bytes en UTF-8 .

Por lo tanto, con el aviso de un amigo, comencé a mirar el paquete desde el módulo de estructura . Así nacieron los tipos de consultas, el formato primitivo, el empaquetado y el desempaquetado. Ahora se usa en el bot absolutamente en todas partes.

El formato es muy simple. El primer byte es el tipo, el segundo es la versión. Todo lo demás son datos para un tipo particular. Los tipos se almacenan como Enum, tienen una ID , que es el primer byte. Además de la ID , cada tipo tiene un formato para empacar y desempacar datos. Por ejemplo: escriba SHOW_TRACK_MENUcuyo formato tiene el valor "s?", donde "s" es el identificador único de la pista y "?" - ¿La pista tiene texto?

En las pistas se usa el tipo de cadena porque: primero, la ID de la pista puede ser una concatenación de ID e ID de álbum de la pista a través de los dos puntos, y en segundo lugar, puede ser el UUID . Pistas con UUID : pistas autocargadas, disponibles solo para el usuario que las descargó.

Dado que los datos no siempre se corresponden con el formato, por ejemplo, la misma ID de pista puede representarse simplemente por un número, antes de empaquetar debe convertirse en un tipo para el formato. En este caso, s. Por lo tanto, en la clase hay un método que normaliza los datos transferidos para el empaquetado, para no hacerlo usted mismo al pasar al constructor.

Las cadenas son autosuficientes y pueden indicar su longitud al empaquetar y tener en cuenta esta longitud al desempacar.

No se planificó la compatibilidad con versiones anteriores, por lo que se genera una excepción si las versiones no coinciden. Al procesar actualizaciones, que analizaré en el próximo párrafo, se llama a la lógica necesaria.

Como Telegram come exclusivamente UTF-8 , los datos empaquetados se codifican en base85 . Sí, estoy perdiendo velocidad aquí y guardando el tamaño más pequeño sin usar base64 , pero dados los pequeños datos, considero que usar base85 es apropiado.

Fuente. Archivo 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. Actualizaciones de enrutamiento y contexto


El proyecto utiliza la biblioteca python-telegram-bot para trabajar con la API de Telegram Bot . Ya tiene un sistema para registrar controladores para ciertos tipos de actualizaciones que han llegado, filtros para expresiones regulares, comandos, etc. Pero, dado mi propio formato de datos y mis tipos, tuve que heredar de TelegramHandler e implementar mi controlador .

La actualización y el contexto se pasan a cada controlador a través de argumentos. En este caso, tengo mi propio contexto y está en Handler'ese está formando, y esto es: recibir y / o agregar un usuario a la base de datos, verificar la relevancia del token para obtener acceso a la música, inicializar el cliente Yandex.Music, dependiendo del estado de autorización y la disponibilidad de una suscripción.

Más allá de mi Handler'a hay controladores más específicos, por ejemplo, CallbackQueryHandler . Con él, se registra un controlador para un cierto tipo de actualización (mi tipo, con un formato de datos). Para verificar si esta actualización es adecuada para el controlador actual, no se desempaquetan todos los datos, sino solo los primeros dos bytes. En esta etapa, se verifica la necesidad de iniciar una devolución de llamada. Solo si es necesario iniciar la devolución de llamada, los datos se desempaquetan por completo y se transfieren como kwargsal manejador final. Inmediatamente hay un envío de datos analíticos a ChatBase .

El registro se realiza secuencialmente, y la prioridad es mayor para quién se registrará antes (de hecho, como en el enrutamiento de Django y en otros proyectos). Por lo tanto, registrar un controlador para una versión obsoleta es el primero entre los controladores CallBackQuery .

La lógica de procesar la versión desactualizada es simple: informar al usuario sobre esto y enviar datos actualizados, si es posible.

4. Servicios


Todos los servicios se inicializan cuando el bot se inicia en una clase de control, que luego se usa universalmente en cualquier parte del bot ( DJ ).

Cada servicio tiene su propio ThreadPoolExecutor con un cierto número de trabajadores en los que se envían las tareas.

Recargar una pista en Telegram


Por el momento, este servicio no se ha reescrito en User Bot para omitir el límite del tamaño del archivo descargado en Telegram. Al final resultó que, en Yandex.Music hay archivos de más de 50 mb - podcasts.

El servicio verifica el tamaño del archivo y, en caso de exceso, lanza una alerta al usuario. Gracias al sistema de almacenamiento en caché descrito en el párrafo 5, esto verifica la disponibilidad y recepción de la letra. Las pistas también se almacenan en caché. El hash del archivo se almacena en la base de datos. Si hay uno, se está enviando audio con un caché conocido.

En ausencia de un archivo en la base de datos, se recibe un enlace directo de Yandex.Music. Aunque en este momento, los usuarios no tienen la capacidad de cambiar la configuración de calidad, pero todos están configurados con valores estándar. El archivo se busca por bitrate y códec desde la configuración del usuario.

El archivo y su portada se descargan como tempfile.TemporaryFile () , luego de lo cual se cargan en Telegram. Vale la pena señalar que TG no siempre reconoce correctamente la duración de la pista, pero en general no digo nada sobre el artista y el título. Por lo tanto, estos datos se toman de Yandex, afortunadamente, es posible transmitirlos al carrito.

Cuando este servicio envía un archivo de audio, se llama a complete_callback () , señalando el servicio por suscripción sobre el final de la descarga.

Servicio de suscripción para recibir pistas y enviar el estado de descarga


Las pistas no se cargan al instante. Es posible que varios usuarios hayan solicitado la misma pista. En este caso, el usuario que solicitó la pista primero es el iniciador y el propietario de la pista. Al recargar más de un segundo, comienza el estado de descarga: "El usuario envía un mensaje de voz". Otros usuarios que solicitaron la misma pista son suscriptores habituales. A ellos, como al propietario, se les envía el estado de descarga una vez cada ~ 4 segundos para que el mensaje no se interrumpa (el estado se cuelga durante 5 segundos). Tan pronto como se complete la descarga de la pista para el propietario, se llama a complete_callback () desde el servicio anterior. Después de eso, todos los suscriptores se eliminan de la lista de estado y reciben la pista descargada.

En la solución arquitectónica, el propietario de la pista también es suscriptor, pero con una cierta marca, ya que las formas de enviar la pista son diferentes.

5. Caché de consultas


Como recordamos de mi último artículo, las solicitudes a la API Yandex.Music son muy pesadas. La lista de pistas puede ser de 3 o 5 mb. Además, solo hay muchas solicitudes. Con cada proceso de actualización, se envían al menos 2 solicitudes a Yandex: para inicializar el cliente y para una acción específica. En algunos lugares, para recopilar suficiente información (por ejemplo, para una lista de reproducción), debe solicitar una lista de reproducción, para recibir sus pistas, para obtener información de la página de destino (si se trata de una lista de reproducción inteligente) y no se olvide de la inicialización del cliente. En general, horror silencioso en cuanto a la cantidad de solicitudes.

Quería algo muy universal, y no hacer ningún tipo de almacenamiento para ciertos objetos, los mismos clientes.

Dado que la biblioteca le permite especificar su propia instancia para la ejecución de consultas, además de las solicitudesEntonces aproveché esto.

El punto es simple. La clase de caché en sí es un singleton. Tiene solo dos parámetros: duración de caché, tamaño. Cuando se ejecuta la solicitud, se llama al reiniciador. El es anulado. La comprobación de la memoria caché se produce por hash de args y cuartos de galón congelados. El caché tiene tiempo para agregar. Cuando se verifica la necesidad de actualizar los datos, se obtienen datos de LimitedSizeDict o se realiza una solicitud real y se agrega al caché.

Algunas solicitudes no se pueden almacenar en caché, por ejemplo, establecer un Me gusta / No me gusta. Si el usuario presiona la siguiente secuencia: like, dislike, like, al final no se entregará like. Para tales casos, al enviar una solicitud, debe pasar el argumento use_cache con un valor igual a False. En realidad, este es el único lugar donde no se usa el caché.

Gracias a esto, hago las solicitudes más audaces para que se almacenen en caché. No estoy tratando de dividirlo en pequeños y solo lo necesito para la página actual. Tomo todo de una vez, y cuando cambio de página tengo una velocidad de cambio enorme (en comparación con el enfoque anterior).

En cuanto a mí, la clase de solicitud en caché resultó muy bien y simplemente se integró.

Fuente
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. Seguimiento de reconocimiento por mensaje de voz


Al principio, no se pensaba agregar esto al bot, pero ocurrió una situación interesante. El bot tiene un chat (se indica en la descripción del bot). Después de un tiempo, noté que la gente entra y envía mensajes de voz con música. Al principio, pensé que era un nuevo tipo de correo no deseado, de modo que alguien bromeó y estaba bromeando. Pero, cuando ya había 10 de esas personas y todos estaban haciendo lo mismo, mi amigo (el mismo sugerido por struct ) sugirió que los usuarios manejaran Yandex.Music en busca de un bot oficial para reconocer la música de Yandex, ¡ven una sala de chat allí y le envían mensajes de voz con toda seriedad! Era solo una suposición brillante y verdadera. Luego, bromeando, dije que era hora de hacer un reconocimiento y agregar un bot al chat. Por diversión ... ¡Después de un tiempo, esto se hizo!

Ahora sobre la API . Recientemente, Yandex ha utilizado cada vez más los sockets web. Conocí su uso en la gestión de un i.module y un i.station. El servicio de reconocimiento de música también funciona en él. Lancé la solución de trabajo mínima en mi bot, pero no agregué la implementación a la biblioteca.

WS se encuentra en la siguiente dirección: wss: //voiceservices.yandex.net/uni.ws
Necesitamos solo dos mensajes: autorización y una solicitud de reconocimiento .
Yandex, en su aplicación oficial, envía archivos cortos en un segundo o tres. En respuesta, puede tener la necesidad de enviar más datos o el resultado, encontrado o no. Si se encuentra el resultado, se devolverá la identificación de la pista. Los archivos Ogg se

envían conCODIFICADOR = SpeechKit Mobile SDK v3.28.0 . No verifiqué cómo funciona con otros codificadores, simplemente lo cambié en el archivo grabado por Telegram.

Cuando se invierte con un socket web, a veces ocurre magia. A veces no podía encontrar la pista, pero cuando cambié el idioma del mensaje con la solicitud de reconocimiento, lo hice. O al principio lo encontró, y luego se detuvo, aunque el archivo es el mismo. Pensé que el idioma de la pista lo establece su SpeechKit en el cliente. Al no tener la oportunidad de hacerlo yo mismo, hago una búsqueda de fuerza bruta.

Mi implementación de reconocimiento de voz hecha por la rodilla por mensajes de voz 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. Pequeñas notas


Inicialmente, solo los usuarios con una suscripción podían usar el bot debido al hecho de que el servicio se puede usar en un número limitado de países (sin suscripción), y el servidor con el bot está ubicado en Europa. El problema se resuelve utilizando un proxy para ejecutar solicitudes de usuarios sin una suscripción. El servidor proxy se encuentra en Moscú.

Hay una variedad de páginas, pero está limitado a 100 (no se pueden agregar más botones, restricción de Telegram). Algunas solicitudes de página comunes tienen muchas más páginas.

En la búsqueda de Yandex, el número de elementos en la página está codificado. Cuántas pistas, cuántas listas de reproducción. A veces esto ni siquiera corresponde a la cantidad de datos que se muestran en el frente. Hay un cambio de página, el número de elementos es flotante. Por lo tanto, en el bot también salta, lo que no es muy hermoso. Y combinar su paginador con el suyo, algo así. En otros lugares donde es posible solicitar un cierto número de elementos por página, todo es perfecto. El mensaje de voz después de implementar la búsqueda en el bot es t.me/MarshalC/416 .

Cuando se reenvía el audio en Telegram, el autor se pierde y se le asigna al que realizó el reenvío. Por lo tanto, todas las pistas se envían con la firma del nombre de usuario del bot.

Mensaje de voz con todo lo que conocí después de la implementación de la radio en la biblioteca - t.me/MarshalC/422(sobre la cadena de pistas, revisándola enviando un montón de comentarios, batch_id ).

Conclusión


A pesar de que este es otro artículo sobre el bot de Telegram, lo leyó hasta aquí, probablemente porque estaba interesado en uno de los elementos del corte y esto es maravilloso, ¡ muchas gracias !

Desafortunadamente, no abrí el código fuente del bot por completo (porque en algunos lugares necesito refactorizar). Mucho se ha descrito en este artículo, pero algunos aspectos, por ejemplo, con los teclados virtuales y su generación no se ven afectados. En su mayor parte, lo que no está en el artículo es solo trabajar con mi biblioteca, nada interesante.

Las clases en torno a las cuales gira todo, las mostré en la forma en que están ahora. Admito que hay errores, pero todo funciona durante mucho tiempo. En algunos lugares me gusta mi código, en otros no, y esto es normal. No olvide que trabajar con WS para el reconocimiento es una solución sobre la rodilla. Listo para leer críticas razonadas en los comentarios.

Aunque el bot se planeó incluso cuando comencé a escribir la biblioteca, rechacé esta idea, pero, al parecer, regresé (era aburrido).

Yandex.Music Bot: un proyecto que demuestra la idoneidad del uso de la biblioteca para trabajar con la API en proyectos.

Muchas gracias a mamá, Yana, Sana'a, Glory. Alguien por corregir errores, algunos por pistas, sin los cuales algunos puntos en este artículo podrían no haber existido, y algunos simplemente por evaluar el artículo antes de su publicación. Arthur por picci para el artículo, Lyod por el logo.

PD: Ahora tengo un problema agudo con la distribución después del estudio. Si está listo para hacerme una llamada, dígame a dónde enviar el CV, entrégueme, por favor. Todos mis contactos están en el perfil. El tiempo está ardiendo, espero por ti. Según la ley, solo puedo entrenar en la República de Bielorrusia.

PPS Este fue el segundo artículo de tres, pero sin la más mínima idea tendré tiempo para hacer otro proyecto sobre este tema y si es necesario. Revelaré públicamente su tema: el cliente multiplataforma Yandex.Music.

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


All Articles