feat(lyrics): тексты песен внутри приложения через LRCLIB
- LrcLibApi (api/get + api/search, User-Agent), DI @Named(lrclib) Retrofit - LyricsRepository.fetchLyrics -> LyricsResult (plain/synced/instrumental) - LyricsViewModel + LyricsSheet (загрузка/инструментал/найдено/не найдено), прокрутка + атрибуция LRCLIB - кнопка «Текст песни» открывает встроенный экран (плеер + деталь трека чартов), вместо ссылки в браузере
This commit is contained in:
24
app/src/main/java/com/radiola/data/remote/LrcLibApi.kt
Normal file
24
app/src/main/java/com/radiola/data/remote/LrcLibApi.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.radiola.data.remote
|
||||
|
||||
import com.radiola.data.remote.dto.LrcLibLyricsDto
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface LrcLibApi {
|
||||
|
||||
@GET("api/get")
|
||||
suspend fun get(
|
||||
@Header("User-Agent") userAgent: String = "radiOLA Android (https://radiorecord.ru)",
|
||||
@Query("artist_name") artistName: String,
|
||||
@Query("track_name") trackName: String,
|
||||
@Query("duration") durationSec: Int? = null
|
||||
): LrcLibLyricsDto
|
||||
|
||||
@GET("api/search")
|
||||
suspend fun search(
|
||||
@Header("User-Agent") userAgent: String = "radiOLA Android (https://radiorecord.ru)",
|
||||
@Query("artist_name") artistName: String,
|
||||
@Query("track_name") trackName: String
|
||||
): List<LrcLibLyricsDto>
|
||||
}
|
||||
16
app/src/main/java/com/radiola/data/remote/dto/LrcLibDto.kt
Normal file
16
app/src/main/java/com/radiola/data/remote/dto/LrcLibDto.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.radiola.data.remote.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LrcLibLyricsDto(
|
||||
@SerialName("id") val id: Int? = null,
|
||||
@SerialName("trackName") val trackName: String? = null,
|
||||
@SerialName("artistName") val artistName: String? = null,
|
||||
@SerialName("albumName") val albumName: String? = null,
|
||||
@SerialName("duration") val duration: Double? = null,
|
||||
@SerialName("instrumental") val instrumental: Boolean = false,
|
||||
@SerialName("plainLyrics") val plainLyrics: String? = null,
|
||||
@SerialName("syncedLyrics") val syncedLyrics: String? = null
|
||||
)
|
||||
@@ -1,24 +1,66 @@
|
||||
package com.radiola.data.repository
|
||||
|
||||
import com.radiola.data.remote.LrcLibApi
|
||||
import com.radiola.domain.repository.LyricsRepository
|
||||
import com.radiola.domain.repository.LyricsResult
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
// Тексты песен — авторское право. Показываем ссылку на лицензированный сервис,
|
||||
// полный текст не храним/не встраиваем.
|
||||
// Для сниппета подключить официальный Musixmatch API (с атрибуцией).
|
||||
private const val USER_AGENT = "radiOLA Android (https://radiorecord.ru)"
|
||||
|
||||
@Singleton
|
||||
class LyricsRepositoryImpl @Inject constructor() : LyricsRepository {
|
||||
class LyricsRepositoryImpl @Inject constructor(
|
||||
private val api: LrcLibApi
|
||||
) : LyricsRepository {
|
||||
|
||||
override fun providerUrl(artist: String, song: String): String {
|
||||
// Musixmatch блокирует прямые переходы (connection reset). Открываем
|
||||
// веб-поиск по треку — пользователь сам выбирает сервис с текстом.
|
||||
// Сам текст не встраиваем и не храним (авторское право).
|
||||
val query = URLEncoder.encode("$artist $song текст песни", "UTF-8")
|
||||
return "https://yandex.ru/search/?text=$query"
|
||||
}
|
||||
|
||||
// TODO: подключить официальный Musixmatch API (с атрибуцией) и вернуть реальный сниппет.
|
||||
override suspend fun snippet(artist: String, song: String): String? = null
|
||||
override suspend fun fetchLyrics(
|
||||
artist: String,
|
||||
song: String,
|
||||
durationSec: Int?
|
||||
): LyricsResult? {
|
||||
val cleanArtist = artist.trim()
|
||||
val cleanSong = song.trim()
|
||||
if (cleanArtist.isEmpty() || cleanSong.isEmpty()) return null
|
||||
|
||||
return try {
|
||||
// Сначала точный запрос
|
||||
val dto = api.get(
|
||||
userAgent = USER_AGENT,
|
||||
artistName = cleanArtist,
|
||||
trackName = cleanSong,
|
||||
durationSec = durationSec
|
||||
)
|
||||
LyricsResult(
|
||||
plain = dto.plainLyrics?.takeIf { it.isNotBlank() },
|
||||
synced = dto.syncedLyrics?.takeIf { it.isNotBlank() },
|
||||
instrumental = dto.instrumental
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
// Фолбэк на поиск — берём первый результат с непустым текстом
|
||||
try {
|
||||
val results = api.search(
|
||||
userAgent = USER_AGENT,
|
||||
artistName = cleanArtist,
|
||||
trackName = cleanSong
|
||||
)
|
||||
val found = results.firstOrNull { !it.plainLyrics.isNullOrBlank() }
|
||||
?: results.firstOrNull { it.instrumental }
|
||||
found?.let {
|
||||
LyricsResult(
|
||||
plain = it.plainLyrics?.takeIf { p -> p.isNotBlank() },
|
||||
synced = it.syncedLyrics?.takeIf { s -> s.isNotBlank() },
|
||||
instrumental = it.instrumental
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user