feat: Fetch tracks, update interface and music

This commit is contained in:
Lucàs
2025-01-05 17:57:06 +01:00
parent 1edd76f618
commit 4c4e06b64b
9 changed files with 185 additions and 135 deletions
@@ -1,5 +0,0 @@
package fr.univpau.queezer.data
data class Album(
val cover: String
)
@@ -1,5 +0,0 @@
package fr.univpau.queezer.data
data class Artist(
val name: String
)
@@ -1,8 +1,19 @@
package fr.univpau.queezer.data package fr.univpau.queezer.data
data class Track( data class Track(
val title: String, val preview: String, // Music preview URL
val artist: Artist, val album: String, // Album picture URL
val preview: String, val title: Input, // Title of the track
val album: Album val artist: Input, // Artist of the track
) )
data class Input(
val value: String,
val answer: Answer = Answer.UNKNOWN
)
enum class Answer {
CORRECT,
INCORRECT,
UNKNOWN
}
@@ -1,31 +0,0 @@
package fr.univpau.queezer.manager
import android.media.MediaPlayer
class AudioManager {
private var mediaPlayer: MediaPlayer = MediaPlayer()
fun play(url: String) {
mediaPlayer.apply {
setDataSource(url)
prepare()
start()
}
}
fun stop() {
mediaPlayer.release()
}
fun pause() {
mediaPlayer.pause()
}
fun resume() {
mediaPlayer.start()
}
fun isPlaying(): Boolean {
return mediaPlayer.isPlaying
}
}
@@ -4,7 +4,7 @@ import android.os.CountDownTimer
class CountdownManager (val duration: Long, val onFinish: () -> Unit) { class CountdownManager (val duration: Long, val onFinish: () -> Unit) {
var timeLeft = duration; var timeLeft = duration / 1000;
var interval = 1000L; var interval = 1000L;
var timer: CountDownTimer? = null var timer: CountDownTimer? = null
@@ -1,71 +1,59 @@
package fr.univpau.queezer.manager package fr.univpau.queezer.manager
import com.google.gson.Gson import android.media.MediaPlayer
import com.google.gson.JsonObject import android.util.Log
import com.google.gson.reflect.TypeToken
import fr.univpau.queezer.data.Settings import fr.univpau.queezer.data.Settings
import fr.univpau.queezer.data.Track import fr.univpau.queezer.data.Track
import java.net.HttpURLConnection
import java.net.URL
class GameManager(var settings: Settings) { class GameManager(var settings: Settings, val tracks: List<Track>) {
val audioManager = AudioManager() private var mediaPlayer: MediaPlayer = MediaPlayer()
val countDownManager = CountdownManager(30000L, onFinish = ::nextTrack) val countDownManager = CountdownManager(30000L, onFinish = ::nextTrack)
var tracks: List<Track> = mutableListOf()
var currentTrackIndex: Int = 0; var currentTrackIndex: Int = 0;
var score = 0 var score = 0
suspend fun loadTracks() { fun getCurrentTrack(): Track? {
val url = URL(settings.playlistUrl) if (tracks.isEmpty()) return null;
val connection = url.openConnection() as HttpURLConnection return tracks[currentTrackIndex]
connection.requestMethod = "GET"
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Failed to load tracks")
} }
val response = connection.inputStream.bufferedReader().use { it.readText() } fun stop() {
val json = Gson().fromJson(response, JsonObject::class.java) mediaPlayer.release()
val tracksJson = json["tracks"].asJsonObject["data"].toString() // countDownManager.stop()
// Assurez-vous d'utiliser un TypeToken explicite pour une liste de Track
tracks = Gson().fromJson(tracksJson, object : TypeToken<List<Track>>() {}.type)
} }
fun nextTrack() { fun nextTrack() {
// Stop the current track mediaPlayer.release()
audioManager.stop()
// Play the next track
if (currentTrackIndex >= tracks.size - 1) { if (currentTrackIndex >= tracks.size - 1) {
return return; // TODO vérifier avant meme de lancer la partie si le nombre de titres est suffisant
}
if (currentTrackIndex >= settings.numberOfTitles!! - 1) {
return;
} }
currentTrackIndex++ currentTrackIndex++
audioManager.play(getCurrentTrack().preview)
// Restart the countdown mediaPlayer = MediaPlayer().apply {
countDownManager.restart() setDataSource(tracks[currentTrackIndex].preview)
prepare()
start()
} }
fun getCurrentTrack(): Track { // countDownManager.restart()
if (tracks.isEmpty()) {
throw IllegalStateException("La liste des pistes est vide")
}
return tracks[currentTrackIndex]
} }
fun start() { fun start() {
if (tracks.isEmpty()) { // countDownManager.start()
throw IllegalStateException("Aucune piste n'a été trouvée dans la playlist") Log.i("GameManager", "Next track: ${tracks[currentTrackIndex].preview}")
mediaPlayer = MediaPlayer().apply {
setDataSource(tracks[currentTrackIndex].preview)
prepare()
start()
} }
countDownManager.start()
audioManager.play(tracks[currentTrackIndex].preview)
} }
} }
@@ -0,0 +1,31 @@
package fr.univpau.queezer.manager
import fr.univpau.queezer.data.Input
import fr.univpau.queezer.data.Track
import fr.univpau.queezer.service.createDeezerApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun fetchAndFormatPlaylist(apiUrl: String): List<Track> {
val deezerApiService = createDeezerApiService()
return withContext(Dispatchers.IO) {
try {
// Récupérer la réponse de la playlist
val playlist = deezerApiService.getPlaylist(apiUrl)
// Transformer les données en liste de `Track`
playlist.tracks.data.map { track ->
Track(
preview = track.preview,
album = track.album.cover,
title = Input(value = track.title),
artist = Input(value = track.artist.name)
)
}
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
}
}
@@ -1,10 +1,9 @@
package fr.univpau.queezer.screen package fr.univpau.queezer.screen
import androidx.compose.foundation.Image import android.util.Log
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -12,14 +11,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -30,18 +33,25 @@ import fr.univpau.queezer.R
import fr.univpau.queezer.data.Settings import fr.univpau.queezer.data.Settings
import fr.univpau.queezer.manager.loadSettings import fr.univpau.queezer.manager.loadSettings
import coil.compose.AsyncImage import coil.compose.AsyncImage
import fr.univpau.queezer.manager.GameManager
import fr.univpau.queezer.manager.fetchAndFormatPlaylist
@Composable @Composable
fun GameScreen(navController: NavHostController) { fun GameScreen(navController: NavHostController) {
val context = LocalContext.current val context = LocalContext.current
val settings: Settings = loadSettings(context) val settings: Settings = loadSettings(context)
// val gameManager: GameManager = remember { GameManager(settings) } var gameManager by remember { mutableStateOf(GameManager(settings, emptyList())) }
var currentTrack by remember { mutableStateOf(gameManager.getCurrentTrack()) }
// LaunchedEffect(gameManager) { LaunchedEffect(settings.playlistUrl) {
// gameManager.loadTracks() val tracks = fetchAndFormatPlaylist(settings.playlistUrl).shuffled()
// gameManager.start() gameManager = GameManager(settings, tracks)
// }
currentTrack = gameManager.getCurrentTrack()
gameManager.start()
Log.i("GameScreen", gameManager.getCurrentTrack().toString())
}
// État de l'utilisateur et des éléments du jeu // État de l'utilisateur et des éléments du jeu
val userInput = remember { mutableStateOf("") } val userInput = remember { mutableStateOf("") }
@@ -54,25 +64,27 @@ fun GameScreen(navController: NavHostController) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
if (currentTrack == null) {
CircularProgressIndicator(
modifier = Modifier.width(64.dp),
color = MaterialTheme.colorScheme.secondary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
} else {
Text("Score : ${gameManager.score}", fontSize = 24.sp)
Text("Temps restant : ${gameManager.countDownManager.timeLeft}sec", fontSize = 20.sp)
Text("Score : 0", fontSize = 24.sp)
Text("Temps restant : 30sec", fontSize = 20.sp)
// Affiche une image a partir d'une url
AsyncImage( AsyncImage(
model = "https://api.deezer.com/album/382921287/image", model = currentTrack!!.album,
contentDescription = "Image from URL", contentDescription = "Image from URL",
modifier = Modifier modifier = Modifier
.width(200.dp) .width(200.dp)
.height(200.dp) .height(200.dp),
.blur(30.dp)
,
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Text("Titre : Légende Vivante", fontSize = 20.sp) Text("Titre : ${currentTrack!!.title.value}", fontSize = 20.sp)
Text("Artiste : Lorenzo", fontSize = 20.sp) Text("Artiste : ${currentTrack!!.artist.value}", fontSize = 20.sp)
// Champ de texte pour entrer la proposition // Champ de texte pour entrer la proposition
TextField( TextField(
@@ -94,11 +106,17 @@ fun GameScreen(navController: NavHostController) {
Button( Button(
onClick = { onClick = {
userInput.value = "" // Réinitialiser le champ de texte userInput.value = "" // Réinitialiser le champ de texte
gameManager.nextTrack()
currentTrack = gameManager.getCurrentTrack()
}, },
) { Text(context.resources.getString(R.string.skip)) } ) { Text(context.resources.getString(R.string.skip)) }
Button(onClick = { navController.popBackStack() }) Button(onClick = {
gameManager.stop()
navController.popBackStack()
})
{ Text(context.resources.getString(R.string.give_up)) } { Text(context.resources.getString(R.string.give_up)) }
} }
} }
}
} }
@@ -0,0 +1,43 @@
package fr.univpau.queezer.service
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Url
data class PlaylistResponse(
val tracks: TrackList
)
data class TrackList(
val data: List<ApiTrack>
)
data class ApiTrack(
val preview: String, // URL d'aperçu de la musique
val album: ApiAlbum, // Informations sur l'album
val title: String, // Titre de la piste
val artist: Artist // Informations sur l'artiste
)
data class ApiAlbum(
val cover: String // URL de la couverture de l'album
)
data class Artist(
val name: String // Nom de l'artiste
)
interface DeezerApiService {
@GET
suspend fun getPlaylist(@Url url: String): PlaylistResponse
}
fun createDeezerApiService(): DeezerApiService {
return Retrofit.Builder()
.baseUrl("https://api.deezer.com/") // Base URL par défaut
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(DeezerApiService::class.java)
}