feat(app): кнопка «Распознать трек» (Shazam) + история распознанных
- кнопка распознавания в плеере: видна только на музыкальных станциях без
метаданных эфира (track == null), показывает спиннер и результат через Toast
- распознанный трек отображается в плеере и пишется в ОТДЕЛЬНУЮ историю
распознанных (не дублируется в историю эфирных треков — гейт по ключу)
- экран Истории: переключатель «Треки эфира | Распознанные», два списка
- Room: таблица recognized_track (миграция 7→8), DAO/репозиторий
- ShazamRepository → POST /shazam/recognize/{stationId}, маппинг 503/400 в текст
- MusicGenres.isMusicStation — клиентский гейт (синхронизирован с бэкендом)
- bump backend submodule (модуль shazam)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,13 @@ import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.radiola.data.local.dao.AlarmDao
|
||||
import com.radiola.data.local.dao.RecognizedTrackDao
|
||||
import com.radiola.data.local.dao.RecordingDao
|
||||
import com.radiola.data.local.dao.StationDao
|
||||
import com.radiola.data.local.dao.TagDao
|
||||
import com.radiola.data.local.dao.TrackHistoryDao
|
||||
import com.radiola.data.local.entity.AlarmEntity
|
||||
import com.radiola.data.local.entity.RecognizedTrackEntity
|
||||
import com.radiola.data.local.entity.RecordingEntity
|
||||
import com.radiola.data.local.entity.StationEntity
|
||||
import com.radiola.data.local.entity.TagEntity
|
||||
@@ -78,9 +80,27 @@ val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем таблицу истории распознанных треков (Shazam)
|
||||
val MIGRATION_7_8 = object : Migration(7, 8) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS recognized_track (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
artist TEXT NOT NULL,
|
||||
song TEXT NOT NULL,
|
||||
stationName TEXT NOT NULL,
|
||||
coverUrl TEXT,
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Database(
|
||||
entities = [StationEntity::class, TrackHistoryEntity::class, TagEntity::class, RecordingEntity::class, AlarmEntity::class],
|
||||
version = 7
|
||||
entities = [StationEntity::class, TrackHistoryEntity::class, TagEntity::class, RecordingEntity::class, AlarmEntity::class, RecognizedTrackEntity::class],
|
||||
version = 8
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun stationDao(): StationDao
|
||||
@@ -88,4 +108,5 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun tagDao(): TagDao
|
||||
abstract fun recordingDao(): RecordingDao
|
||||
abstract fun alarmDao(): AlarmDao
|
||||
abstract fun recognizedTrackDao(): RecognizedTrackDao
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.radiola.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.radiola.data.local.entity.RecognizedTrackEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RecognizedTrackDao {
|
||||
|
||||
@Query("SELECT * FROM recognized_track ORDER BY timestamp DESC LIMIT 200")
|
||||
fun getAll(): Flow<List<RecognizedTrackEntity>>
|
||||
|
||||
@Insert
|
||||
suspend fun insert(track: RecognizedTrackEntity)
|
||||
|
||||
@Query("DELETE FROM recognized_track WHERE id NOT IN (SELECT id FROM recognized_track ORDER BY timestamp DESC LIMIT 200)")
|
||||
suspend fun cleanupOld()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.radiola.data.local.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recognized_track")
|
||||
data class RecognizedTrackEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
val artist: String,
|
||||
val song: String,
|
||||
val stationName: String,
|
||||
val coverUrl: String?,
|
||||
val timestamp: Long
|
||||
)
|
||||
@@ -8,6 +8,7 @@ import com.radiola.data.remote.dto.GenresResponseDto
|
||||
import com.radiola.data.remote.dto.HistoryResponseDto
|
||||
import com.radiola.data.remote.dto.MagicLinkRequestDto
|
||||
import com.radiola.data.remote.dto.MagicLinkVerifyDto
|
||||
import com.radiola.data.remote.dto.RecognizeResponseDto
|
||||
import com.radiola.data.remote.dto.TrackStatsDto
|
||||
import com.radiola.data.remote.dto.UserSettingsDto
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -30,6 +31,10 @@ interface RadiolaApi {
|
||||
@GET("now-playing")
|
||||
suspend fun getNowPlaying(): List<BackendNowPlayingDto>
|
||||
|
||||
// Распознавание играющего трека через Shazam (бэкенд сам тянет аудио из потока).
|
||||
@POST("shazam/recognize/{stationId}")
|
||||
suspend fun recognizeTrack(@Path("stationId") stationId: Int): RecognizeResponseDto
|
||||
|
||||
// Сабмит обложки, найденной клиентом в iTunes (см. CoverEnrichmentManager).
|
||||
@POST("covers/submit")
|
||||
suspend fun submitCover(@Body dto: com.radiola.data.remote.dto.SubmitCoverDto): com.radiola.data.remote.dto.SubmitCoverResponse
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.radiola.data.remote.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RecognizeResponseDto(
|
||||
val matched: Boolean,
|
||||
val artist: String? = null,
|
||||
val song: String? = null,
|
||||
val coverUrl: String? = null,
|
||||
val album: String? = null
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.radiola.data.repository
|
||||
|
||||
import com.radiola.data.local.AppDatabase
|
||||
import com.radiola.data.local.entity.RecognizedTrackEntity
|
||||
import com.radiola.domain.model.Track
|
||||
import com.radiola.domain.repository.RecognizedTrackRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecognizedTrackRepositoryImpl @Inject constructor(
|
||||
private val db: AppDatabase
|
||||
) : RecognizedTrackRepository {
|
||||
|
||||
override fun getHistory(): Flow<List<Track>> =
|
||||
db.recognizedTrackDao().getAll().map { list -> list.map { it.toDomain() } }
|
||||
|
||||
override suspend fun addTrack(track: Track) {
|
||||
db.recognizedTrackDao().insert(
|
||||
RecognizedTrackEntity(
|
||||
artist = track.artist,
|
||||
song = track.song,
|
||||
stationName = track.stationName,
|
||||
coverUrl = track.coverUrl,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
db.recognizedTrackDao().cleanupOld()
|
||||
}
|
||||
|
||||
private fun RecognizedTrackEntity.toDomain(): Track = Track(
|
||||
artist = artist, song = song, coverUrl = coverUrl, stationName = stationName
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.radiola.data.repository
|
||||
|
||||
import com.radiola.data.remote.RadiolaApi
|
||||
import com.radiola.domain.model.Track
|
||||
import com.radiola.domain.repository.RecognizeResult
|
||||
import com.radiola.domain.repository.ShazamRepository
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
class ShazamRepositoryImpl @Inject constructor(
|
||||
private val api: RadiolaApi
|
||||
) : ShazamRepository {
|
||||
|
||||
override suspend fun recognize(stationId: Int, stationName: String): RecognizeResult {
|
||||
return try {
|
||||
val res = api.recognizeTrack(stationId)
|
||||
if (res.matched && !res.artist.isNullOrBlank() && !res.song.isNullOrBlank()) {
|
||||
RecognizeResult.Found(
|
||||
Track(
|
||||
artist = res.artist,
|
||||
song = res.song,
|
||||
coverUrl = res.coverUrl,
|
||||
stationName = stationName
|
||||
)
|
||||
)
|
||||
} else {
|
||||
RecognizeResult.NotFound
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
val msg = when (e.code()) {
|
||||
503 -> "Распознавание временно недоступно"
|
||||
400 -> "На этой станции нет музыки"
|
||||
else -> "Не удалось распознать трек"
|
||||
}
|
||||
RecognizeResult.Error(msg)
|
||||
} catch (e: Exception) {
|
||||
RecognizeResult.Error("Нет связи с сервером")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user