diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml
index 1d94de0b..6c7183f5 100755
--- a/airsonic-main/pom.xml
+++ b/airsonic-main/pom.xml
@@ -87,6 +87,16 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
org.springframework.security
spring-security-ldap
diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java
index 73ab02aa..ee193430 100644
--- a/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java
+++ b/airsonic-main/src/main/java/org/airsonic/player/controller/SubsonicRESTController.java
@@ -1274,7 +1274,7 @@ public class SubsonicRESTController {
child.setSuffix(suffix);
child.setContentType(StringUtil.getMimeType(suffix));
child.setIsVideo(mediaFile.isVideo());
- child.setPath(getRelativePath(mediaFile));
+ child.setPath(getRelativePath(mediaFile, settingsService));
org.airsonic.player.domain.Bookmark bookmark = bookmarkCache.get(new BookmarkKey(username, mediaFile.getId()));
if (bookmark != null) {
@@ -1329,7 +1329,7 @@ public class SubsonicRESTController {
return null;
}
- private String getRelativePath(MediaFile musicFile) {
+ public static String getRelativePath(MediaFile musicFile, SettingsService settingsService) {
String filePath = musicFile.getPath();
diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java
index 17ad5943..2b59e321 100644
--- a/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java
+++ b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDao.java
@@ -22,6 +22,7 @@ package org.airsonic.player.dao;
import org.airsonic.player.domain.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@@ -43,6 +44,9 @@ public class PlayerDao extends AbstractDao {
"last_seen, cover_art_scheme, transcode_scheme, dynamic_ip, technology, client_id, mixer";
private static final String QUERY_COLUMNS = "id, " + INSERT_COLUMNS;
+ @Autowired
+ private PlayerDaoPlayQueueFactory playerDaoPlayQueueFactory;
+
private PlayerRowMapper rowMapper = new PlayerRowMapper();
private Map playlists = Collections.synchronizedMap(new HashMap());
@@ -166,7 +170,7 @@ public class PlayerDao extends AbstractDao {
private void addPlaylist(Player player) {
PlayQueue playQueue = playlists.get(player.getId());
if (playQueue == null) {
- playQueue = new PlayQueue();
+ playQueue = playerDaoPlayQueueFactory.createPlayQueue();
playlists.put(player.getId(), playQueue);
}
player.setPlayQueue(playQueue);
diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java
new file mode 100644
index 00000000..ceb65087
--- /dev/null
+++ b/airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java
@@ -0,0 +1,12 @@
+package org.airsonic.player.dao;
+
+import org.airsonic.player.domain.PlayQueue;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PlayerDaoPlayQueueFactory {
+
+ public PlayQueue createPlayQueue() {
+ return new PlayQueue();
+ }
+}
diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java
index 59668a0a..908dc43c 100644
--- a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java
+++ b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxJavaService.java
@@ -1,9 +1,9 @@
package org.airsonic.player.service;
-import com.github.biconou.AudioPlayer.JavaPlayer;
-import com.github.biconou.AudioPlayer.api.*;
+import com.github.biconou.AudioPlayer.api.PlayList;
+import com.github.biconou.AudioPlayer.api.PlayerListener;
import org.airsonic.player.domain.*;
-import org.airsonic.player.domain.Player;
+import org.airsonic.player.service.jukebox.JavaPlayerFactory;
import org.airsonic.player.util.FileUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
@@ -19,23 +19,25 @@ import java.util.Map;
/**
- * @author R??mi Cocula
+ * @author Rémi Cocula
*/
@Service
public class JukeboxJavaService {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(JukeboxJavaService.class);
+ private static final float DEFAULT_GAIN = 0.75f;
+
@Autowired
private AudioScrobblerService audioScrobblerService;
@Autowired
private StatusService statusService;
@Autowired
- private SettingsService settingsService;
- @Autowired
private SecurityService securityService;
@Autowired
private MediaFileService mediaFileService;
+ @Autowired
+ private JavaPlayerFactory javaPlayerFactory;
private TransferStatus status;
@@ -45,31 +47,32 @@ public class JukeboxJavaService {
/**
* Finds the corresponding active audio player for a given airsonic player.
+ * If no player exists we create one.
* The JukeboxJavaService references all active audio players in a map indexed by airsonic player id.
*
* @param airsonicPlayer a given airsonic player.
- * @return the corresponding active audio player of null if none exists.
+ * @return the corresponding active audio player.
*/
private com.github.biconou.AudioPlayer.api.Player retrieveAudioPlayerForAirsonicPlayer(Player airsonicPlayer) {
com.github.biconou.AudioPlayer.api.Player foundPlayer = activeAudioPlayers.get(airsonicPlayer.getId());
if (foundPlayer == null) {
synchronized (activeAudioPlayers) {
- foundPlayer = initAudioPlayer(airsonicPlayer);
- if (foundPlayer == null) {
+ com.github.biconou.AudioPlayer.api.Player newPlayer = initAudioPlayer(airsonicPlayer);
+ if (newPlayer == null) {
throw new RuntimeException("Did not initialized a player");
- } else {
- activeAudioPlayers.put(airsonicPlayer.getId(), foundPlayer);
- String mixer = airsonicPlayer.getJavaJukeboxMixer();
- if (StringUtils.isBlank(mixer)) {
- mixer = DEFAULT_MIXER_ENTRY_KEY;
- }
- List playersForMixer = activeAudioPlayersPerMixer.get(mixer);
- if (playersForMixer == null) {
- playersForMixer = new ArrayList<>();
- activeAudioPlayersPerMixer.put(mixer, playersForMixer);
- }
- playersForMixer.add(foundPlayer);
}
+ activeAudioPlayers.put(airsonicPlayer.getId(), newPlayer);
+ String mixer = airsonicPlayer.getJavaJukeboxMixer();
+ if (StringUtils.isBlank(mixer)) {
+ mixer = DEFAULT_MIXER_ENTRY_KEY;
+ }
+ List playersForMixer = activeAudioPlayersPerMixer.get(mixer);
+ if (playersForMixer == null) {
+ playersForMixer = new ArrayList<>();
+ activeAudioPlayersPerMixer.put(mixer, playersForMixer);
+ }
+ playersForMixer.add(newPlayer);
+ foundPlayer = newPlayer;
}
}
return foundPlayer;
@@ -88,11 +91,12 @@ public class JukeboxJavaService {
if (StringUtils.isNotBlank(airsonicPlayer.getJavaJukeboxMixer())) {
log.info("use mixer : {}", airsonicPlayer.getJavaJukeboxMixer());
- audioPlayer = new JavaPlayer(airsonicPlayer.getJavaJukeboxMixer());
+ audioPlayer = javaPlayerFactory.createJavaPlayer(airsonicPlayer.getJavaJukeboxMixer());
} else {
log.info("use default mixer");
- audioPlayer = new JavaPlayer();
+ audioPlayer = javaPlayerFactory.createJavaPlayer();
}
+ audioPlayer.setGain(DEFAULT_GAIN);
if (audioPlayer != null) {
audioPlayer.registerListener(new PlayerListener() {
@Override
@@ -159,10 +163,7 @@ public class JukeboxJavaService {
throw new RuntimeException("The player " + airsonicPlayer.getName() + " is not a java jukebox player");
}
com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
- if (audioPlayer != null) {
- return audioPlayer.getGain();
- }
- return 0.5f;
+ return audioPlayer.getGain();
}
public void setGain(final Player airsonicPlayer, final float gain) {
@@ -271,20 +272,8 @@ public class JukeboxJavaService {
}
}
- public void start(Player airsonicPlayer) throws Exception {
- log.debug("begin start jukebox : player = id:{};name:{}", airsonicPlayer.getId(), airsonicPlayer.getName());
-
- com.github.biconou.AudioPlayer.api.Player audioPlayer = retrieveAudioPlayerForAirsonicPlayer(airsonicPlayer);
-
- // Control user authorizations
- User user = securityService.getUserByName(airsonicPlayer.getUsername());
- if (!user.isJukeboxRole()) {
- log.warn("{} is not authorized for jukebox playback.", user.getUsername());
- return;
- }
-
- log.debug("PlayQueue.Status is {}", airsonicPlayer.getPlayQueue().getStatus());
- audioPlayer.play();
+ public void start(Player airsonicPlayer) {
+ play(airsonicPlayer);
}
public void stop(Player airsonicPlayer) throws Exception {
@@ -332,7 +321,6 @@ public class JukeboxJavaService {
}
}
-
public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
this.audioScrobblerService = audioScrobblerService;
}
@@ -342,7 +330,6 @@ public class JukeboxJavaService {
}
public void setSettingsService(SettingsService settingsService) {
- this.settingsService = settingsService;
}
public void setSecurityService(SecurityService securityService) {
diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java
index 4205bf43..939d07c5 100644
--- a/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java
+++ b/airsonic-main/src/main/java/org/airsonic/player/service/JukeboxLegacySubsonicService.java
@@ -21,6 +21,7 @@ package org.airsonic.player.service;
import org.airsonic.player.domain.*;
import org.airsonic.player.service.jukebox.AudioPlayer;
+import org.airsonic.player.service.jukebox.AudioPlayerFactory;
import org.airsonic.player.util.FileUtil;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
@@ -52,6 +53,8 @@ public class JukeboxLegacySubsonicService implements AudioPlayer.Listener {
private SecurityService securityService;
@Autowired
private MediaFileService mediaFileService;
+ @Autowired
+ private AudioPlayerFactory audioPlayerFactory;
private AudioPlayer audioPlayer;
private Player player;
@@ -111,7 +114,7 @@ public class JukeboxLegacySubsonicService implements AudioPlayer.Listener {
String command = settingsService.getJukeboxCommand();
parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false));
in = transcodingService.getTranscodedInputStream(parameters);
- audioPlayer = new AudioPlayer(in, this);
+ audioPlayer = audioPlayerFactory.createAudioPlayer(in, this);
audioPlayer.setGain(gain);
audioPlayer.play();
onSongStart(file);
diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java b/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java
index 21499ee8..9176b281 100644
--- a/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java
+++ b/airsonic-main/src/main/java/org/airsonic/player/service/TranscodingService.java
@@ -32,6 +32,7 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.io.File;
@@ -61,6 +62,7 @@ public class TranscodingService {
@Autowired
private SettingsService settingsService;
@Autowired
+ @Lazy // used to deal with circular dependencies between PlayerService and TranscodingService
private PlayerService playerService;
/**
diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java
new file mode 100644
index 00000000..a7253011
--- /dev/null
+++ b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java
@@ -0,0 +1,14 @@
+package org.airsonic.player.service.jukebox;
+
+import org.airsonic.player.service.JukeboxLegacySubsonicService;
+import org.springframework.stereotype.Component;
+
+import java.io.InputStream;
+
+@Component
+public class AudioPlayerFactory {
+
+ public AudioPlayer createAudioPlayer(InputStream in, JukeboxLegacySubsonicService jukeboxLegacySubsonicService) throws Exception {
+ return new AudioPlayer(in, jukeboxLegacySubsonicService);
+ }
+}
diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java
new file mode 100644
index 00000000..7ed6b2ed
--- /dev/null
+++ b/airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java
@@ -0,0 +1,17 @@
+package org.airsonic.player.service.jukebox;
+
+import com.github.biconou.AudioPlayer.JavaPlayer;
+import com.github.biconou.AudioPlayer.api.Player;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JavaPlayerFactory {
+
+ public Player createJavaPlayer() {
+ return new JavaPlayer();
+ }
+
+ public Player createJavaPlayer(String mixerName) {
+ return new JavaPlayer(mixerName);
+ }
+}
diff --git a/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java b/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java
index 12b102a0..50c8b5d0 100644
--- a/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java
+++ b/airsonic-main/src/test/java/org/airsonic/player/TestCaseUtils.java
@@ -1,5 +1,6 @@
package org.airsonic.player;
+import org.airsonic.player.controller.JAXBWriter;
import org.airsonic.player.dao.DaoHelper;
import org.airsonic.player.service.MediaScannerService;
import org.apache.commons.io.FileUtils;
@@ -37,6 +38,13 @@ public class TestCaseUtils {
return airsonicHomeDirForTest.getAbsolutePath();
}
+ /**
+ *
+ * @return current REST api version.
+ */
+ public static String restApiVersion() {
+ return new JAXBWriter().getRestProtocolVersion();
+ }
/**
* Cleans the AIRSONIC_HOME directory used for tests.
@@ -106,6 +114,7 @@ public class TestCaseUtils {
* Scans the music library * @param mediaScannerService
*/
public static void execScan(MediaScannerService mediaScannerService) {
+ // TODO create a synchronous scan
mediaScannerService.scanLibrary();
while (mediaScannerService.isScanning()) {
diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java
new file mode 100644
index 00000000..0dce645d
--- /dev/null
+++ b/airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java
@@ -0,0 +1,59 @@
+package org.airsonic.player.api;
+
+import org.airsonic.player.TestCaseUtils;
+import org.airsonic.player.util.HomeRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+public class AirsonicRestApiIntTest {
+
+ public static final String CLIENT_NAME = "airsonic";
+ public static final String AIRSONIC_USER = "admin";
+ public static final String AIRSONIC_PASSWORD = "admin";
+ public static final String EXPECTED_FORMAT = "json";
+
+ private static String AIRSONIC_API_VERSION;
+
+ @Autowired
+ private MockMvc mvc;
+
+ @ClassRule
+ public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir
+
+ @BeforeClass
+ public static void setupClass() {
+ AIRSONIC_API_VERSION = TestCaseUtils.restApiVersion();
+ }
+
+ @Test
+ public void pingTest() throws Exception {
+ mvc.perform(get("/rest/ping")
+ .param("v", AIRSONIC_API_VERSION)
+ .param("c", CLIENT_NAME)
+ .param("u", AIRSONIC_USER)
+ .param("p", AIRSONIC_PASSWORD)
+ .param("f", EXPECTED_FORMAT)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
+ .andExpect(jsonPath("$.subsonic-response.version").value(AIRSONIC_API_VERSION))
+ .andDo(print());
+ }
+}
diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java
new file mode 100644
index 00000000..0195fedd
--- /dev/null
+++ b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java
@@ -0,0 +1,279 @@
+package org.airsonic.player.api.jukebox;
+
+import org.airsonic.player.TestCaseUtils;
+import org.airsonic.player.dao.*;
+import org.airsonic.player.domain.*;
+import org.airsonic.player.service.MediaScannerService;
+import org.airsonic.player.service.PlayerService;
+import org.airsonic.player.service.SettingsService;
+import org.airsonic.player.util.HomeRule;
+import org.junit.*;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+public abstract class AbstractAirsonicRestApiJukeboxIntTest {
+
+ @ClassRule
+ public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir
+
+ @TestConfiguration
+ static class Config {
+ private static class SpiedPlayerDaoPlayQueueFactory extends PlayerDaoPlayQueueFactory {
+ @Override
+ public PlayQueue createPlayQueue() {
+ return spy(super.createPlayQueue());
+ }
+ }
+
+ @Bean
+ public PlayerDaoPlayQueueFactory playerDaoPlayQueueFactory() {
+ return new SpiedPlayerDaoPlayQueueFactory();
+ }
+ }
+
+ protected static final String CLIENT_NAME = "airsonic";
+ protected static final String JUKEBOX_PLAYER_NAME = CLIENT_NAME + "-jukebox";
+ private static final String EXPECTED_FORMAT = "json";
+ private static String AIRSONIC_API_VERSION;
+
+ private static boolean dataBasePopulated;
+ private static DaoHelper staticDaoHelper;
+
+ @Autowired
+ protected PlayerService playerService;
+ @Autowired
+ private MockMvc mvc;
+ @Autowired
+ private MusicFolderDao musicFolderDao;
+ @Autowired
+ private SettingsService settingsService;
+ @Autowired
+ private MediaScannerService mediaScannerService;
+ @Autowired
+ private PlayerDao playerDao;
+ @Autowired
+ private MediaFileDao mediaFileDao;
+ @Autowired
+ private DaoHelper daoHelper;
+ @Autowired
+ private AlbumDao albumDao;
+ @Autowired
+ private ArtistDao artistDao;
+
+ private Player testJukeboxPlayer;
+
+ @BeforeClass
+ public static void setupClass() {
+ AIRSONIC_API_VERSION = TestCaseUtils.restApiVersion();
+ dataBasePopulated = false;
+ }
+
+ @AfterClass
+ public static void cleanDataBase() {
+ staticDaoHelper.getJdbcTemplate().execute("DROP SCHEMA PUBLIC CASCADE");
+ staticDaoHelper = null;
+ dataBasePopulated = false;
+ }
+
+ /**
+ * Populate test datas in the database only once.
+ *
+ *
+ * - Creates 2 music folder
+ * - Scans the music folders
+ * - Creates a test jukebox player
+ *
+ */
+ private void populateDatabase() {
+ if (!dataBasePopulated) {
+ staticDaoHelper = daoHelper;
+
+ assertThat(musicFolderDao.getAllMusicFolders().size()).isEqualTo(1);
+ MusicFolderTestData.getTestMusicFolders().forEach(musicFolderDao::createMusicFolder);
+ settingsService.clearMusicFolderCache();
+
+ TestCaseUtils.execScan(mediaScannerService);
+
+ assertThat(playerDao.getAllPlayers().size()).isEqualTo(0);
+ createTestPlayer();
+ assertThat(playerDao.getAllPlayers().size()).isEqualTo(1);
+
+ dataBasePopulated = true;
+ }
+ }
+
+ @Before
+ public void setup() throws Exception {
+ populateDatabase();
+
+ testJukeboxPlayer = findTestJukeboxPlayer();
+ assertThat(testJukeboxPlayer).isNotNull();
+ reset(testJukeboxPlayer.getPlayQueue());
+ testJukeboxPlayer.getPlayQueue().clear();
+ assertThat(testJukeboxPlayer.getPlayQueue().size()).isEqualTo(0);
+ testJukeboxPlayer.getPlayQueue().addFiles(true,
+ mediaFileDao.getSongsForAlbum("_DIR_ Ravel", "Complete Piano Works"));
+ assertThat(testJukeboxPlayer.getPlayQueue().size()).isEqualTo(2);
+ }
+
+ protected abstract void createTestPlayer();
+
+ private Player findTestJukeboxPlayer() {
+ return playerDao.getAllPlayers().stream().filter(player -> player.getName().equals(JUKEBOX_PLAYER_NAME))
+ .findFirst().orElseThrow(() -> new RuntimeException("No player found in database"));
+ }
+
+ private String convertDateToString(Date date) {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.000'Z'");
+ formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return formatter.format(date);
+ }
+
+ private ResultMatcher playListItem1isCorrect() {
+ MediaFile mediaFile = testJukeboxPlayer.getPlayQueue().getFile(0);
+ MediaFile parent = mediaFileDao.getMediaFile(mediaFile.getParentPath());
+ Album album = albumDao.getAlbum(mediaFile.getArtist(), mediaFile.getAlbumName());
+ Artist artist = artistDao.getArtist(mediaFile.getArtist());
+ assertThat(album).isNotNull();
+ return result -> {
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].id").value(mediaFile.getId()).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].parent").value(parent.getId()).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].isDir").value(false).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].title").value("01 - Gaspard de la Nuit - i. Ondine").match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].album").value("Complete Piano Works").match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].artist").value("_DIR_ Ravel").match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].coverArt").value(parent.getId()).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].size").value(45138).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].contentType").value("audio/mpeg").match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].suffix").value("mp3").match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].duration").value(2).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].bitRate").value(128).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].path").value("_DIR_ Ravel/_DIR_ Ravel - Complete Piano Works/01 - Gaspard de la Nuit - i. Ondine.mp3").match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].isVideo").value(false).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].playCount").isNumber().match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].created").value(convertDateToString(mediaFile.getCreated())).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].albumId").value(album.getId()).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].artistId").value(artist.getId()).match(result);
+ jsonPath("$.subsonic-response.jukeboxPlaylist.entry[0].type").value("music").match(result);
+ };
+ }
+
+ @Test
+ @WithMockUser(username = "admin")
+ public void jukeboxStartActionTest() throws Exception {
+ // Given
+
+ // When and Then
+ performStartAction();
+ performStatusAction("true");
+ performGetAction()
+ .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.currentIndex").value("0"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.playing").value("true"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.gain").value("0.75"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.position").value("0"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.entry").isArray())
+ .andExpect(jsonPath("$.subsonic-response.jukeboxPlaylist.entry.length()").value(2))
+ .andExpect(playListItem1isCorrect())
+ .andDo(print());
+
+ verify(testJukeboxPlayer.getPlayQueue(), times(2)).setStatus(PlayQueue.Status.PLAYING);
+ assertThat(testJukeboxPlayer.getPlayQueue().getIndex()).isEqualTo(0);
+ assertThat(testJukeboxPlayer.getPlayQueue().getStatus()).isEqualTo(PlayQueue.Status.PLAYING);
+ }
+
+ @Test
+ @WithMockUser(username = "admin")
+ public void jukeboxStopActionTest() throws Exception {
+ // Given
+
+ // When and Then
+ performStartAction();
+ performStatusAction("true");
+ performStopAction();
+ performStatusAction("false");
+
+ verify(testJukeboxPlayer.getPlayQueue(), times(2)).setStatus(PlayQueue.Status.PLAYING);
+ verify(testJukeboxPlayer.getPlayQueue(), times(1)).setStatus(PlayQueue.Status.STOPPED);
+ assertThat(testJukeboxPlayer.getPlayQueue().getIndex()).isEqualTo(0);
+ assertThat(testJukeboxPlayer.getPlayQueue().getStatus()).isEqualTo(PlayQueue.Status.STOPPED);
+ }
+
+ private void performStatusAction(String expectedPlayingValue) throws Exception {
+ mvc.perform(get("/rest/jukeboxControl.view")
+ .param("v", AIRSONIC_API_VERSION)
+ .param("c", CLIENT_NAME)
+ .param("f", EXPECTED_FORMAT)
+ .param("action", "status")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value(expectedPlayingValue))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0"));
+ }
+
+ private ResultActions performGetAction() throws Exception {
+ return mvc.perform(get("/rest/jukeboxControl.view")
+ .param("v", AIRSONIC_API_VERSION)
+ .param("c", CLIENT_NAME)
+ .param("f", EXPECTED_FORMAT)
+ .param("action", "get")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.subsonic-response.status").value("ok"));
+ }
+
+ private void performStopAction() throws Exception {
+ mvc.perform(get("/rest/jukeboxControl.view")
+ .param("v", AIRSONIC_API_VERSION)
+ .param("c", CLIENT_NAME)
+ .param("f", EXPECTED_FORMAT)
+ .param("action", "stop")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value("false"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0"));
+ }
+
+ private void performStartAction() throws Exception {
+ mvc.perform(get("/rest/jukeboxControl.view")
+ .param("v", AIRSONIC_API_VERSION)
+ .param("c", CLIENT_NAME)
+ .param("f", EXPECTED_FORMAT)
+ .param("action", "start")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.subsonic-response.status").value("ok"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.currentIndex").value("0"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.playing").value("true"))
+ .andExpect(jsonPath("$.subsonic-response.jukeboxStatus.position").value("0"));
+ }
+}
diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java
new file mode 100644
index 00000000..f0c61a0b
--- /dev/null
+++ b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java
@@ -0,0 +1,38 @@
+package org.airsonic.player.api.jukebox;
+
+import com.github.biconou.AudioPlayer.JavaPlayer;
+import org.airsonic.player.domain.Player;
+import org.airsonic.player.domain.PlayerTechnology;
+import org.airsonic.player.service.jukebox.JavaPlayerFactory;
+import org.junit.Before;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class AirsonicRestApiJukeboxIntTest extends AbstractAirsonicRestApiJukeboxIntTest {
+
+ @MockBean
+ protected JavaPlayerFactory javaPlayerFactory;
+
+ @Before
+ @Override
+ public void setup() throws Exception {
+ super.setup();
+ JavaPlayer mockJavaPlayer = mock(JavaPlayer.class);
+ when(mockJavaPlayer.getPlayingInfos()).thenReturn( () -> 0 );
+ when(mockJavaPlayer.getGain()).thenReturn(0.75f);
+ when(javaPlayerFactory.createJavaPlayer()).thenReturn(mockJavaPlayer);
+ }
+
+ @Override
+ protected void createTestPlayer() {
+ Player jukeBoxPlayer = new Player();
+ jukeBoxPlayer.setName(JUKEBOX_PLAYER_NAME);
+ jukeBoxPlayer.setUsername("admin");
+ jukeBoxPlayer.setClientId(CLIENT_NAME + "-jukebox");
+ jukeBoxPlayer.setTechnology(PlayerTechnology.JAVA_JUKEBOX);
+ playerService.createPlayer(jukeBoxPlayer);
+ }
+
+}
\ No newline at end of file
diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java
new file mode 100644
index 00000000..2cdee852
--- /dev/null
+++ b/airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java
@@ -0,0 +1,62 @@
+package org.airsonic.player.api.jukebox;
+
+import org.airsonic.player.domain.Player;
+import org.airsonic.player.domain.PlayerTechnology;
+import org.airsonic.player.service.TranscodingService;
+import org.airsonic.player.service.jukebox.AudioPlayer;
+import org.airsonic.player.service.jukebox.AudioPlayerFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.security.test.context.support.WithMockUser;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+public class AirsonicRestApiJukeboxLegacyIntTest extends AirsonicRestApiJukeboxIntTest {
+
+ @SpyBean
+ private TranscodingService transcodingService;
+ @MockBean
+ protected AudioPlayerFactory audioPlayerFactory;
+
+ private AudioPlayer mockAudioPlayer;
+
+ @Before
+ @Override
+ public void setup() throws Exception {
+ super.setup();
+ mockAudioPlayer = mock(AudioPlayer.class);
+ when(audioPlayerFactory.createAudioPlayer(any(), any())).thenReturn(mockAudioPlayer);
+ doReturn(null).when(transcodingService).getTranscodedInputStream(any());
+ }
+
+ @Override
+ protected final void createTestPlayer() {
+ Player jukeBoxPlayer = new Player();
+ jukeBoxPlayer.setName(JUKEBOX_PLAYER_NAME);
+ jukeBoxPlayer.setUsername("admin");
+ jukeBoxPlayer.setClientId(CLIENT_NAME + "-jukebox");
+ jukeBoxPlayer.setTechnology(PlayerTechnology.JUKEBOX);
+ playerService.createPlayer(jukeBoxPlayer);
+ }
+
+ @Test
+ @WithMockUser(username = "admin")
+ @Override
+ public void jukeboxStartActionTest() throws Exception {
+ super.jukeboxStartActionTest();
+ verify(mockAudioPlayer).play();
+ }
+
+ @Test
+ @WithMockUser(username = "admin")
+ @Override
+ public void jukeboxStopActionTest() throws Exception {
+ super.jukeboxStopActionTest();
+ verify(mockAudioPlayer).play();
+ verify(mockAudioPlayer).pause();
+ }
+
+}
diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java
new file mode 100644
index 00000000..1c9293b4
--- /dev/null
+++ b/airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java
@@ -0,0 +1,41 @@
+package org.airsonic.player.service;
+
+import org.airsonic.player.domain.Transcoding;
+import org.airsonic.player.util.HomeRule;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.test.context.junit4.SpringRunner;
+import static org.mockito.Mockito.*;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class TranscodingServiceIntTest {
+
+ @Autowired
+ private TranscodingService transcodingService;
+ @SpyBean
+ private PlayerService playerService;
+
+ @ClassRule
+ public static final HomeRule classRule = new HomeRule(); // sets airsonic.home to a temporary dir
+
+ @Test
+ public void createTranscodingTest() {
+ // Given
+ Transcoding transcoding = new Transcoding(null,
+ "test-transcoding",
+ "mp3",
+ "wav",
+ "step1",
+ "step2",
+ "step3",
+ true);
+
+ transcodingService.createTranscoding(transcoding);
+ verify(playerService).getAllPlayers();
+ }
+}
diff --git a/airsonic-main/src/test/resources/application.properties b/airsonic-main/src/test/resources/application.properties
new file mode 100644
index 00000000..29c1c5dd
--- /dev/null
+++ b/airsonic-main/src/test/resources/application.properties
@@ -0,0 +1,13 @@
+spring.mvc.view.prefix: /WEB-INF/jsp/
+spring.mvc.view.suffix: .jsp
+server.error.includeStacktrace: ALWAYS
+logging.level.root=WARN
+logging.level.org.airsonic=INFO
+logging.level.liquibase=INFO
+logging.pattern.console=%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p){green} %clr(---){faint} %clr(%-40.40logger{32}){blue} %clr(:){faint} %m%n%wEx
+logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- %-40.40logger{32} : %m%n%wEx
+DatabaseConfigType=embed
+DatabaseConfigEmbedDriver=org.hsqldb.jdbcDriver
+DatabaseConfigEmbedUrl=jdbc:hsqldb:mem:airsonic
+DatabaseConfigEmbedUsername=sa
+DatabaseConfigEmbedPassword=
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index be66c6d3..b63145f6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -255,6 +255,8 @@
org.springframework:*
org.springframework.security:*
org.springframework.boot:*
+ org.assertj:*
+ org.hamcrest:*
org.apache.tomcat.embed:tomcat-embed-core*
org.apache.tomcat:tomcat-annotations-api:*