diff --git a/app/src/main/java/com/shaarit/domain/audio/AudioUrlValidator.kt b/app/src/main/java/com/shaarit/domain/audio/AudioUrlValidator.kt new file mode 100644 index 0000000..03f42ac --- /dev/null +++ b/app/src/main/java/com/shaarit/domain/audio/AudioUrlValidator.kt @@ -0,0 +1,85 @@ +package com.shaarit.domain.audio + +import com.shaarit.domain.classifier.AudioClassifier +import java.net.URI + +/** + * Validateur rapide (sans appel réseau) pour déterminer si une URL + * est techniquement lisible par le lecteur audio intégré. + * + * Utilisé dans l'UI pour conditionner l'affichage du bouton Play : + * afficher le bouton SEULEMENT si (tag audio) ET (URL streamable). + * + * Cas couverts : + * - Extensions audio directes : .mp3, .aac, .ogg, .opus, .flac, .wav, .m4a, .wma + * - Extensions playlist/stream : .m3u, .m3u8, .pls, .asx + * - Extensions feed : .rss, .xml (flux podcast) + * - Protocoles radio : Icecast/Shoutcast (ports :8xxx, /stream, sous-domaines stream/live) + * - Plateformes supportées : Spotify (track/episode/show/album), SoundCloud, + * Mixcloud, Bandcamp, Deezer, Tidal, Apple Music/Podcasts, YouTube Music, + * TuneIn, Radio Garden, etc. + * - Tout ce que AudioClassifier.classify() reconnaît déjà + * + * Performance : uniquement du String/Regex matching, O(1) — safe pour LazyColumn. + */ +object AudioUrlValidator { + + // Extensions audio directes jouables par ExoPlayer + private val DIRECT_AUDIO_EXTENSIONS = Regex( + """\.(mp3|aac|ogg|oga|opus|flac|wav|m4a|wma)(\?.*)?$""", + RegexOption.IGNORE_CASE + ) + + // Extensions playlist/stream + private val STREAM_PLAYLIST_EXTENSIONS = Regex( + """\.(m3u|m3u8|pls|asx|mpd)(\?.*)?$""", + RegexOption.IGNORE_CASE + ) + + // Extensions feed (podcast RSS) + private val FEED_EXTENSIONS = Regex( + """\.(rss|xml|atom)(\?.*)?$""", + RegexOption.IGNORE_CASE + ) + + // Ports typiques de flux Icecast/Shoutcast (:8000-:8999) + private val STREAMING_PORT_PATTERN = Regex(""":8\d{3}(/|$)""") + + // Chemins typiques de flux radio (/stream, /listen, /live, /;) + private val STREAMING_PATH_PATTERN = Regex( + """/(stream|listen|live|radio|;)(/|$|\?)""", + RegexOption.IGNORE_CASE + ) + + /** + * Détermine si l'URL est techniquement lisible par le lecteur audio. + * Vérification rapide, sans appel réseau. + * + * @return true si l'URL pointe vers un flux audio, un fichier audio, + * ou une plateforme audio supportée. + */ + fun isStreamable(url: String): Boolean { + if (url.isBlank()) return false + val lowerUrl = url.lowercase() + + // 1. AudioClassifier reconnaît déjà les plateformes + flux radio/podcast/musique + if (AudioClassifier.isAudioUrl(url)) return true + + // 2. Extension audio directe (.mp3, .ogg, etc.) + if (DIRECT_AUDIO_EXTENSIONS.containsMatchIn(lowerUrl)) return true + + // 3. Extension playlist/stream (.m3u, .m3u8, .pls, .asx, .mpd) + if (STREAM_PLAYLIST_EXTENSIONS.containsMatchIn(lowerUrl)) return true + + // 4. Extension feed (.rss, .xml) — potentiellement un podcast + if (FEED_EXTENSIONS.containsMatchIn(lowerUrl)) return true + + // 5. Port de streaming typique (:8000-:8999) + if (STREAMING_PORT_PATTERN.containsMatchIn(lowerUrl)) return true + + // 6. Chemin de streaming typique (/stream, /listen, /live) + if (STREAMING_PATH_PATTERN.containsMatchIn(lowerUrl)) return true + + return false + } +} diff --git a/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt b/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt index 46f57a4..675e0b8 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt @@ -48,12 +48,33 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.graphics.Color +import com.shaarit.domain.audio.AudioUrlValidator +import com.shaarit.domain.classifier.AudioClassifier private val AUDIO_TAGS = setOf("podcast", "radio", "music", "audio") +/** + * Détermine si le bouton Play doit être affiché. + * + * Condition principale : tag audio ET URL techniquement lisible. + * Fallback : si l'URL est un flux audio évident (ex: .m3u8, plateforme connue), + * le bouton s'affiche même sans tag audio explicite. + */ fun ShaarliLink.isAudioPlayable(): Boolean { - return tags.any { it.lowercase() in AUDIO_TAGS } || + val hasAudioTag = tags.any { it.lowercase() in AUDIO_TAGS } || contentType?.lowercase() in AUDIO_TAGS + val isStreamable = AudioUrlValidator.isStreamable(url) + + // Cas 1 : tag audio + URL streamable → Play + if (hasAudioTag && isStreamable) return true + + // Cas 2 : URL reconnue comme audio par le classifieur (plateforme/flux) + // même sans tag → Play (ex: lien Spotify oublié sans tag "music") + if (AudioClassifier.isAudioUrl(url)) return true + + // Cas 3 : tag audio seul SANS URL streamable → PAS de Play + // (ex: article de blog tagué "audio" → on ne montre pas Play) + return false } /** diff --git a/version.properties b/version.properties index e864067..826c26b 100644 --- a/version.properties +++ b/version.properties @@ -1,3 +1,3 @@ -#Thu Feb 12 10:28:52 2026 -VERSION_NAME=1.2.3 -VERSION_CODE=11 \ No newline at end of file +#Thu Feb 12 12:09:07 2026 +VERSION_NAME=1.3.0 +VERSION_CODE=12 \ No newline at end of file