telegram-slskd-local-bot
Servidor MCP de un buscador de música Soulseek autoalojado: resuelve una canción en Spotify o MusicBrainz, lista sus copias en Soulseek a través de slskd, descarga una o un álbum completo en la biblioteca y mantiene una lista de deseos.
Documentación
El bot también habla el Model Context Protocol, por lo que un agente como Claude Code o Claude Desktop puede encontrar una canción, elegir una copia y guardarla sin Telegram. Impulsa el mismo pipeline que el bot: la misma búsqueda en Spotify, la misma búsqueda y clasificación en slskd, la misma verificación de pérdida sin pérdida, la misma biblioteca, historial y lista de deseos.
Hay dos formas de acceder a él:
- A través de stdio,
python -m music_downloader mcpinicia un servidor en stdin y stdout para un agente en la misma máquina. Lee las mismas variables de entorno o.envque el bot. - A través de HTTP, con
MCP_PORTconfigurado, el proceso del bot también sirve MCP sobre HTTP transmisible enhttp://<host>:<MCP_PORT>/mcp. Cada solicitud necesitaAuthorization: Bearer <MCP_TOKEN>. Este servidor comparte la base de datos, el cliente slskd y el verificador de lista de deseos del bot.
Herramientas
Cada herramienta devuelve un objeto JSON. Las pistas y copias vienen con identificadores cortos (t3fa9c1, c81d0e2) que la siguiente llamada toma; un identificador es válido durante una hora.
| Herramienta | Qué hace | Ejemplo de llamada |
|---|---|---|
resolve_track(query, artist, title, duration_secs) | Candidatos de pista para una consulta de texto libre, o para un artista y título, primero los más confiables, cada uno con un id de pista, artista, título, álbum, año, duración, source (spotify, musicbrainz o soulseek), source_id y confident. Spotify primero, MusicBrainz cuando Spotify no tiene un candidato confiable, la mejor copia de Soulseek cuando ninguno tiene uno. Ver Cómo se resuelve una pista | resolve_track(artist="Graham Central Station", title="Jam", duration_secs=219.9) |
search_copies(track_id, profile="library", limit=10) | Copias de Soulseek de esa pista, primero las mejores, cada una con un id de copia, formato, calidad, nivel de calidad, tamaño, duración, usuario fuente, puntuación, lossless y fits_cap (bajo TELEGRAM_MAX_UPLOAD_MB). profile="library" pone primero las de pérdida sin pérdida; "chat" clasifica por sonido por megabyte. Busca "artista título", luego solo el título | search_copies(track_id="t3fa9c1", profile="library", limit=5) |
download(copy_id, deliver="library") | Descarga la copia y espera por ella, hasta DOWNLOAD_TIMEOUT_SECS, enviando notificaciones de progreso. deliver="library" la renombra, incrusta la portada, la mueve a OUTPUT_DIR y la registra en el historial; "path" deja el archivo en DOWNLOAD_DIR y devuelve su ruta. Ambos devuelven el veredicto de la verificación de pérdida sin pérdida (FLAC, WAV y AIFF; null en caso contrario). "library" pasa por la puerta de pérdida sin pérdida, ver abajo | download(copy_id="c81d0e2", deliver="library") |
album_listing(copy_id) | Los archivos de audio de la carpeta del par de la que proviene la copia, generalmente su lanzamiento completo: nombre, formato, calidad, tamaño y duración de cada uno, el tamaño total y los formatos presentes. answered es falso, con un reason (no_answer, unreachable), cuando el par está fuera de línea o no responde dentro de 30 s; no_audio cuando la carpeta no contiene audio | album_listing(copy_id="c81d0e2") |
album_download(copy_id, deliver="library") | Descarga cada archivo de audio de esa carpeta (una solicitud slskd para todo el lote), con progreso por archivo. deliver="library" guarda cada archivo en OUTPUT_DIR a medida que llega, nombrado desde sus etiquetas (o su nombre de archivo cuando faltan las etiquetas), con la portada de Spotify del álbum y una fila de historial anotada album; un archivo que la biblioteca ya tiene bajo ese nombre se omite (skipped verdadero, path el archivo de la biblioteca) y su descarga se elimina. "path" deja los archivos en DOWNLOAD_DIR. Un archivo se rinde cuando su transferencia no mueve ningún byte durante DOWNLOAD_TIMEOUT_SECS; el álbum completo espera hasta ALBUM_TIMEOUT_SECS. Devuelve un resultado por archivo (ok, path, skipped, error, state) y los conteos; un archivo fallido nunca detiene a los demás | album_download(copy_id="c81d0e2", deliver="library") |
history(limit=20) | Las descargas más recientes, primero las más nuevas, con su estado (success, delivered, failed...) | history(limit=10) |
library_has(artist, title) | Si la biblioteca contiene un archivo que se parece a esta pista (la verificación de duplicados del bot), con las coincidencias más cercanas | library_has(artist="Nancy Sinatra", title="Bang Bang") |
wishlist_add(track_id, wanted="any") | Busca la pista de nuevo cada WISHLIST_CHECK_HOURS. "any" espera cualquier copia; "better" espera una copia por encima de la mejor que search_copies encontró para esa pista | wishlist_add(track_id="t3fa9c1", wanted="better") |
wishlist_list() | Cada deseo, de cada chat | wishlist_list() |
wishlist_remove(id) | Elimina un deseo por el id que wishlist_list muestra | wishlist_remove(id=4) |
library_sweep_run(force=false) | Inicia un barrido de biblioteca en segundo plano y regresa de inmediato con el id de ejecución y el estado. force=true verifica cada canción sin importar su nivel y última verificación. Solo con LIBRARY_SWEEP_USERS configurado; de lo contrario, un error de herramienta | library_sweep_run() |
library_sweep_status() | El barrido actual o el último (posición, conteos por resultado, archivo actual), canciones por nivel, pares en espera, el horario y la próxima inicio, si fpcalc está allí | library_sweep_status() |
library_sweep_reviews() | Los pares que esperan una decisión: tallo, por qué, la ruta de ambos archivos, calidad, duración y corte, la similitud de huellas dactilares y el nombre que la propuesta obtiene con keep_both | library_sweep_reviews() |
library_sweep_decide(stem, decision) | keep_mine elimina la propuesta, take_new reemplaza la canción con ella (la canción se estaciona durante LIBRARY_SWEEP_KEEP_DAYS), keep_both la agrega como una canción propia | library_sweep_decide(stem="Nancy Sinatra - Bang Bang", decision="keep_both") |
status() | Si slskd responde, descargas esperando un botón Guardar, Rechazar o Reintentar en Telegram, descargas MCP en ejecución, el número de deseos, el límite de subida y la versión | status() |
Cómo se resuelve una pista
resolve_track pregunta a Spotify primero. Un candidato es confident cuando su artista y título son los solicitados una vez que se pliega la ortografía y, si pasas duration_secs (la duración del archivo que tienes), cuando dura dentro de 8 segundos de eso. El plegado ignora acentos, apóstrofes (rectos, curvos o faltantes), & contra "y", un "The" inicial y sufijos que nombran la misma grabación: " - Single Edit", " - 2011 Remaster", "(feat....)", "(From...)", "Mono". Un sufijo que nombra otra grabación, como " - Live", "(Remix)", " - Acoustic" o "Karaoke", mantiene los títulos separados. El artista coincide cuando un nombre contiene al otro, por lo que "Kevin Rowland & Dexys Midnight Runners" coincide con "Dexys Midnight Runners".
Cuando ningún candidato de Spotify es confiable, el servidor pregunta a MusicBrainz por grabaciones de ese artista y título. No envía clave, como máximo una solicitud por segundo, con un User-Agent que nombra este proyecto. Las grabaciones que MusicBrainz marca como en vivo o remix se omiten a menos que el título solicitado lo diga. Sus grabaciones confiables vienen primero, source musicbrainz, seguidas de los candidatos de Spotify.
Cuando ninguno tiene un candidato confiable, el servidor busca en Soulseek "artista título" y convierte la mejor copia en un candidato con source soulseek, confident falso, y el artista, título y duración leídos del nombre del archivo. Su search_copies clasifica las copias de esa misma búsqueda en lugar de buscar de nuevo.
Los respaldos necesitan tanto un artista como un título. Pásalos como artist y title, escribe la consulta como "Artista - Título", o usa una consulta de texto libre cuyo artista Spotify reconozca al inicio o al final. Una consulta que es solo un título obtiene los candidatos de Spotify y nada más.
El id de pista de cada candidato funciona igual en search_copies, download, album_listing, album_download y wishlist_add, sin importar su fuente. Un deseo mantiene el source y source_id de la pista (spotify:track:<id>, musicbrainz:recording:<mbid>, soulseek:<user>:<path>).
Una cadena típica: resolve_track → search_copies con el id de pista → download con el id de copia → history. Para el lanzamiento completo: album_listing con el mismo id de copia, luego album_download.
Los errores vienen en dos formas. Un argumento incorrecto, un id caducado o un deseo rechazado es un error de herramienta (is_error verdadero) con una razón de una línea. Un download que se ejecutó y falló es un resultado normal con ok falso, el error y el estado de slskd. wishlist_add para una pista que el propietario ya espera devuelve ese deseo con already_waiting verdadero en lugar de agregar un segundo.
Un deseo agregado a través de MCP pertenece al propietario, el primer id en TELEGRAM_ALLOWED_USERS. El verificador de lista de deseos del bot lo anuncia en ese chat privado, y con /auto allí, obtiene la copia como lo hace una búsqueda automática. El servidor stdio no ejecuta verificador: sus deseos esperan al bot, que lee la misma base de datos cuando ambos usan el mismo DATA_DIR.
Un album_download cuyo par no se puede listar devuelve ok falso con el reason del listado y sin trabajo. Un álbum que el servidor stdio ejecuta es propio: un reinicio del bot en el mismo DATA_DIR no lo retoma. Si el servidor stdio se detiene a mitad del álbum, nada guarda los archivos que llegan después; el barrido por hora los elimina.
Un archivo dejado con deliver="path" permanece en DOWNLOAD_DIR hasta que el barrido lo elimine después de ORPHAN_SWEEP_HOURS (6 por defecto), así que cópialo antes de eso. Un download que agota el tiempo cancela su transferencia en slskd y elimina lo que haya llegado.
Con deliver="library" la descarga pasa por la puerta de pérdida sin pérdida (LOSSLESS_GATE, activada por defecto). Una copia sin pérdida cuyo espectro muestra que se hizo desde un archivo con pérdida se elimina y la siguiente copia del mismo search_copies se descarga en su lugar, hasta LOSSLESS_GATE_MAX_REJECTIONS veces. Cuando la puerta actuó, el resultado también tiene:
| Campo | Significado |
|---|---|
rejected | Cada copia descartada: filename, source, reason ("transcoded from lossy, cutoff 14.0 kHz", o "upsampled from lossy, ..." para un archivo de alta resolución) |
copy | La copia sobre la que trata el resultado: filename, source, quality |
kept_lossy | Presente cuando la copia se mantuvo solo porque la puerta se quedó sin rechazos, con la razón por la que se habría rechazado |
wishlist_hint | La llamada wishlist_add que busca la pista de nuevo más tarde |
Cuando cada copia restante es rechazada, el resultado es ok falso con error "all_rejected" y no se guarda nada. deliver="path" no está sujeto a la puerta.
Barrido de biblioteca
Las cuatro herramientas library_sweep_* impulsan el mismo barrido que /sweep en Telegram; Barrido de biblioteca tiene las reglas. Responden con un error de herramienta mientras LIBRARY_SWEEP_USERS está vacío. Un barrido iniciado a través de HTTP informa en Telegram cuando termina, como el programado. El servidor stdio también puede ejecutar un barrido, pero no le dice a nadie: consulta library_sweep_status. Dos procesos nunca barren la misma biblioteca a la vez.
Conectar a través de stdio
Para Claude Code, agrega esto a .mcp.json en tu proyecto (o ejecuta claude mcp add); Claude Desktop toma el mismo bloque bajo mcpServers en claude_desktop_config.json. Apunta command a un Python que tenga el paquete instalado, desde pip install telegram-slskd-local-bot o el .venv de un 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"
}
}
}
}
El servidor stdio nunca habla con Telegram, por lo que no necesita TELEGRAM_BOT_TOKEN. Los registros van a stderr.
Conectar a través de HTTP
Configura un puerto y un token en .env, descomenta el bloque ports en docker-compose.yml y recrea el contenedor.
MCP_PORT=8765
MCP_TOKEN=$(openssl rand -hex 32)
Luego apunta el cliente a la URL con el token. Para Claude Code:
claude mcp add --transport http slskd http://192.168.1.10:8765/mcp \
--header "Authorization: Bearer <MCP_TOKEN>"
o en .mcp.json:
{
"mcpServers": {
"slskd": {
"type": "http",
"url": "http://192.168.1.10:8765/mcp",
"headers": { "Authorization": "Bearer <MCP_TOKEN>" }
}
}
}
Una solicitud sin el token correcto obtiene 401.
Seguridad
- Mantenlo en tu LAN. El servidor HTTP no tiene TLS. Publica el puerto en la dirección LAN del host (o accede a él a través de una VPN), nunca en una interfaz pública.
- El token es obligatorio. El bot se niega a iniciar con
MCP_PORTconfigurado yMCP_TOKENvacío, y compara el token en tiempo constante. Cualquiera que tenga el token puede descargar en tu biblioteca. - No se aplican reglas de Telegram. El llamador de MCP es el propietario:
TELEGRAM_ALLOWED_USERSno lo restringe,TELEGRAM_CHAT_DELIVERY_USERSno lo bloquea a la entrega por chat, y ve y elimina los deseos de cada chat. Dale el token solo a agentes a los que les darías tu biblioteca.