Mi pestaña Discover estaba vacía porque le hice la pregunta equivocada a Apple Music
Esta app se renombró después a Deep Cut Atlas. Acá abajo se la llama “Discoverer” en todo el texto, porque así se llamaba el día en que pasó esto.
La pestaña Discover en mi app es la razón de ser de la app entera: tomar los artistas ya presentes en la biblioteca de Apple Music y mostrar sus lanzamientos que todavía no se tienen. Deep cuts. La pestaña llegó a mi propio teléfono mostrando “Ya está todo al día” con una lista vacía, y la consola de Xcode escupía errores cada vez que cargaba. Función central, muerta.
El síntoma me mintió
Un estado vacío de “todo al día” y una consola llena de errores parecen dos errores distintos. Mi primer instinto fue pensar en limitación de tasa: la pestaña recorría toda mi biblioteca, obtenía el catálogo de cada artista, y comparaba. Disparar tantas solicitudes contra MusicKit y este responde con resistencia. Esa historia explicaba los errores pero no el feed vacío, y me tenía yendo hacia una solución de throttling antes de entender nada.
Lo honesto era dejar de adivinar y leer lo que el dispositivo realmente hacía. MusicKit no devuelve nada en el simulador, así que este es un error exclusivo de dispositivo físico. Conecté un diagnóstico descartable detrás de una bandera de lanzamiento, compilé e instalé en mi teléfono vía devicectl, y capturé la consola.
El error real: los ids de biblioteca no son ids de catálogo
El código viejo hacía esto:
var request = MusicCatalogResourceRequest<Artist>(matching: \.id, equalTo: MusicItemID(libraryArtist.id))
request.properties = [.albums]
Tomaba un id de artista de la biblioteca del usuario y le pedía ese id al catálogo. Esos son espacios de id distintos. En el dispositivo, la solicitud no devuelve vacío en silencio, lanza un error HTTP (parecía un 404) para prácticamente todos los artistas. Eso era la inundación de la consola.
¿Y el feed vacío? Un puñado de artistas resultó devolver vacío en vez de lanzar un error, así que la protección “¿fallaron todos los artistas?” nunca se activó. El código concluyó “nada falló, simplemente no hay nada nuevo” y mostró “todo al día”. Los dos síntomas eran el mismo error con dos disfraces distintos.
La solución: pedirle al objeto de biblioteca sus álbumes de catálogo
Apple deliberadamente no expone el id de catálogo de un elemento de biblioteca (está escondido en un playParameters.catalogID privado). El puente soportado es with(_:preferredSource:): conservar el objeto Artist de biblioteca y pedirle que cargue una relación desde el catálogo.
let enriched = try await libraryArtist.with([.albums], preferredSource: .catalog)
let catalogAlbums = enriched.albums // the artist's catalog discography
Lo que no podía saber sin el dispositivo: ¿preferredSource: .catalog devuelve la discografía completa del artista, o solo los álbumes que ya se poseen? Es una preferencia, no una garantía, y si solo devolviera lo ya poseído el feed habría quedado vacío de una forma nueva y más confusa. Así que hice que el diagnóstico imprimiera, por artista, total / poseído / la diferencia. El dispositivo respondió con claridad:
'A.N.I.M.A.L.' total=25 owned=4 cuts=21
'A$AP Rocky' total=25 owned=6 cuts=19
'Acid Bath' total=7 owned=6 cuts=1
Discografía completa. Cortes reales después de la comparación. Una salvedad que vale la pena conocer: with(.catalog) limita .albums a unos 25 sin paginación explícita, lo cual alcanza de sobra para un feed pero es algo para tener presente con artistas prolíficos.
El otro número que importó: 1.610
El diagnóstico también imprimió el conteo de artistas de mi biblioteca: 1.610. El diseño anterior obtenía cada uno de esos catálogos en cada carga. Incluso con el error del id arreglado, son ~1.600 solicitudes de red para abrir una pestaña. Así que arreglar el id era necesario pero no suficiente.
Rediseñé el feed para que fuera incremental. Cada actualización revisa un lote acotado (40 artistas), guarda en caché los cortes de cada artista con una marca de tiempo de última revisión, y el feed es la unión de todo lo cacheado. Las actualizaciones van rotando por la biblioteca en vez de recorrerla entera cada vez.
La parte que más me gusta es el orden. Una pestaña de “descubrimiento” no debería priorizar los artistas que ya están en rotación pesada, debería resurgir a los que quedaron olvidados. Así que la clasificación pone primero a los artistas ausentes de lo reproducido recientemente, y después a los que llevan más tiempo sin revisarse. Lo reproducido recientemente pasó de ser algo que se favorecía a algo que se penaliza.
Lecciones
- Un estado vacío y un estado de error pueden ser el mismo error. La pantalla de “todo al día” no era un problema separado de los errores de consola, era consecuencia de ellos. Perseguirlos por separado habría desperdiciado horas.
- Para frameworks exclusivos de dispositivo, pasar al dispositivo temprano. Casi diseñé una solución de throttling alrededor de una causa adivinada. Lo más útil que hice fue imprimir conteos reales de mi biblioteca real.
- Las APIs “preferidas” necesitan que su semántica se confirme, no que se asuma.
preferredSource: .catalogcompila sin importar si devuelve la discografía completa o solo lo poseído. Solo el dispositivo dice cuál de las dos. - El tamaño de la biblioteca es un dato de diseño. 1.610 artistas convirtieron “traer todo” de perezoso en roto.
Lecturas relacionadas
La ventana espejada mintió sobre que el teléfono estaba desbloqueado
Un arreglo de agrupación de una línea que ya estaba escrito, y tres muros entre eso y la prueba: aislamiento de actores, el alcance exclusivo a Mac de log stream, y una sesión espejada que parece desbloqueada cuando el dispositivo no lo está.
Álbumes de preestreno, y la solución que no pude verificar del todo
Los marcadores de posición 'Track N' de Apple se mostraban como si fueran datos reales. La única señal de detección que no pude confirmar se convirtió en la única señal de la que dejé de depender.
Un botón de suscripción, una API de MusicKit que nunca había usado, y una sheet que se cierra en silencio
musicSubscriptionOffer presenta la sheet nativa de Apple, y nada en la app se enteraría jamás de que se cerró. La acción correctamente codificada que igual era un callejón sin salida funcional.