package org.terst.feedreader.readability /** * Why an extraction did not produce article content. * * Extraction used to fail silently: ReaderScreen did `extracted?.content?.let { }`, * so a null result — whatever caused it — left the reader showing the raw feed * content with nothing logged and nothing shown. The only way a failure ever * surfaced was the user noticing and filing a bug report by hand, which could * not say why either. */ sealed interface ExtractionFailure { /** The page load reported a transport-level error. */ data class LoadError(val code: Int, val description: String?) : ExtractionFailure /** The server answered, but with an error status. */ data class HttpError(val statusCode: Int, val reason: String?) : ExtractionFailure /** The page never finished loading within the budget. */ data class TimedOut(val millis: Long) : ExtractionFailure /** Readability itself threw, or returned something unusable. */ data class ReadabilityError(val message: String) : ExtractionFailure /** Readability ran but found no article body worth showing. */ data object NoContent : ExtractionFailure /** The JS result could not be decoded. */ data class BadResult(val raw: String) : ExtractionFailure /** * Readability returned content, but it was the page's cookie/consent notice, * not the article -- see [ConsentNotice]. */ data object ConsentInterstitial : ExtractionFailure } private const val MILLIS_PER_SECOND = 1_000 /** * How much of the raw JS payload a diagnostic keeps. The result can be tens of * kilobytes; a bug report needs enough to recognise the shape of it, not all. */ private const val RAW_DIAGNOSTIC_CHARS = 200 /** A short, user-facing explanation. Shown in a snackbar and in bug reports. */ fun ExtractionFailure.userMessage(): String = when (this) { is ExtractionFailure.LoadError -> "Couldn't load the page" + (description?.takeIf { it.isNotBlank() }?.let { " ($it)" } ?: "") is ExtractionFailure.HttpError -> "Server returned HTTP $statusCode" is ExtractionFailure.TimedOut -> "Page took longer than ${millis / MILLIS_PER_SECOND}s to load" is ExtractionFailure.ReadabilityError -> "Couldn't parse the article ($message)" ExtractionFailure.NoContent -> "No article text found on the page" is ExtractionFailure.BadResult -> "Unexpected result from the page" ExtractionFailure.ConsentInterstitial -> "Only the site's cookie notice came through, not the article" } /** * A longer form for the bug report, carrying the detail the short message drops. * Bug reports are the only channel for a failure the user has already lost, so * this keeps the raw specifics rather than the tidy summary. */ fun ExtractionFailure.diagnostic(): String = when (this) { is ExtractionFailure.LoadError -> "LoadError code=$code description=${description ?: "none"}" is ExtractionFailure.HttpError -> "HttpError status=$statusCode reason=${reason ?: "none"}" is ExtractionFailure.TimedOut -> "TimedOut after ${millis}ms" is ExtractionFailure.ReadabilityError -> "ReadabilityError $message" ExtractionFailure.NoContent -> "NoContent (Readability returned no usable body)" is ExtractionFailure.BadResult -> "BadResult raw=${raw.take(RAW_DIAGNOSTIC_CHARS)}" ExtractionFailure.ConsentInterstitial -> "ConsentInterstitial (extracted content was the page's cookie/consent notice)" }