feat: Refine audio playback eligibility logic with URL validation and classifier fallback
- Replace simple tag-based audio detection with multi-condition validation in isAudioPlayable() - Add AudioUrlValidator.isStreamable() check to verify URL technical playability - Implement AudioClassifier.isAudioUrl() fallback for platform/stream recognition without explicit tags - Prevent Play button display for audio-tagged articles without streamable URLs - Add comprehensive French documentation explaining
This commit is contained in:
parent
e742502bdc
commit
aa32ff1c2b
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -48,12 +48,33 @@ import coil.compose.AsyncImage
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.graphics.Color
|
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")
|
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 {
|
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
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
#Thu Feb 12 10:28:52 2026
|
#Thu Feb 12 12:09:07 2026
|
||||||
VERSION_NAME=1.2.3
|
VERSION_NAME=1.3.0
|
||||||
VERSION_CODE=11
|
VERSION_CODE=12
|
||||||
Loading…
x
Reference in New Issue
Block a user