Skip to content

Commit

Permalink
Merge pull request #36 from milospi/feature/deezer-integration-final
Browse files Browse the repository at this point in the history
Added deezer integration
  • Loading branch information
milospi authored Apr 10, 2026
2 parents b8ac607 + e4936f0 commit cc7d9c7
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 44 deletions.
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

0 comments on commit cc7d9c7

Please sign in to comment.