telegram-slskd-local-bot

Servidor MCP de um buscador de música Soulseek auto-hospedado: resolva uma música no Spotify ou MusicBrainz, liste suas cópias no Soulseek via slskd, baixe uma ou um álbum inteiro na biblioteca e mantenha uma lista de desejos.

Documentação

O bot também fala o Model Context Protocol, então um agente como Claude Code ou Claude Desktop pode encontrar uma música, escolher uma cópia e salvá-la sem o Telegram. Ele usa o mesmo pipeline do bot: a mesma busca no Spotify, a mesma pesquisa e classificação no slskd, a mesma verificação de lossless, a mesma biblioteca, histórico e lista de desejos.

Há duas formas de acessá-lo:

  • Via stdio, python -m music_downloader mcp inicia um servidor em stdin e stdout para um agente na mesma máquina. Ele lê as mesmas variáveis de ambiente ou .env que o bot.
  • Via HTTP, com MCP_PORT definido, o processo do bot também serve MCP sobre HTTP streamable em http://<host>:<MCP_PORT>/mcp. Cada requisição precisa de Authorization: Bearer <MCP_TOKEN>. Este servidor compartilha o banco de dados do bot, o cliente slskd e o verificador de lista de desejos.

Ferramentas

Cada ferramenta retorna um objeto JSON. Faixas e cópias vêm com ids curtos (t3fa9c1, c81d0e2) que a próxima chamada usa; um id é válido por uma hora.

FerramentaO que fazExemplo de chamada
resolve_track(query, artist, title, duration_secs)Candidatos de faixa para uma consulta de texto livre, ou para um artista e título, os mais confiantes primeiro, cada um com id da faixa, artista, título, álbum, ano, duração, source (spotify, musicbrainz ou soulseek), source_id e confident. Spotify primeiro, MusicBrainz quando o Spotify não tem candidato confiante, a melhor cópia do Soulseek quando nenhum dos dois tem. Veja Como uma faixa é resolvidaresolve_track(artist="Graham Central Station", title="Jam", duration_secs=219.9)
search_copies(track_id, profile="library", limit=10)Cópias Soulseek dessa faixa, melhores primeiro, cada uma com id da cópia, formato, qualidade, nível de qualidade, tamanho, duração, usuário de origem, pontuação, lossless e fits_cap (sob TELEGRAM_MAX_UPLOAD_MB). profile="library" coloca lossless primeiro; "chat" classifica por som por megabyte. Pesquisa "artista título", depois apenas o títulosearch_copies(track_id="t3fa9c1", profile="library", limit=5)
download(copy_id, deliver="library")Baixa a cópia e espera por ela, até DOWNLOAD_TIMEOUT_SECS, enviando notificações de progresso. deliver="library" renomeia, insere a capa, move para OUTPUT_DIR e registra no histórico; "path" deixa o arquivo em DOWNLOAD_DIR e retorna seu caminho. Ambos retornam o veredito da verificação lossless (FLAC, WAV e AIFF; null caso contrário). "library" passa pela verificação lossless, veja abaixodownload(copy_id="c81d0e2", deliver="library")
album_listing(copy_id)Os arquivos de áudio da pasta do peer de onde a cópia veio, geralmente o lançamento inteiro: nome, formato, qualidade, tamanho e duração de cada um, o tamanho total e os formatos presentes. answered é falso, com um reason (no_answer, unreachable), quando o peer está offline ou não responde em 30 s; no_audio quando a pasta não tem áudioalbum_listing(copy_id="c81d0e2")
album_download(copy_id, deliver="library")Baixa todos os arquivos de áudio daquela pasta (uma requisição slskd para o lote), com progresso por arquivo. deliver="library" salva cada arquivo em OUTPUT_DIR conforme chega, nomeado pelas tags (ou pelo nome do arquivo quando as tags estão ausentes), com a capa do Spotify do álbum e uma linha no histórico anotada album; um arquivo que a biblioteca já tem com esse nome é pulado (skipped verdadeiro, path o arquivo da biblioteca) e seu download é excluído. "path" deixa os arquivos em DOWNLOAD_DIR. Um arquivo desiste quando sua transferência não move nenhum byte por DOWNLOAD_TIMEOUT_SECS; o álbum inteiro espera até ALBUM_TIMEOUT_SECS. Retorna um resultado por arquivo (ok, path, skipped, error, state) e as contagens; um arquivo com falha nunca impede os outrosalbum_download(copy_id="c81d0e2", deliver="library")
history(limit=20)Os downloads mais recentes, do mais novo ao mais antigo, com seu status (success, delivered, failed...)history(limit=10)
library_has(artist, title)Se a biblioteca tem um arquivo que parece esta faixa (a verificação de duplicados do bot), com as correspondências mais próximaslibrary_has(artist="Nancy Sinatra", title="Bang Bang")
wishlist_add(track_id, wanted="any")Pesquisa a faixa novamente a cada WISHLIST_CHECK_HOURS. "any" espera por qualquer cópia; "better" espera por uma cópia acima da melhor que search_copies encontrou para aquela faixawishlist_add(track_id="t3fa9c1", wanted="better")
wishlist_list()Todos os desejos, de todos os chatswishlist_list()
wishlist_remove(id)Remove um desejo pelo id que wishlist_list mostrawishlist_remove(id=4)
library_sweep_run(force=false)Inicia uma varredura da biblioteca em segundo plano e retorna imediatamente com o id da execução e o status. force=true verifica cada música independentemente do nível e da última verificação. Somente com LIBRARY_SWEEP_USERS definido; caso contrário, um erro de ferramentalibrary_sweep_run()
library_sweep_status()A varredura atual ou a última (posição, contagens por resultado, arquivo atual), músicas por nível, pares aguardando, o agendamento e o próximo início, se fpcalc está presentelibrary_sweep_status()
library_sweep_reviews()Os pares aguardando decisão: stem, motivo, caminho de ambos os arquivos, qualidade, duração e cutoff, a similaridade da impressão digital e o nome que a proposta recebe com keep_bothlibrary_sweep_reviews()
library_sweep_decide(stem, decision)keep_mine exclui a proposta, take_new substitui a música por ela (a música fica estacionada por LIBRARY_SWEEP_KEEP_DAYS), keep_both a adiciona como uma música próprialibrary_sweep_decide(stem="Nancy Sinatra - Bang Bang", decision="keep_both")
status()Se o slskd responde, downloads aguardando um botão Salvar, Rejeitar ou Tentar novamente no Telegram, downloads MCP em execução, o número de desejos, o limite de upload e a versãostatus()

Como uma faixa é resolvida

resolve_track pergunta ao Spotify primeiro. Um candidato é confident quando seu artista e título são os solicitados após a normalização da grafia e, se você passar duration_secs (a duração do arquivo que você tem), quando dura dentro de 8 segundos dele. A normalização ignora acentos, apóstrofos (reto, curvo ou ausente), & contra "and", um "The" inicial e sufixos que nomeiam a mesma gravação: " - Single Edit", " - 2011 Remaster", "(feat....)", "(From...)", "Mono". Um sufixo que nomeia outra gravação, como " - Live", "(Remix)", " - Acoustic" ou "Karaoke", mantém os títulos separados. O artista corresponde quando um nome contém o outro, então "Kevin Rowland & Dexys Midnight Runners" corresponde a "Dexys Midnight Runners".

Quando nenhum candidato do Spotify é confiante, o servidor pergunta ao MusicBrainz por gravações daquele artista e título. Ele não envia chave, no máximo uma requisição por segundo, com um User-Agent que nomeia este projeto. Gravações que o MusicBrainz marca como ao vivo ou remix são deixadas de fora, a menos que o título solicitado diga isso. Suas gravações confiantes vêm primeiro, source musicbrainz, seguidas pelos candidatos do Spotify.

Quando nenhum dos dois tem um candidato confiante, o servidor pesquisa no Soulseek por "artista título" e transforma a melhor cópia em um candidato com source soulseek, confident falso, e o artista, título e duração lidos do nome do arquivo. Seu search_copies classifica as cópias dessa mesma pesquisa em vez de pesquisar novamente.

Os fallbacks precisam de artista e título. Passe-os como artist e title, escreva a consulta como "Artista - Título", ou use uma consulta de texto livre cujo artista o Spotify reconhece no início ou no fim. Uma consulta que é apenas um título recebe os candidatos do Spotify e nada mais.

O id da faixa de cada candidato funciona da mesma forma em search_copies, download, album_listing, album_download e wishlist_add, independentemente da fonte. Um desejo mantém o source e source_id da faixa (spotify:track:<id>, musicbrainz:recording:<mbid>, soulseek:<user>:<path>).

Uma cadeia típica: resolve_track → search_copies com o id da faixa → download com o id da cópia → history. Para o lançamento inteiro: album_listing com o mesmo id da cópia, depois album_download.

Erros vêm em duas formas. Um argumento inválido, um id expirado ou um desejo recusado é um erro de ferramenta (is_error verdadeiro) com um motivo de uma linha. Um download que rodou e falhou é um resultado normal com ok falso, o erro e o estado do slskd. wishlist_add para uma faixa que o dono já aguarda retorna esse desejo com already_waiting verdadeiro em vez de adicionar um segundo.

Um desejo adicionado via MCP pertence ao dono, o primeiro id em TELEGRAM_ALLOWED_USERS. O verificador de lista de desejos do bot o anuncia naquele chat privado e, com /auto ativado lá, busca a cópia como uma busca automática faz. O servidor stdio não roda verificador: seus desejos esperam pelo bot, que lê o mesmo banco de dados quando ambos usam o mesmo DATA_DIR.

Um album_download cujo peer não pode ser listado retorna ok falso com o reason da listagem e nenhum job. Um álbum que o servidor stdio roda é próprio: um reinício do bot no mesmo DATA_DIR não o assume. Se o servidor stdio parar no meio do álbum, nada salva os arquivos que chegam depois; a varredura horária os remove.

Um arquivo deixado com deliver="path" fica em DOWNLOAD_DIR até a varredura removê-lo após ORPHAN_SWEEP_HOURS (6 por padrão), então copie-o antes disso. Um download que expira cancela sua transferência no slskd e exclui o que quer que tenha chegado.

Com deliver="library", o download passa pela verificação lossless (LOSSLESS_GATE, ativada por padrão). Uma cópia lossless cujo espectro mostra que foi feita de um arquivo com perdas é excluída e a próxima cópia do mesmo search_copies é baixada, até LOSSLESS_GATE_MAX_REJECTIONS vezes. Quando a verificação agiu, o resultado também tem:

CampoSignificado
rejectedCada cópia descartada: filename, source, reason ("transcoded from lossy, cutoff 14.0 kHz", ou "upsampled from lossy, ..." para um arquivo hi-res)
copyA cópia sobre a qual o resultado trata: filename, source, quality
kept_lossyPresente quando a cópia foi mantida apenas porque a verificação ficou sem rejeições, com o motivo pelo qual teria sido rejeitada
wishlist_hintA chamada wishlist_add que pesquisa a faixa novamente mais tarde

Quando toda cópia restante é rejeitada, o resultado é ok falso com error "all_rejected" e nada é salvo. deliver="path" não passa pela verificação.

Varredura da biblioteca

As quatro ferramentas library_sweep_* dirigem a mesma varredura que /sweep no Telegram; Varredura da biblioteca tem as regras. Elas respondem com um erro de ferramenta enquanto LIBRARY_SWEEP_USERS está vazio. Uma varredura iniciada via HTTP reporta no Telegram quando termina, como a agendada. O servidor stdio também pode rodar uma varredura, mas não avisa ninguém: consulte library_sweep_status. Dois processos nunca varrem a mesma biblioteca ao mesmo tempo.

Conectar via stdio

Para Claude Code, adicione isto a .mcp.json no seu projeto (ou rode claude mcp add); Claude Desktop aceita o mesmo bloco sob mcpServers em claude_desktop_config.json. Aponte command para um Python que tenha o pacote instalado, de pip install telegram-slskd-local-bot ou do .venv de um checkout.

{
  "mcpServers": {
    "slskd": {
      "command": "python",
      "args": ["-m", "music_downloader", "mcp"],
      "env": {
        "TELEGRAM_ALLOWED_USERS": "123456789",
        "SPOTIFY_CLIENT_ID": "...",
        "SPOTIFY_CLIENT_SECRET": "...",
        "SLSKD_HOST": "http://192.168.1.100:5030",
        "SLSKD_API_KEY": "...",
        "DOWNLOAD_DIR": "/path/to/slskd/downloads",
        "OUTPUT_DIR": "/path/to/music",
        "DATA_DIR": "/path/to/data"
      }
    }
  }
}

O servidor stdio nunca fala com o Telegram, então não precisa de TELEGRAM_BOT_TOKEN. Logs vão para stderr.

Conectar via HTTP

Defina uma porta e um token em .env, descomente o bloco ports em docker-compose.yml e recrie o contêiner.

MCP_PORT=8765
MCP_TOKEN=$(openssl rand -hex 32)

Depois aponte o cliente para a URL com o token. Para Claude Code:

claude mcp add --transport http slskd http://192.168.1.10:8765/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"

ou em .mcp.json:

{
  "mcpServers": {
    "slskd": {
      "type": "http",
      "url": "http://192.168.1.10:8765/mcp",
      "headers": { "Authorization": "Bearer <MCP_TOKEN>" }
    }
  }
}

Uma requisição sem o token correto recebe 401.

Segurança

  • Mantenha na sua LAN. O servidor HTTP não possui TLS. Publique a porta no endereço LAN do host (ou acesse via VPN), nunca em uma interface pública.
  • O token é obrigatório. O bot se recusa a iniciar com MCP_PORT definido e MCP_TOKEN vazio, e compara o token em tempo constante. Qualquer pessoa com o token pode baixar arquivos para a sua biblioteca.
  • Nenhuma regra do Telegram se aplica. O chamador do MCP é o proprietário: TELEGRAM_ALLOWED_USERS não o controla, TELEGRAM_CHAT_DELIVERY_USERS não o restringe à entrega por chat, e ele vê e remove os desejos de todos os chats. Dê o token apenas a agentes aos quais você daria acesso à sua biblioteca.