Limpieza: código muerto, SOLID y duplicación - #32
Merged
Merged
Conversation
The project worked, but it carried leftovers from earlier refactors: a provider that no longer exists, constants nobody read, and a few classes doing three jobs each. None of it broke anything; all of it made every change cost more. Dead code: - `bot_in_voice()` had no caller outside the two tests written for it. - `track["spotify_url"]` was read in two places and written in none, since Spotify support was removed. - `cogs/lyrics.py` split titles on " — " to undo Spotify's "Song — Artist" format. Without Spotify that split truncates legitimate YouTube titles. - `track["stream"]` was set to None on every track and never read. - `config.COOKIES_PATH` was defined but unused: `services/media.py` read the environment variable itself, leaving the setting with two owners. Responsibilities: - `MusicPlayer` no longer builds embeds or sends messages. A new `ChannelAnnouncer` owns that, so wording changes stay out of the audio core. - `MusicPlayer` no longer calls the global `players` registry from `destroy()`. It takes an `on_destroy` callback instead, which breaks the cycle between the class and the registry holding it — the test fixture no longer has to scrub process-wide state after every test. - `PlayerManager` reads `is_destroyed` rather than another object's private attribute, and takes a guild and a channel instead of a whole command Context it only needed two fields from. - Effects come from one registry. Adding one was three edits (filter dict, label dict, a near-identical command method); it is now a single entry, with the command names derived from the registry's keys. - `!help` is generated from the commands actually registered. The hardcoded list had already drifted: `!effect` existed and was documented nowhere. The README row it was also missing is added, and `tests/test_help.py` fails if any command or alias goes undocumented again. Also: `utils/checks` reuses `error_embed` rather than reimplementing it; `lyrics_api` logs instead of printing and reuses one Genius client; and the `@lyrics.error` handler is gone — it leaked raw exception text to users and, because `errors.handle` defers to any command with its own handler, silently disabled central error handling for the whole lyrics cog. Comments that restated their own line are gone. The ones explaining why something is the way it is — the player-client order, stderr as a file rather than a pipe, the load-bearing `aresample` — are kept; each documents a bug that already cost a fix. No behaviour changes beyond two deliberate ones: `!help` now lists `!effect`, and `!lyrics` failures no longer print exception text into the channel. Tests: 293 passing, up from 279. No assertion was relaxed. Closes #31
This was referenced Sep 12, 2026
Isma-L154
added a commit
that referenced
this pull request
Sep 12, 2026
Every lookup died in the client constructor:
WARNING loopify.lyrics: Genius lookup failed for '...':
Genius.__init__() got an unexpected keyword argument 'quiet'
`quiet` belonged to lyricsgenius 2.x. The pinned 3.12.2 has no such argument and
no `verbose` either — it prints nothing of its own, so there was never anything
to silence and the argument is simply dropped rather than replaced.
The TypeError happened while building the client, so it took out every query,
and the command answered "Couldn't find lyrics for ..." — which reads like a
failed search rather than a broken build. That is why it went unnoticed: the
command appeared to work and simply never found anything.
The bug predates the cleanup in #32. What changed is that #32 replaced
`print(f"[Lyrics] Error: {e}")` with a real logger call, so the reason finally
showed up in journalctl with a level and a logger name attached.
Two related problems fixed alongside it:
- No timeout was passed, so a lookup could pin an executor thread indefinitely —
and a pinned executor thread delays the loop's shutdown, the ceiling #35 was
about. It is now bounded at 10s (the library default is 5s).
- A missing token built a client anyway, and lyricsgenius then falls back to
$GENIUS_ACCESS_TOKEN and raises KeyError. `fetch` now gives up before that,
which is also the honest answer for a user typing !lyrics on a deployment
without a token.
`skip_non_songs=True` and `remove_section_headers=False` match the library's
current defaults, but stay explicit because the behaviour is relied on: Genius
indexes tracklists and credits pages, and the `[Chorus]` markers are wanted in
the embed. Tests assert both rather than trusting the defaults to hold.
tests/test_lyrics_api.py is new — this module had no tests at all, which is
exactly how a TypeError in a constructor lived here unnoticed. Nothing in them
talks to Genius; they check that the installed library accepts what we pass, that
a missing token is handled before it can raise, that the call is bounded, and
that the blocking search never runs on the event loop.
Verified against the real API from the server: "Bohemian Rhapsody" / "Queen"
comes back with its section headers intact, and a nonsense query returns None
with no traceback.
Tests: 344, up from 332.
Closes #37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cierra #31.
Limpieza del árbol completo: código muerto, responsabilidades mezcladas y las
duplicaciones que ya habían empezado a desincronizarse. Cero cambios de
comportamiento salvo dos correcciones deliberadas, listadas abajo.
269 líneas añadidas, 338 eliminadas. 293 tests pasando (antes 279), sin
relajar ninguna aserción.
Código muerto eliminado
utils.checks.bot_in_voice()track["spotify_url"](2 sitios)_build_tracknunca escribía esa clave.title.split(" — ")en!lyrics"Canción — Artista"de Spotify. Sin Spotify, truncaba títulos legítimos de YouTube que llevaran un guion largo.track["stream"]Noneen cada track y no se leía en ninguna parte.config.COOKIES_PATHsin dueñoservices/media.pyleía la variable de entorno por su cuenta. Un ajuste, dos dueños.Responsabilidades separadas
MusicPlayerera a la vez motor de audio, maquetador de embeds y emisor demensajes. Los dos últimos trabajos se van a
utils/announcer.py. Cambiar laredacción de un aviso ya no obliga a tocar el núcleo de reproducción.
MusicPlayer↔playersera un ciclo.destroy()llamaba al singletonglobal que contenía al propio player. Ahora recibe un callback
on_destroy. Elefecto más visible está en el fixture de tests:
Construir un player ya no ensucia estado de todo el proceso.
PlayerManagerdeja de leerplayer._destroyed(atributo privado ajeno) yusa
is_destroyed. Y recibe unguildy unchannelen vez de unContextentero de discord.py del que solo necesitaba dos campos.
Añadir un efecto costaba tres ediciones — el filtro en
EFFECTS, el textoen
_LABELS, y un método de comando casi idéntico a los otros ocho. Dos dictscon las mismas claves que podían desincronizarse en silencio. Ahora es una
entrada:
Los nombres de comando salen de las claves del registro.
CogMetarecolectalos comandos al ejecutar el cuerpo de la clase, así que no se puede registrar
uno por efecto a posteriori; un único handler con cada nombre como alias da el
mismo resultado desde una sola definición, y
ctx.invoked_withdice cuál setecleó. Un test nuevo recorre
EFFECTSy falla si una clave no es alcanzablecomo comando.
!helpse genera del registro real de comandos. La lista escrita a mano yahabía derivado:
!effectexistía y no aparecía ni en la ayuda ni en elREADME, y nada fallaba.
tests/test_help.pyahora falla si cualquier comandoo alias queda sin documentar.
main.pypasa de 96 a 68 líneas.Otros
utils/checks._errorera idéntica autils.embeds.error_embed. Ahora reusa.services/lyrics_apiusabaprint()— no llegaba ajournalctlcon nivel nitimestamp, contra la regla de logging del
CLAUDE.md. Ahora usalogging, yreutiliza un solo cliente de Genius en vez de construir uno por consulta.
@lyrics.error. Hacíactx.send(str(error)), lo queexponía texto crudo de excepción al usuario y, como
errors.handlese rindeante cualquier comando con handler propio, desactivaba el manejo central de
errores para todo el cog de letras — justo lo que construyó el Answer every wrong command invocation #16.
Comentarios
Se borran los que repiten su propia línea y los banners decorativos. Se
conservan los que explican el porqué: el orden de
_PLAYER_CLIENTS, por quéel stderr de
AudioStreames un archivo y no un pipe, por qué elaresampleinicial es load-bearing. Cada uno documenta un bug que ya costó un arreglo
(#17, #25, #28, #30); borrarlos invita a reintroducirlos.
deploy/launch_ec2.shse queda: la EC2 está detenida, no terminada, ydeploy/README.mdla documenta como camino de despliegue alterno.Cómo probarlo
pip install -r requirements.txt -r requirements-dev.txt pytest # 293 pasandoLos tests de efectos renderizan audio con FFmpeg real y se saltan solos si no
está instalado.
En Discord, lo que conviene mirar a mano:
!help— debe listar!effect, y los 8 efectos en una sola línea.!play <algo>→!bass→!nightcoredentro de 3 s — el segundo deberesponder "wait 3s", no aplicarse.
!play→!bassa mitad de canción — debe seguir reanudando en la mismaposición, no volver a 0:00.
!lyricssin nada sonando, y!lyrics Bohemian Rhapsody - Queen.!skipcon la cola vacía,!remove 99,!move 1 99— cada uno responde.!stopy luego!playotra vez — el player se recrea; conis_destroyedes el mismo camino que antes.
Fuera de alcance
El bot murió el 2026-09-11 a las 06:18 UTC con
ClientConnectorDNSErrordurante un
unattended-upgradesque actualizó glibc y reinició el resolver.bot.start()no tiene reintento, así que cualquier hipo de DNS en el arranquemata el proceso.
Restart=on-failurelo recuperó al tercer intento. Es uncambio de comportamiento y va en su propio issue, no mezclado aquí.