Skip to content

added deezer integration #36

Merged
merged 1 commit into from
Apr 10, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
<application
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package group07.beatbattle.android;

import android.content.Context;
import android.media.AudioAttributes;
import android.media.MediaPlayer;

import com.badlogic.gdx.Gdx;

import group07.beatbattle.audio.AudioPlayer;

public class AndroidAudioPlayer implements AudioPlayer {

private final Context context;
private MediaPlayer mediaPlayer;
private boolean muted = false;

public AndroidAudioPlayer(Context context) {
this.context = context;
}

@Override
public void play(String url) {
stop();
if (url == null || url.isEmpty()) return;
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build());
mediaPlayer.setDataSource(url);
float vol = muted ? 0f : 1f;
mediaPlayer.setVolume(vol, vol);
mediaPlayer.setOnPreparedListener(MediaPlayer::start);
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
Gdx.app.error("AndroidAudioPlayer", "MediaPlayer error what=" + what + " extra=" + extra);
return false;
});
mediaPlayer.prepareAsync();
} catch (Exception e) {
Gdx.app.error("AndroidAudioPlayer", "Failed to play: " + url, e);
}
}

@Override
public void stop() {
if (mediaPlayer != null) {
try { mediaPlayer.stop(); } catch (IllegalStateException ignored) {}
mediaPlayer.release();
mediaPlayer = null;
}
}

@Override
public void setMuted(boolean muted) {
this.muted = muted;
if (mediaPlayer != null) {
float vol = muted ? 0f : 1f;
mediaPlayer.setVolume(vol, vol);
}
}

@Override
public void dispose() {
stop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ protected void onCreate(Bundle savedInstanceState) {
FirestoreSessionRepository sessionRepository = new FirestoreSessionRepository(firestore);
FirebaseGateway firebaseGateway = new AndroidFirebaseGateway(sessionRepository);

initialize(new BeatBattle(firebaseGateway), configuration);
BeatBattle game = new BeatBattle(firebaseGateway);
game.setServices(new DeezerMusicService(), new AndroidAudioPlayer(this));
initialize(game, configuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package group07.beatbattle.android;

import com.badlogic.gdx.Gdx;

import group07.beatbattle.model.Song;
import group07.beatbattle.service.MusicService;
import group07.beatbattle.service.MusicServiceCallback;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DeezerMusicService implements MusicService {

private static final String CHART_URL = "https://api.deezer.com/chart/0/tracks?limit=50";

@Override
public void fetchTracks(int count, MusicServiceCallback callback) {
new Thread(() -> {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(CHART_URL).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(10_000);
conn.setReadTimeout(10_000);

StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) sb.append(line);
}
conn.disconnect();

JSONObject json = new JSONObject(sb.toString());
JSONArray data = json.getJSONArray("data");

List<Song> songs = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
JSONObject track = data.getJSONObject(i);
String id = String.valueOf(track.getLong("id"));
String title = track.getString("title");
String artist = track.getJSONObject("artist").getString("name");
String preview = track.optString("preview", "");
String cover = track.getJSONObject("album").optString("cover_medium", "");
if (!preview.isEmpty()) {
songs.add(new Song(id, title, artist, preview, cover));
}
}

Collections.shuffle(songs);
List<Song> result = new ArrayList<>(songs.subList(0, Math.min(count, songs.size())));

Gdx.app.postRunnable(() -> callback.onSuccess(result));

} catch (Exception e) {
Gdx.app.postRunnable(() -> callback.onFailure(e.getMessage()));
}
}).start();
}
}
14 changes: 14 additions & 0 deletions core/src/main/java/group07/beatbattle/BeatBattle.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;

import group07.beatbattle.audio.AudioPlayer;
import group07.beatbattle.firebase.FirebaseGateway;
import group07.beatbattle.controller.LobbyController;
import group07.beatbattle.ecs.Engine;
import group07.beatbattle.service.MusicService;
import group07.beatbattle.states.StartState;
import group07.beatbattle.states.StateManager;
import group07.beatbattle.ecs.systems.AudioSystem;
Expand All @@ -28,6 +30,17 @@ public BeatBattle(FirebaseGateway firebaseGateway) {
this.firebaseGateway = firebaseGateway;
}

private MusicService musicService;
private AudioPlayer audioPlayer;

public void setServices(MusicService musicService, AudioPlayer audioPlayer) {
this.musicService = musicService;
this.audioPlayer = audioPlayer;
}

public MusicService getMusicService() { return musicService; }
public AudioPlayer getAudioPlayer() { return audioPlayer; }

@Override
public void create() {
montserratFont = loadFont("fonts/Montserrat-Regular.ttf", 54);
Expand All @@ -37,6 +50,7 @@ public void create() {
Engine engine = Engine.getInstance();
engine.addSystem(RoundSystem.getInstance());
engine.addSystem(AudioSystem.getInstance());
AudioSystem.getInstance().setAudioPlayer(audioPlayer);

LobbyController lobbyController = new LobbyController(this);
StateManager.getInstance().setState(new StartState(this, lobbyController));
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/java/group07/beatbattle/audio/AudioPlayer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package group07.beatbattle.audio;

public interface AudioPlayer {
void play(String url);
void stop();
void setMuted(boolean muted);
void dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,26 @@
import group07.beatbattle.model.GameSession;
import group07.beatbattle.model.Player;
import group07.beatbattle.model.Question;
import group07.beatbattle.model.SessionCreationResult;
import group07.beatbattle.model.Song;
import group07.beatbattle.model.services.LobbyService;
import group07.beatbattle.service.MusicServiceCallback;
import group07.beatbattle.states.InRoundState;
import group07.beatbattle.states.JoinCreateState;
import group07.beatbattle.states.LobbyState;
import group07.beatbattle.states.StartState;
import group07.beatbattle.states.StateManager;
import group07.beatbattle.view.JoinCreateView;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class LobbyController {

private static final int OPTIONS_PER_Q = 4;

private final BeatBattle game;
private int numRounds = 5; // default, overwritten when host picks rounds

public LobbyController(BeatBattle game) {
this.game = game;
Expand All @@ -42,6 +47,8 @@ public void onReady(
int totalRounds,
JoinCreateView view
) {
numRounds = totalRounds;

if (mode != GameMode.CREATE) {
StateManager.getInstance().setState(new LobbyState(game, mode, this));
return;
Expand All @@ -58,7 +65,7 @@ public void onReady(
totalRounds,
new LobbyService.CreateSessionCallback() {
@Override
public void onSuccess(SessionCreationResult result) {
public void onSuccess(group07.beatbattle.model.SessionCreationResult result) {
Gdx.app.postRunnable(() -> {
view.setLoadingState(false);
view.setStatusMessage("");
Expand Down Expand Up @@ -99,36 +106,58 @@ public void onBackFromLobby(GameMode mode) {
}

public void onStartGame() {
// TODO: replace mock session with real data from Firebase/Deezer
GameSession session = new GameSession("123456", "host1", 2);

session.addPlayer(new Player("p1", "You"));
session.addPlayer(new Player("p2", "Oda"));
session.addPlayer(new Player("p3", "Lea"));
session.addPlayer(new Player("p4", "Milos"));
session.addPlayer(new Player("p5", "Emma"));
session.addPlayer(new Player("p6", "Lucas"));
session.addPlayer(new Player("p7", "Sofia"));
session.addPlayer(new Player("p8", "Noah"));
session.addPlayer(new Player("p9", "Mia"));
int tracksNeeded = numRounds + (OPTIONS_PER_Q - 1) * numRounds;
game.getMusicService().fetchTracks(tracksNeeded, new MusicServiceCallback() {
@Override
public void onSuccess(List<Song> songs) {
buildAndStartSession(songs);
}

@Override
public void onFailure(String error) {
Gdx.app.error("LobbyController", "Deezer fetch failed: " + error);
}
});
}

private void buildAndStartSession(List<Song> songs) {
GameSession session = new GameSession("123456", "host1", numRounds);

session.addPlayer(new Player("p1", "You"));
session.addPlayer(new Player("p2", "Oda"));
session.addPlayer(new Player("p3", "Lea"));
session.addPlayer(new Player("p4", "Milos"));
session.addPlayer(new Player("p5", "Emma"));
session.addPlayer(new Player("p6", "Lucas"));
session.addPlayer(new Player("p7", "Sofia"));
session.addPlayer(new Player("p8", "Noah"));
session.addPlayer(new Player("p9", "Mia"));
session.addPlayer(new Player("p10", "Elias"));
session.addPlayer(new Player("p11", "Nora"));

Song song1 = new Song("1", "Smells Like Teen Spirit", "Nirvana", "", "");
session.addQuestion(new Question("q1", song1, Arrays.asList(
"Smells Like Teen Spirit - Nirvana",
"About a Girl - Nirvana",
"Come as You Are - Nirvana",
"Lithium - Nirvana"
), 0));

Song song2 = new Song("2", "Billie Jean", "Michael Jackson", "", "");
session.addQuestion(new Question("q2", song2, Arrays.asList(
"Billie Jean - Michael Jackson",
"Thriller - Michael Jackson",
"Beat It - Michael Jackson",
"Black or White - Michael Jackson"
), 1));
// Songs 0..numRounds-1 are correct answers; the rest are the decoy pool
List<Song> decoyPool = new ArrayList<>(songs.subList(numRounds, songs.size()));

for (int q = 0; q < numRounds && q < songs.size(); q++) {
Song correct = songs.get(q);

List<String> options = new ArrayList<>();
options.add(correct.getTitle() + " - " + correct.getArtist());

int decoysAdded = 0;
for (Song decoy : decoyPool) {
if (decoysAdded >= OPTIONS_PER_Q - 1) break;
String label = decoy.getTitle() + " - " + decoy.getArtist();
if (!options.contains(label)) {
options.add(label);
decoysAdded++;
}
}
if (!decoyPool.isEmpty()) Collections.rotate(decoyPool, OPTIONS_PER_Q - 1);

Collections.shuffle(options);
session.addQuestion(new Question("q" + (q + 1), correct, options, q));
}

RoundController roundController = new RoundController(game, session.getCurrentQuestion(), session);
StateManager.getInstance().setState(new InRoundState(game, roundController));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import group07.beatbattle.ecs.components.TimerComponent;
import group07.beatbattle.ecs.entities.Entity;
import group07.beatbattle.ecs.entities.RoundFactory;
import group07.beatbattle.ecs.systems.AudioSystem;
import group07.beatbattle.model.GameSession;
import group07.beatbattle.model.Leaderboard;
import group07.beatbattle.model.Player;
Expand Down Expand Up @@ -34,6 +35,7 @@ public RoundController(BeatBattle game, Question question, GameSession session)
question.getSong().getPreviewUrl()
);
Engine.getInstance().addEntity(roundEntity);
AudioSystem.getInstance().play(roundEntity);
}

public Question getQuestion() {
Expand Down Expand Up @@ -75,6 +77,7 @@ public void onRoundExpired() {
}

public void onLeaveSession() {
AudioSystem.getInstance().stop(roundEntity);
Engine.getInstance().removeEntity(roundEntity);
StateManager.getInstance().setState(new StartState(game, new LobbyController(game)));
}
Expand All @@ -93,6 +96,7 @@ private void simulateMockPlayers() {

private void transitionToLeaderboard() {
simulateMockPlayers();
AudioSystem.getInstance().stop(roundEntity);
Engine.getInstance().removeEntity(roundEntity);
Leaderboard leaderboard = new Leaderboard(session.getPlayers());
LeaderboardController leaderboardController = new LeaderboardController(game, session, leaderboard);
Expand Down
Loading
Loading