https://a.storyblok.com/f/270183/956768/b4766b482f/streaming-last-christmas-into-a-phone-call.png

Reproduza “Last Christmas” durante uma ligação telefônica usando Python

Publicado em May 10, 2021

Tempo de leitura: 6 minutos

O Natal é uma época cara, mas posso ajudar você a economizar uma fortuna em presentes, garantindo que ninguém fale com você no dia de Natal — basta mandar todo mundo para Whamhalla!

Todo ano, meus amigos e eu participamos do Whamageddon, e as regras são simples:

  1. O objetivo é aguentar o máximo de tempo possível sem ouvir o clássico natalino do WHAM, “Last Christmas”

  2. O jogo começa no dia 1º de dezembro e termina à meia-noite do dia 24 de dezembro

  3. Apenas a versão original é válida

  4. Você está fora assim que reconhecer a música

Qualquer pessoa que já tenha saído de casa ou ligado o rádio durante o Natal pode confirmar como é difícil evitar ouvir “Last Christmas”. A partir do primeiro segundo após a meia-noite do dia 1º de dezembro, é como se todas as lojas estivessem tocando essa música sem parar. A maioria das pessoas adota uma estratégia defensiva, evitando os lugares onde é mais provável que a música esteja tocando. Mas nós não somos como a maioria; vamos partir para o ataque.

Os Princípios da Guerra

Santa driving a tankSanta driving a tank

Conquistar, manter e aproveitar a iniciativa. A ação ofensiva é a maneira mais eficaz e decisiva de alcançar um objetivo comum claramente definido. As operações ofensivas são o meio pelo qual uma força militar conquista e mantém a iniciativa, ao mesmo tempo em que preserva a liberdade de ação e alcança resultados decisivos. Isso é fundamentalmente verdadeiro em todos os níveis da guerra.

Podemos vencer o Whamageddon seja evitando a música até o dia 25 de dezembro, seja garantindo que todos os nossos amigos a ouçam antes de nós, para que sejamos os últimos a resistir. Precisamos encontrar uma maneira de fazer com que eles ouçam a música, sem que a gente tenha que ouvi-la ao mesmo tempo. Nada de “fogo amigo”, por favor!

O vetor de ataque mais óbvio seria enviar a eles um link do YouTube com a música, mas o “Rickrolling” deixou todo mundo com muito receio de clicar em links desconhecidos do YouTube. Precisamos de um canal que eles não suspeitem.

Atacar o inimigo em um momento, local ou de uma maneira para os quais ele não esteja preparado. O elemento surpresa pode alterar de forma decisiva o equilíbrio do poder de combate. Ao buscar o elemento surpresa, as forças podem alcançar um sucesso muito superior ao esforço despendido. O elemento surpresa pode estar no ritmo da ação, no tamanho da força, na direção ou localização do esforço principal e no momento certo. O engano pode aumentar a probabilidade de se alcançar o elemento surpresa.

Transmissão de áudio durante uma ligação telefônica

Tanto quanto “Last Christmas” esteja em toda parte nesta época do ano, acho que nunca atendi o telefone e ouvi essa música. Ninguém jamais suspeitaria que o Wham! estivesse ligando.

Vamos usar duas APIs para fazer isso, a Voice API da Nexmo para fazer a chamada e reproduzir o arquivo MP3 para nossos amigos, e a API do Spotify para fornecer um pequeno trecho de “Last Christmas”. Ambas as APIs têm wrappers em Python para facilitar o trabalho, e vamos integrar tudo isso em uma interface de linha de comando (CLI) bacana para que possamos “eliminar” nossos amigos com um único comando. Quando terminarmos, ficará exatamente assim:

Screencast of our Voice Application CLScreencast of our Voice Application CL

Introdução

Você vai precisar de um pouco de experiência com Python para fazer isso funcionar, e pelo menos a versão 3.6, já que estamos usando f-strings (porque elas são incríveis) , além de algumas outras coisas:

  1. O código-fonte do GitHub

  2. Um Account no Spotify e seu próprio aplicativo do Spotify. Anote seu ID de cliente e sua chave secreta, pois você precisará adicioná-los ao seu .env mais tarde

  3. Ngrok; se você não sabe ao certo para que serve, é melhor ler primeiro nosso tutorial sobre o ngrok

Configuração

Há um arquivo de exemplo .env.example no repositório; renomeie-o para .env e comece a preencher os valores. Para o NEXMO_APPLICATION_ID e NEXMO_PRIVATE_KEY , você precisa criar um novo aplicativo Nexmo; você pode fazer isso pelo Painel de Controle. Lembre-se de salvar o gerado private.key em um local seguro e definir o valor de NEXMO_PRIVATE_KEY para o local onde sua chave está armazenada.

O script também possui várias dependências; recomendo instalá-las via pipenv, mas você pode usar algum outro gerenciador de dependências do Python, se preferir.

pipenv install

Depois de instalar as dependências, certifique-se de ter ativado seu ambiente virtual (isso seria pipenv shell se você estiver usando o pipenv) e que seu shell contenha as variáveis de ambiente do .env arquivo. Por fim, precisamos expor nosso script à internet pública para que os webhooks do Nexmo possam acessá-lo. Para isso, usaremos o ngrok; não se preocupe com a configuração do túnel, apenas certifique-se de que o ngrok esteja em execução.

ngrok http 80

Agora você está pronto para executar o script.

python pvpwham.py NUMBER

Existem algumas opções adicionais que você pode especificar; execute python pvpwham.py --help para obter mais informações.

Vamos examinar algumas das partes mais interessantes do código com mais detalhes.

Validação do número

É fundamental garantir que tenhamos um número de telefone válido para contatar; sem ele, nada mais funciona. Por isso, dedicamos algum tempo para validar o número e convertê-lo para o formato correto. No entanto, as pessoas escrevem números de telefone das mais variadas e inusitadas maneiras; portanto, se não conseguirmos validá-lo, solicitamos que sigam o padrão E.164 e até fornecemos um link com mais informações antes de pedir que tentem novamente.

while e164_number == False:
    insight_response = nexmo_client.get_basic_number_insight(number=number)
    if insight_response["status"] == 3:
        insight_response = nexmo_client.get_basic_number_insight(
            number=number, country=country
        )

    if insight_response["status"] != 0:
        click.clear()
        click.secho(intro, bg="magenta", fg="green")
        click.secho(
            f"{number} does not appear to be a valid telephone number",
            bg="magenta",
            fg="white",
        )
        click.secho(
            "It might work if you enter it in the E.164 format",
            bg="magenta",
            fg="white",
        )

        if click.confirm(wtf_e164_message):
            click.launch(
                "https://developer.nexmo.com/concepts/guides/glossary#e-164-format"
            )

        if click.confirm(try_number_again_message):
            number = click.prompt("Ok, give it to me in E.164 this time")
        else:
            raise click.BadArgumentUsage(
                click.style(
                    f"{number} does not appear to be a valid number. Try entering it in the E.164 format",
                    bg="red",
                    fg="white",
                    bold=True,
                )
            )
    else:
        e164_number = insight_response["international_format_number"]

Encontrar um arquivo MP3 da música

Não precisamos da música inteira; alguns segundos já devem bastar para levar alguém ao Whamhalla. A prévia de 30 segundos do Spotify é ideal. O script usa “Last Christmas” como padrão, mas você pode configurá-lo usando a --track opção.

spotify_client_credentials_manager = SpotifyClientCredentials(
    client_id=os.environ["SPOTIFY_CLIENT_ID"],
    client_secret=os.environ["SPOTIFY_CLIENT_SECRET"],
)
spotify_client = spotipy.Spotify(
    client_credentials_manager=spotify_client_credentials_manager
)
tracks = spotify_client.search(track, limit=1, type="track")

if len(tracks["tracks"]["items"]) == 0:
    raise click.BadOptionUsage(
        track,
        click.style(f"Can't find track: {track}", bg="red", fg="white", bold=True),
    )

track = tracks["tracks"]["items"][0]

Nosso servidor

Temos vários webhooks diferentes da Nexmo que precisamos gerenciar.

URL de resposta — ela é chamada sempre que alguém atende a chamada e contém uma lista de ações que a Nexmo deve executar. No nosso caso, vamos instruir a Nexmo a gravar a chamada e a usar a função de conversão de texto em fala para ler um breve aviso ao usuário (essa função está desativada por padrão, mas pode ser ativada usando a --delay opção), e, por fim, transmitir nossa prévia do Spotify durante a chamada

@cherrypy.tools.json_out()
def index(self, **params):
    ncco_file = [
        {
            "action": "record",
            "eventUrl": [f"{self.ngrok_tunnel['public_url']}/recording"],
        }
    ]

    if delay == "short":
        ncco_file.append({"action": "talk", "text": "whamageddon"})
    elif delay == "long":
        ncco_file.append(
            {
                "action": "talk",
                "text": "hang up your phone or prepare to enter Whamhalla",
            }
        )

    ncco_file.append(
        {"action": "stream", "streamUrl": [f"{self.preview_url}?t=mp3"]}
    )

    return ncco_file

URL da gravação — Assim que o usuário desligar o telefone e a chamada for encerrada, a Nexmo aciona esse webhook com os detalhes da gravação para que possamos baixar o arquivo MP3 e ouvir o desânimo do nosso amigo ao perceber o que aconteceu.

Observação: Gravamos tudo, inclusive o áudio que transmitimos durante a chamada. Você não vai querer acabar indo para Whamhalla sem querer também. Você pode modificar o script para usar gravações separadas para evitar isso.

@cherrypy.expose
def fetch_recording():
    data = cherrypy.request.json
    click.secho("## Fetching Call Recording", bg="green", fg="black", bold=True)
    recording_response = nexmo_client.get_recording(data["recording_url"])

    recordingfile = f"/tmp/{data['recording_uuid']}.mp3"
    os.makedirs(os.path.dirname(recordingfile), exist_ok=True)

    with open(recordingfile, "wb") as f:
        f.write(recording_response)

    click.secho("## Call Recording Saved", bg="green", fg="black", bold=True)
    if click.confirm(
        click.style(
            "## Listen to your friend's anguish now?", bg="magenta", fg="white"
        )
    ):
        click.launch(recordingfile)

URL de eventos — Isso serve apenas para fins informativos. O script atualiza o terminal com o status mais recente que recebe por meio do webhook de eventos.

@cherrypy.expose
@cherrypy.tools.json_in()
def events(self):
    data = cherrypy.request.json
    click.secho(
        f"## Status: {data['status']}", bg="blue", fg="white", bold=True
    )
    return "OK"

Serviço de limpeza

Sempre que a chamada for concluída, temos um on_end_request função que faz algumas tarefas de limpeza. Desligamos nosso servidor Cherrypy e encerramos nosso túnel Ngrok.

def quit_cherry():
    cherrypy.engine.exit()
    click.secho("## Exiting NCCO Server", bg="blue", fg="white", bold=True)
    requests.delete("http://localhost:4040/api/tunnels/pvpwham")
    click.secho("## Closing tunnel", bg="blue", fg="white", bold=True)

Outros comandos divertidos

Dá pra encontrar algumas coisas bem estranhas no Spotify, se você tiver criatividade suficiente.

python pvpwham.py NUMBER --track='Rick Astley Never gonna give you up'

python pvpwham.py NUMBER --track='Sound Effects Animals Chimps, Apes'

python pvpwham.py NUMBER --track='Halloween Sound effects machine ghostly whispers'

E agora?

Orientar todas as operações militares para um objetivo claramente definido, decisivo e alcançável. O objetivo militar fundamental da guerra é a destruição da capacidade de combate e da vontade de lutar do inimigo.

Mesmo que você não consiga eliminar todos os seus amigos, espero que os sobreviventes fiquem tão ansiosos toda vez que o telefone tocar que acabem se rendendo. Entrando no shopping local para sentar e esperar pelo inevitável.

Então, depois de ter derrotado a oposição, dê uma olhada em algumas das aplicações menos agressivas da Voice API:

Compartilhar:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Aaron BassettEx-funcionários da Vonage

Aaron era um defensor de desenvolvedores na Nexmo. Engenheiro de software experiente e aspirante a artista digital, Aaron costuma ser visto criando coisas com código ou com eletrônica; às vezes, os dois. Geralmente, dá para perceber quando ele está trabalhando em algo novo pelo cheiro de componentes queimando no ar.