Sob o capô do cliente de bot Yandex.Music

Introdução


Olá Habr! Novamente, eu estou com o segundo artigo que afeta a API Yandex.Music. O caso é planejado e mencionado no primeiro artigo .

Mãos alcançadas, prontas. Hoje vou falar de momentos interessantes, na minha opinião, presentes na base de código do meu bot Telegram, que se posiciona como um cliente de música completo. Também tocaremos na API de reconhecimento de música Yandex.

Antes de prosseguir para a história ponto a ponto da implementação de uma coisa em particular, valeria a pena ter uma idéia sobre o bot em si e suas capacidades funcionais.

Demonstração em vídeo do cliente


Na parte principal, falarei sobre o seguinte:

  1. Entre na sua conta através do site nas Páginas do GitHub (por que e por que).
  2. O formato dos dados, sua embalagem e uso nos dados dos botões.
  3. Roteamento de atualização, versão de dados, lançamento de contexto em manipuladores.
  4. Serviços:
    • Trilha de recarga de serviço no Telegram.
    • O serviço de "assinaturas" para receber uma faixa com o envio do status do download.
  5. A implementação de cache de consultas mais simples e elegante.
  6. Reconhecimento de uma faixa por mensagem de voz e como ela geralmente aparece no bot.
  7. Pequenas notas.

Se você estiver interessado em pelo menos um ponto - seja bem-vindo ao gato.
O bot trabalha com usuários que fizeram login em sua conta, mas não. Tudo gira em torno de quatro classes principais de serviço: álbum, artista, lista de reprodução e faixa. Todos os dados da entidade são suportados, há paginação com seleção de página. Para interagir com eles, um usuário autorizado pode usar seu menu pessoal, que inclui: playlists inteligentes, a playlist “Eu gosto” e suas próprias playlists. Para usuários não autorizados, a pesquisa está disponível - enviando uma consulta de pesquisa como uma mensagem regular. Agora há apenas uma lista detalhada do que mais existe: recebimento de faixas e outros objetos por meio de um link direto, recebimento de letras de músicas, capacidade de gostar / não gostar da faixa e do artista, visualizar faixas semelhantes, reconhecer faixas por mensagem de voz e muito mais.

Parte principal


1. Faça o login na conta


Inicialmente, o bot não estava planejado para funcionar sem autorização, portanto, mesmo durante o design, foi decidido como a autorização deveria ocorrer - através de seu site . Os principais argumentos foram segurança e transparência. Em nenhum caso, você não quis aceitar logins e senhas de usuários em mensagens limpas. E a autorização do computador do usuário é elementar. Portanto, um site foi escrito para autorização no React . É uma forma de autorização com um redirecionamento de volta para o bot. Uma peça com autorização e processamento de captcha foi retirada da biblioteca em Python e reescrita em JavaScript .

Para autorização, é usado o token OAuth recebido, que é transferido de volta para o bot via link direto.

O link final para o redirecionamento é assim: t.me/music_yandex_bot?start={oauth_token}

Eu queria o melhor, mas os usuários não confiavam. Somente todo décimo usuário foi autorizado (no momento em que era obrigatório). Portanto, com as seguintes atualizações, tive que explicar por que você deveria confiar. Todos os itens sobre HTTPS , exclusivamente sobre solicitações do lado do cliente e código-fonte aberto, foram publicados no site para autorização e duplicados em uma mensagem de boas-vindas no bot. Melhorou.

Além disso, entre os fatores de baixa autorização, a inacessibilidade de espelhos para o t.me na Rússia acabou sendo um suporte para apenas usuários com assinatura (mais sobre isso nas notas, parágrafo 7).

O primeiro item foi resolvido usando o URI ( tg: // ) e, em uma pitada, já existe um link para o espelho (se o redirecionamento automático não funcionou e o botão não ajudou), e o segundo está novamente no item 7.


2. Formato dos dados


Para esse bot, decidi pela primeira vez usar o NoSQL DB - MongoDB . Entendido: quando incorporar um documento, quando não. Gostava de trabalhar com mongoengine. Mas eu realmente não queria armazenar todos os dados do botão em casa, e o cliente só tem o ID do registro no banco de dados. Eu tinha pavor da quantidade de dados, mas queria usar um servidor Mongo gratuito com um limite de 512 MB para armazenamento de dados. Criar uma bela reutilização de registros se os dados coincidirem em vários botões e limpar os desatualizados é muito mais difícil do que armazenar tudo nos próprios botões. Depois de analisar o tamanho dos dados a serem armazenados, concluí que eles se encaixam facilmente.

No começo, eu apenas usei JSON, mas ele rapidamente recusou quando chegou a um limite. No Telegram, o conteúdo dos dados do botão não pode ter mais que 64 bytes em UTF-8 .

Portanto, com a solicitação de um amigo, comecei a olhar o pacote no módulo struct . Assim, nasceram os tipos de consultas, o formato primitivo, a embalagem e a descompactação. Agora ele é usado no bot absolutamente em todos os lugares.

O formato é muito simples. O primeiro byte é do tipo, o segundo é a versão. Tudo o resto são dados para um tipo específico. Os tipos são armazenados como Enum, têm um ID , que é o primeiro byte. Além do ID , cada tipo tem um formato para empacotar e descompactar dados. Por exemplo: digite SHOW_TRACK_MENUcujo formato tem o valor "s?", onde "s" é o identificador exclusivo da faixa e "?" - A faixa possui texto.

Nas faixas usadas, digite o tipo de sequência porque: primeiro, o ID da faixa pode ser uma concatenação de ID e ID do álbum da faixa através dos dois pontos e, em segundo lugar, pode ser o UUID . Faixas com UUID - faixas com carregamento automático, disponíveis apenas para o usuário que as baixou.

Como os dados nem sempre correspondem ao formato, por exemplo, o mesmo ID de faixa pode ser representado simplesmente por um número, antes de compactá-los, deve ser convertido em um tipo para o formato. Nesse caso, s. Portanto, na classe existe um método que normaliza os dados transferidos para empacotamento, para não fazer isso sozinho ao passar para o construtor.

As strings são auto-suficientes e são capazes de indicar seu comprimento ao embalar e levar esse comprimento em consideração ao desembalar.

O suporte para versões mais antigas não foi planejado, portanto, uma exceção será lançada se as versões não corresponderem. Ao processar atualizações, que discutirei no próximo parágrafo, a lógica necessária é chamada.

Como o Telegram come exclusivamente UTF-8 , os dados compactados são codificados na base85 . Sim, estou perdendo velocidade aqui e salvando o menor tamanho sem usar o base64 , mas, considerando os pequenos dados, considero apropriado o uso do base85 .

Fonte. Arquivo 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. Atualizações e contexto de roteamento


O projeto usa a biblioteca python-telegram-bot para trabalhar com a API do Telegram Bot . Ele já possui um sistema para registrar manipuladores para certos tipos de atualizações que chegaram, filtros para expressões regulares, comandos e assim por diante. Mas, dado meu próprio formato de dados e meus tipos, tive que herdar do TelegramHandler e implementar meu Handler .

Atualização e contexto são passados ​​para cada manipulador por meio de argumentos. Nesse caso, eu tenho meu próprio contexto e está em Handler'eele está sendo formado e é: recebendo e / ou adicionando um usuário ao banco de dados, verificando a relevância do token para obter acesso à música, inicializando o cliente Yandex.Music, dependendo do status da autorização e da disponibilidade de uma assinatura.

Além do meu Handler'a, existem manipuladores mais específicos, por exemplo, CallbackQueryHandler . Com ele, um manipulador é registrado para um determinado tipo de atualização (meu tipo, com um formato de dados). Para verificar se essa atualização é adequada para o manipulador atual, nem todos os dados são descompactados, mas apenas os dois primeiros bytes. Nesta fase, a necessidade de iniciar um retorno de chamada é verificada. Somente se for necessário iniciar o retorno de chamada - os dados serão totalmente descompactados e transferidos como kwargspara o manipulador final. Imediatamente há um envio de dados analíticos para o ChatBase .

O registro ocorre sequencialmente, e a prioridade é maior para quem será registrado anteriormente (de fato, como no roteamento do Django e em outros projetos). Portanto, o registro de um manipulador para uma versão obsoleta é o primeiro entre os manipuladores do CallBackQuery .

A lógica do processamento da versão desatualizada é simples - informe o usuário sobre isso e envie dados atualizados, se possível.

4. Serviços


Todos os serviços são inicializados quando o bot é iniciado em uma classe de controle, que é usada universalmente em qualquer lugar do bot ( DJ ).

Cada serviço possui seu próprio ThreadPoolExecutor com um certo número de trabalhadores nos quais as tarefas são enviadas.

Recarregando uma faixa no Telegram


No momento, este serviço não foi reescrito no User Bot para ignorar o limite do tamanho do arquivo baixado no Telegram. Como se viu, no Yandex.Music existem arquivos maiores que 50 mb - podcasts.

O serviço verifica o tamanho do arquivo e, em caso de excesso, lança um alerta ao usuário. Graças ao sistema de armazenamento em cache descrito no parágrafo 5, isso verifica a disponibilidade e o recebimento das letras. As faixas também são armazenadas em cache. O hash do arquivo é armazenado no banco de dados. Se houver um, o áudio com um cache conhecido será enviado.

Na ausência de um arquivo no banco de dados, um link direto é recebido do Yandex.Music. Embora, no momento, os usuários não tenham a capacidade de alterar as configurações de qualidade, mas todas estão definidas com valores padrão. O arquivo é pesquisado por taxa de bits e codec nas configurações do usuário.

O arquivo e sua capa são baixados como tempfile.TemporaryFile () , após o qual são carregados no Telegram. Vale a pena notar que TG nem sempre reconhece corretamente a duração da faixa, mas geralmente sou silencioso sobre o artista e o título. Portanto, esses dados são retirados do Yandex, felizmente, é possível transmiti-los ao carrinho.

Quando um arquivo de áudio é enviado por esse serviço, o nome finish_callback () é chamado , sinalizando o serviço por assinatura sobre o final do download.

Serviço de assinatura para receber faixas e enviar status de download


As faixas não são carregadas instantaneamente. É possível que vários usuários tenham solicitado a mesma faixa. Nesse caso, o usuário que solicitou a faixa primeiro é o iniciador e o proprietário da faixa. Ao recarregar mais de um segundo, o status do download é iniciado: “O usuário envia uma mensagem de voz”. Outros usuários que solicitaram a mesma faixa são assinantes regulares. Eles, como o proprietário, recebem o status do download uma vez a cada ~ 4 segundos, para que a mensagem não seja interrompida (o status trava por 5 segundos). Assim que o download da faixa para o proprietário for concluído, o finish_callback () é chamado no serviço acima. Depois disso, todos os assinantes são excluídos da lista de status e recebem a faixa baixada.

Na solução arquitetônica, o proprietário da faixa também é assinante, mas com uma certa marca, pois as formas de envio da faixa são diferentes.

5. Cache de consulta


Como lembramos do meu último artigo, os pedidos para a API Yandex.Music são muito pesados. A lista de faixas pode ser de 3 ou 5 mb. Além disso, existem muitos pedidos. Com cada processamento de atualização, pelo menos 2 solicitações são enviadas ao Yandex: para inicializar o cliente e para uma ação específica. Em alguns lugares, para coletar informações suficientes (por exemplo, para uma lista de reprodução), você precisa fazer uma solicitação para uma lista de reprodução, para receber suas faixas, para obter informações da página de destino (se esta for uma lista de reprodução inteligente) e não se esqueça da inicialização do cliente. Em geral, horror silencioso em termos do número de solicitações.

Eu queria algo muito universal, e não fizesse nenhum tipo de armazenamento para certos objetos, os mesmos clientes.

Como a biblioteca permite que você especifique sua própria instância para execução de consultas, além de solicitações, aproveitei isso.

O ponto é simples. A própria classe de cache é um singleton. Ele possui apenas dois parâmetros: vida útil do cache, tamanho. Quando a solicitação é executada, o wrapper é chamado. Ele é substituído. A verificação do cache ocorre por hash de args e quartos congelados. O cache tem tempo para adicionar. Ao verificar a necessidade de atualizar os dados, os dados de LimitedSizeDict são obtidos ou uma solicitação real é feita e adicionada ao cache.

Alguns pedidos não podem ser armazenados em cache, por exemplo, definindo um gostar / não gostar. Se o usuário pressionar a seguinte sequência: gostar, não gostar, gostar, então, no final, o gosto não será entregue. Nesses casos, ao enviar uma solicitação, você precisa passar o argumento use_cache com um valor igual a False. Na verdade, este é o único local em que o cache não é usado.

Graças a isso, faço as solicitações mais ousadas para que elas sejam armazenadas em cache. Não estou tentando dividi-lo em páginas pequenas e necessário apenas para a página atual. Eu pego tudo de uma vez e, ao alternar entre as páginas, tenho uma velocidade de comutação enorme (em comparação com a abordagem antiga).

Quanto a mim, a classe de solicitação em cache teve um desempenho bonito e foi simplesmente integrada.

Fonte
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. Rastreie o reconhecimento por mensagem de voz


No começo, não se pensava em adicionar isso ao bot, mas ocorreu uma situação interessante. O bot tem um bate-papo (é indicado na descrição do bot). Depois de algum tempo, notei que as pessoas entram e enviam mensagens de voz com música. No começo, eu pensei que era um novo tipo de spam, de tal forma que alguém engasgava e estava brincando. Mas, quando já havia 10 pessoas assim e todo mundo estava fazendo a mesma coisa, meu amigo (o mesmo sugerido por struct ) sugeriu que os usuários dirigissem o Yandex.Music em busca de um bot oficial para reconhecer músicas do Yandex, eles veem uma sala de bate-papo lá e enviam mensagens de voz com toda a seriedade! Foi apenas uma suposição brilhante e verdadeira. Em tom de brincadeira, disse que estava na hora de fazer o reconhecimento e adicionar um bot ao chat. Por diversão ... Depois de um tempo, isso foi feito!

Agora sobre a API . Recentemente, o Yandex usou cada vez mais soquetes da web. Eu conheci o uso deles no gerenciamento de um i.module e um i.station. O serviço de reconhecimento de música também funciona nele. Joguei a solução mínima de trabalho no meu bot, mas não adicionei a implementação à biblioteca.

O WS está localizado no seguinte endereço: wss: //voiceservices.yandex.net/uni.ws
Precisamos apenas de duas mensagens - autorização e um pedido de reconhecimento .
O próprio Yandex, em seu pedido oficial, envia arquivos curtos em um segundo ou três. Em resposta, você pode enviar mais dados ou o resultado - encontrado ou não. Se o resultado for encontrado, o ID da faixa será retornado. Os arquivos .Ogg são

enviados comENCODER = SDK do SpeechKit Mobile v3.28.0 . Não verifiquei como ele funciona com outros codificadores, apenas o alterei no arquivo gravado pelo Telegram.

Ao reverter com um soquete da Web, às vezes a mágica acontecia. Às vezes, não conseguia encontrar a faixa, mas quando mudei o idioma na mensagem com a solicitação de reconhecimento, consegui. Ou, a princípio, encontrou e depois parou, embora o arquivo seja o mesmo. Eu pensei que o idioma da faixa é definido pelo seu SpeechKit no cliente. Não tendo essa oportunidade de fazer isso sozinho, faço uma busca por força bruta.

Minha implementação feita no joelho do reconhecimento de voz por mensagens de voz do 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. notas pequenas


Inicialmente, apenas usuários com uma assinatura poderiam usar o bot devido ao fato de que o serviço pode ser usado em um número limitado de países (sem assinatura) e o servidor com o bot está localizado na Europa. O problema é resolvido usando um proxy para executar solicitações de usuários sem uma assinatura. O servidor proxy está localizado em Moscou.

Há uma escolha de páginas, mas é limitado a 100 (não é possível adicionar mais botões, restrição de telegrama). Algumas solicitações de página comuns têm muito mais páginas.

Na pesquisa Yandex, o número de elementos na página é codificado. Quantas faixas, quantas listas de reprodução. Às vezes, isso nem corresponde à quantidade de dados exibidos na frente. Há uma alteração de página, o número de elementos está flutuando. Portanto, no bot também salta, o que não é muito bonito. E combinar o paginador deles com o dele - algo assim. Em outros lugares onde é possível solicitar um certo número de elementos por página, tudo é perfeito. A mensagem de voz após a implementação da pesquisa no bot é t.me/MarshalC/416 .

Ao encaminhar o áudio no Telegram, o autor é perdido e designado para quem fez o encaminhamento. Portanto, todas as faixas são enviadas com a assinatura do nome de usuário do bot.

Mensagem de voz com tudo o que conheci após a implementação do rádio na biblioteca - t.me/MarshalC/422(sobre a cadeia de trilhas, passando por isso enviando um monte de feedback, batch_id ).

Conclusão


Apesar de ser outro artigo sobre o bot do Telegram, você o leu até aqui, provavelmente porque estava interessado em um dos itens do corte e isso é maravilhoso, muito obrigado !

Infelizmente, não abri completamente o código fonte do bot (porque em alguns lugares eu preciso refatorar). Muito foi descrito neste artigo, mas alguns aspectos, por exemplo, dos teclados virtuais e sua geração não são afetados. Na maioria das vezes, o que não está no artigo é apenas trabalhar com minha biblioteca, nada de interessante.

As aulas em torno das quais tudo gira, mostrei na forma em que elas estão agora. Eu admito que há erros, mas tudo funciona por um longo tempo. Em alguns lugares, gosto do meu código, em outros, não - e isso é normal. Não esqueça que trabalhar com o WS para reconhecimento é uma solução no joelho. Pronto para ler críticas fundamentadas nos comentários.

Embora o bot tenha sido planejado mesmo quando comecei a escrever a biblioteca, deserdei essa ideia, mas, aparentemente, voltei (era chato).

Yandex.Music Bot - um projeto que comprova a adequação do uso da biblioteca para trabalhar com a API em projetos.

Muito obrigado a mamãe, Yana, Sana'a, Glória. Alguém por revisar erros, outros por dicas, sem os quais alguns pontos deste artigo podem não ter existido, e outros simplesmente por avaliar o artigo antes da publicação. Arthur por picci para o artigo, Lyod pelo logotipo.

PS Agora, tenho um problema agudo com a distribuição após o estudo. Se você estiver pronto para me ligar, diga-me para onde enviar o currículo, me entreviste, por favor. Todos os meus contatos estão no perfil. O tempo está queimando, espero por você. Segundo a lei, só posso trabalhar na República da Bielorrússia.

PPS Este foi o segundo artigo de três, mas sem a menor idéia, terei tempo para fazer outro projeto sobre esse tópico e se é necessário. Vou revelar publicamente o tópico dele - o cliente de plataforma cruzada Yandex.Music.

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


All Articles