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 mcpinicia um servidor em stdin e stdout para um agente na mesma máquina. Ele lê as mesmas variáveis de ambiente ou.envque o bot. - Via HTTP, com
MCP_PORTdefinido, o processo do bot também serve MCP sobre HTTP streamable emhttp://<host>:<MCP_PORT>/mcp. Cada requisição precisa deAuthorization: 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.
| Ferramenta | O que faz | Exemplo 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 é resolvida | resolve_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ítulo | search_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 abaixo | download(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 áudio | album_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 outros | album_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óximas | library_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 faixa | wishlist_add(track_id="t3fa9c1", wanted="better") |
wishlist_list() | Todos os desejos, de todos os chats | wishlist_list() |
wishlist_remove(id) | Remove um desejo pelo id que wishlist_list mostra | wishlist_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 ferramenta | library_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á presente | library_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_both | library_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ópria | library_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ão | status() |
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:
| Campo | Significado |
|---|---|
rejected | Cada cópia descartada: filename, source, reason ("transcoded from lossy, cutoff 14.0 kHz", ou "upsampled from lossy, ..." para um arquivo hi-res) |
copy | A cópia sobre a qual o resultado trata: filename, source, quality |
kept_lossy | Presente quando a cópia foi mantida apenas porque a verificação ficou sem rejeições, com o motivo pelo qual teria sido rejeitada |
wishlist_hint | A 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_PORTdefinido eMCP_TOKENvazio, 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_USERSnão o controla,TELEGRAM_CHAT_DELIVERY_USERSnã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.