Skip to content

Limpieza: código muerto, SOLID y duplicación - #32

Merged
Isma-L154 merged 1 commit into
mainfrom
limpieza-solid-codigo-muerto
Sep 12, 2026
Merged

Isma-L154 merged 1 commit into
mainfrom
limpieza-solid-codigo-muerto

Conversation

@Isma-L154

Copy link
Copy Markdown
Owner

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

Qué Por qué estaba muerto
utils.checks.bot_in_voice() Ningún cog la llamaba; sus únicas referencias eran los dos tests escritos para ella.
track["spotify_url"] (2 sitios) Spotify se eliminó del proyecto; _build_track nunca escribía esa clave.
title.split(" — ") en !lyrics Deshacía el formato "Canción — Artista" de Spotify. Sin Spotify, truncaba títulos legítimos de YouTube que llevaran un guion largo.
track["stream"] Se fijaba a None en cada track y no se leía en ninguna parte.
config.COOKIES_PATH sin dueño Se definía pero nadie la importaba: services/media.py leía la variable de entorno por su cuenta. Un ajuste, dos dueños.

Responsabilidades separadas

MusicPlayer era a la vez motor de audio, maquetador de embeds y emisor de
mensajes.
Los dos últimos trabajos se van a utils/announcer.py. Cambiar la
redacción de un aviso ya no obliga a tocar el núcleo de reproducción.

MusicPlayer ↔ players era un ciclo. destroy() llamaba al singleton
global que contenía al propio player. Ahora recibe un callback on_destroy. El
efecto más visible está en el fixture de tests:

 def player(fake_bot, fake_guild):
-    p = MusicPlayer(fake_bot, fake_guild, MagicMock())
-    yield p
-    # Keep the module-level singleton clean between tests.
-    players.discard(fake_guild.id)
+    return MusicPlayer(fake_bot, fake_guild, MagicMock())

Construir un player ya no ensucia estado de todo el proceso.

PlayerManager deja de leer player._destroyed (atributo privado ajeno) y
usa is_destroyed. Y recibe un guild y un channel en vez de un Context
entero de discord.py del que solo necesitaba dos campos.

Añadir un efecto costaba tres ediciones — el filtro en EFFECTS, el texto
en _LABELS, y un método de comando casi idéntico a los otros ocho. Dos dicts
con las mismas claves que podían desincronizarse en silencio. Ahora es una
entrada:

EFFECTS = {
    "echo": Effect("aecho=0.8:0.88:60:0.4", "Echo effect applied 🔔"),
}

Los nombres de comando salen de las claves del registro. CogMeta recolecta
los 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_with dice cuál se
tecleó. Un test nuevo recorre EFFECTS y falla si una clave no es alcanzable
como comando.

!help se genera del registro real de comandos. La lista escrita a mano ya
había derivado: !effect existía y no aparecía ni en la ayuda ni en el
README
, y nada fallaba. tests/test_help.py ahora falla si cualquier comando
o alias queda sin documentar. main.py pasa de 96 a 68 líneas.

Otros

  • utils/checks._error era idéntica a utils.embeds.error_embed. Ahora reusa.
  • services/lyrics_api usaba print() — no llegaba a journalctl con nivel ni
    timestamp, contra la regla de logging del CLAUDE.md. Ahora usa logging, y
    reutiliza un solo cliente de Genius en vez de construir uno por consulta.
  • Se elimina el handler @lyrics.error. Hacía ctx.send(str(error)), lo que
    exponía texto crudo de excepción al usuario y, como errors.handle se rinde
    ante 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 AudioStream es un archivo y no un pipe, por qué el aresample
inicial es load-bearing. Cada uno documenta un bug que ya costó un arreglo
(#17, #25, #28, #30); borrarlos invita a reintroducirlos.

deploy/launch_ec2.sh se queda: la EC2 está detenida, no terminada, y
deploy/README.md la documenta como camino de despliegue alterno.

Cómo probarlo

pip install -r requirements.txt -r requirements-dev.txt
pytest                  # 293 pasando

Los tests de efectos renderizan audio con FFmpeg real y se saltan solos si no
está instalado.

En Discord, lo que conviene mirar a mano:

  1. !help — debe listar !effect, y los 8 efectos en una sola línea.
  2. !play <algo> → !bass → !nightcore dentro de 3 s — el segundo debe
    responder "wait 3s", no aplicarse.
  3. !play → !bass a mitad de canción — debe seguir reanudando en la misma
    posición, no volver a 0:00.
  4. !lyrics sin nada sonando, y !lyrics Bohemian Rhapsody - Queen.
  5. !skip con la cola vacía, !remove 99, !move 1 99 — cada uno responde.
  6. !stop y luego !play otra vez — el player se recrea; con is_destroyed
    es el mismo camino que antes.

Fuera de alcance

El bot murió el 2026-09-11 a las 06:18 UTC con ClientConnectorDNSError
durante un unattended-upgrades que actualizó glibc y reinició el resolver.
bot.start() no tiene reintento, así que cualquier hipo de DNS en el arranque
mata el proceso. Restart=on-failure lo recuperó al tercer intento. Es un
cambio de comportamiento y va en su propio issue, no mezclado aquí.

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
@Isma-L154
Isma-L154 merged commit 3f5e6e2 into main Sep 12, 2026
3 checks passed
@Isma-L154
Isma-L154 deleted the limpieza-solid-codigo-muerto branch September 12, 2026 03:42
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant