Merge branch 'master' of https://github.com/airsonic/airsonic into transifex_update
This commit is contained in:
+3
-1
@@ -2,6 +2,8 @@ language: java
|
||||
sudo: required
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
services:
|
||||
- docker
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
@@ -13,4 +15,4 @@ before_script:
|
||||
- export M2_HOME=$PWD/apache-maven-3.5.4
|
||||
- export PATH=$PWD/apache-maven-3.5.4/bin:$PATH
|
||||
script:
|
||||
- mvn package install
|
||||
- mvn verify -P integration-test
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ Note that with this release, the jdbc-extra flavored war is now the default and
|
||||
* New login page
|
||||
* Added additional war with builtin support for external databases
|
||||
* Improved playlist handling
|
||||
* DLNA browsing improvments
|
||||
* DLNA browsing improvements
|
||||
* Small fixes and improvements
|
||||
|
||||
## v6.2
|
||||
|
||||
@@ -8,7 +8,7 @@ Airsonic
|
||||
What is Airsonic?
|
||||
-----------------
|
||||
|
||||
Airsonic is a free, web-based media streamer, providing ubiquitious access to your music. Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple players simultaneously, for instance to one player in your kitchen and another in your living room.
|
||||
Airsonic is a free, web-based media streamer, providing ubiquitous access to your music. Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple players simultaneously, for instance to one player in your kitchen and another in your living room.
|
||||
|
||||
Airsonic is designed to handle very large music collections (hundreds of gigabytes). Although optimized for MP3 streaming, it works for any audio or video format that can stream over HTTP, for instance AAC and OGG. By using transcoder plug-ins, Airsonic supports on-the-fly conversion and streaming of virtually any audio format, including WMA, FLAC, APE, Musepack, WavPack and Shorten.
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@
|
||||
<gav regex="true">^org\.postgresql:postgresql:.*$</gav>
|
||||
<cve>CVE-2018-1115</cve>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes>Does not affect the postgres client</notes>
|
||||
<gav regex="true">^org\.postgresql:postgresql:.*$</gav>
|
||||
<cve>CVE-2016-7048</cve>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes>This is for nodejs</notes>
|
||||
<gav regex="true">^org\.mariadb\.jdbc:mariadb-java-client:.*$</gav>
|
||||
|
||||
+23
-2
@@ -15,6 +15,7 @@
|
||||
<properties>
|
||||
<metrics.version>3.1.0</metrics.version>
|
||||
<chameleon.version>1.2.1-RELEASE</chameleon.version>
|
||||
<tomcat.server.scope>provided</tomcat.server.scope>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -86,6 +87,16 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-ldap</artifactId>
|
||||
@@ -303,6 +314,10 @@
|
||||
<version>1.10.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- UPNP / DLNA -->
|
||||
<dependency>
|
||||
@@ -445,12 +460,12 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<scope>provided</scope>
|
||||
<scope>${tomcat.server.scope}</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-jasper</artifactId>
|
||||
<scope>provided</scope>
|
||||
<scope>${tomcat.server.scope}</scope>
|
||||
</dependency>
|
||||
<!-- Embedded Jetty -->
|
||||
<dependency>
|
||||
@@ -599,5 +614,11 @@
|
||||
<boot.artifact.excludes>spring-boot-starter-tomcat</boot.artifact.excludes>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>ide-tomcat-embed</id>
|
||||
<properties>
|
||||
<tomcat.server.scope>compile</tomcat.server.scope>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -189,6 +189,7 @@ public class Application extends SpringBootServletInitializer implements Embedde
|
||||
|
||||
@Override
|
||||
public void customize(ConfigurableEmbeddedServletContainer container) {
|
||||
LOG.trace("Servlet container is {}", container.getClass().getCanonicalName());
|
||||
// Yes, there is a good reason we do this.
|
||||
// We cannot count on the tomcat classes being on the classpath which will
|
||||
// happen if the war is deployed to another app server like Jetty. So, we
|
||||
@@ -197,6 +198,7 @@ public class Application extends SpringBootServletInitializer implements Embedde
|
||||
try {
|
||||
Class<?> tomcatESCF = Class.forName("org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory");
|
||||
if(tomcatESCF.isInstance(container)) {
|
||||
LOG.info("Detected Tomcat web server");
|
||||
LOG.debug("Attempting to optimize tomcat");
|
||||
Object tomcatESCFInstance = tomcatESCF.cast(container);
|
||||
Class<?> tomcatApplicationClass = Class.forName("org.airsonic.player.TomcatApplication");
|
||||
@@ -207,10 +209,19 @@ public class Application extends SpringBootServletInitializer implements Embedde
|
||||
LOG.debug("Skipping tomcat optimization as we are not running on tomcat");
|
||||
}
|
||||
} catch (NoClassDefFoundError | ClassNotFoundException e) {
|
||||
LOG.debug("Skipping tomcat optimization as the tomcat classes are not available");
|
||||
LOG.debug("No tomcat classes found");
|
||||
} catch (Exception e) {
|
||||
LOG.warn("An error happened while trying to optimize tomcat", e);
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> jettyESCF = Class.forName("org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory");
|
||||
if(jettyESCF.isInstance(container)) {
|
||||
LOG.warn("Detected Jetty web server. Here there be dragons.");
|
||||
}
|
||||
} catch (NoClassDefFoundError | ClassNotFoundException e) {
|
||||
LOG.debug("No jetty classes found");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.airsonic.player;
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.Wrapper;
|
||||
import org.apache.catalina.webresources.StandardRoot;
|
||||
import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
|
||||
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
|
||||
|
||||
@@ -12,6 +13,11 @@ public class TomcatApplication {
|
||||
|
||||
tomcatFactory.addContextCustomizers((TomcatContextCustomizer) context -> {
|
||||
|
||||
StandardJarScanFilter standardJarScanFilter = new StandardJarScanFilter();
|
||||
standardJarScanFilter.setTldScan("dwr-*.jar,jstl-*.jar,spring-security-taglibs-*.jar,spring-web-*.jar,spring-webmvc-*.jar,string-*.jar,taglibs-standard-impl-*.jar,tomcat-annotations-api-*.jar,tomcat-embed-jasper-*.jar");
|
||||
standardJarScanFilter.setTldSkip("*");
|
||||
context.getJarScanner().setJarScanFilter(standardJarScanFilter);
|
||||
|
||||
boolean development = (System.getProperty("airsonic.development") != null);
|
||||
|
||||
// Increase the size and time before eviction of the Tomcat
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
@@ -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<Integer, PlayQueue> playlists = Collections.synchronizedMap(new HashMap<Integer, PlayQueue>());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<com.github.biconou.AudioPlayer.api.Player> 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<com.github.biconou.AudioPlayer.api.Player> 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;
|
||||
}
|
||||
@@ -341,10 +329,6 @@ public class JukeboxJavaService {
|
||||
this.statusService = statusService;
|
||||
}
|
||||
|
||||
public void setSettingsService(SettingsService settingsService) {
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void setSecurityService(SecurityService securityService) {
|
||||
this.securityService = securityService;
|
||||
}
|
||||
|
||||
+4
-1
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,6 @@ 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
|
||||
|
||||
# Helpful to debug which jars are scanned
|
||||
#logging.level.org.apache.tomcat.util.scan=TRACE
|
||||
|
||||
+1
-1
@@ -279,7 +279,7 @@ help.homepage.title=Homepage
|
||||
help.forum.title=Forum
|
||||
help.shop.title=Merchandise
|
||||
help.contact.title=Contact
|
||||
help.contact.text=Airsonic is a community project. You can find us in <a href="irc://chat.freenode.net/airsonic">#airsonic on Freenode</a>. Technical issues can be submitted to the <a href="https://github.com/airsonic/airsonic/issues">issue tracker on GitHub</a>.
|
||||
help.contact.text=Airsonic is a community project. You can find us in <a href="irc://chat.freenode.net/airsonic">#airsonic on Freenode</a>. Technical issues can be submitted to the <a href="https://github.com/airsonic/airsonic/issues" target="_blank">issue tracker on GitHub</a>.
|
||||
help.log=Log
|
||||
help.logfile=The complete log is saved in {0}.
|
||||
# settingsHeader.jsp
|
||||
|
||||
+1
-1
@@ -279,7 +279,7 @@ help.homepage.title=\u9996\u9801
|
||||
help.forum.title=\u8AD6\u58C7
|
||||
help.shop.title=\u5546\u54C1
|
||||
help.contact.title=\u806F\u7E6B
|
||||
help.contact.text=Airsonic \u662F\u793E\u7FA4\u5C08\u6848\u3002 \u60A8\u53EF\u4EE5\u5728 <a href="irc://chat.freenode.net/airsonic">Freenode \u4E0A\u7684 #airsonic</a> \u627E\u5230\u6211\u5011\u3002\u6280\u8853\u554F\u984C\u53EF\u4EE5\u63D0\u4EA4\u5230 <a href="https://github.com/airsonic/airsonic/issues">Github \u4E0A\u7684\u554F\u984C\u8FFD\u8E64\u5668</a>\u3002
|
||||
help.contact.text=Airsonic \u662F\u793E\u7FA4\u5C08\u6848\u3002 \u60A8\u53EF\u4EE5\u5728 <a href="irc://chat.freenode.net/airsonic">Freenode \u4E0A\u7684 #airsonic</a> \u627E\u5230\u6211\u5011\u3002\u6280\u8853\u554F\u984C\u53EF\u4EE5\u63D0\u4EA4\u5230 <a href="https://github.com/airsonic/airsonic/issues" target="_blank">Github \u4E0A\u7684\u554F\u984C\u8FFD\u8E64\u5668</a>\u3002
|
||||
help.log=\u8A18\u9304
|
||||
help.logfile=\u5B8C\u6574\u7684\u7D00\u9304\u5B58\u653E\u5728 {0}\u3002
|
||||
# settingsHeader.jsp
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+279
@@ -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.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Creates 2 music folder</li>
|
||||
* <li>Scans the music folders</li>
|
||||
* <li>Creates a test jukebox player</li>
|
||||
* </ul>
|
||||
*/
|
||||
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"));
|
||||
}
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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=
|
||||
@@ -16,6 +16,7 @@ RUN apk --no-cache add \
|
||||
ttf-dejavu \
|
||||
ca-certificates \
|
||||
tini \
|
||||
curl \
|
||||
openjdk8-jre
|
||||
|
||||
COPY run.sh /usr/local/bin/run.sh
|
||||
@@ -28,6 +29,6 @@ EXPOSE $AIRSONIC_PORT
|
||||
|
||||
VOLUME $AIRSONIC_DIR/data $AIRSONIC_DIR/music $AIRSONIC_DIR/playlists $AIRSONIC_DIR/podcasts
|
||||
|
||||
HEALTHCHECK CMD wget -q http://localhost:"$AIRSONIC_PORT""$CONTEXT_PATH"/rest/ping -O /dev/null || exit 1
|
||||
HEALTHCHECK --interval=15s --timeout=3s CMD wget -q http://localhost:"$AIRSONIC_PORT""$CONTEXT_PATH"rest/ping -O /dev/null || exit 1
|
||||
|
||||
ENTRYPOINT ["tini", "--", "run.sh"]
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>airsonic-integration-test</artifactId>
|
||||
<name>Airsonic Integration Test</name>
|
||||
|
||||
<parent>
|
||||
<groupId>org.airsonic.player</groupId>
|
||||
<artifactId>airsonic</artifactId>
|
||||
<version>10.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<cucumber.version>2.3.1</cucumber.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-core</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-junit</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-java8</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cucumber</groupId>
|
||||
<artifactId>cucumber-spring</artifactId>
|
||||
<version>${cucumber.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.spotify</groupId>
|
||||
<artifactId>docker-client</artifactId>
|
||||
<version>8.13.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xmlunit</groupId>
|
||||
<artifactId>xmlunit-core</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xmlunit</groupId>
|
||||
<artifactId>xmlunit-matchers</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.airsonic.player</groupId>
|
||||
<artifactId>subsonic-rest-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<testResources>
|
||||
<testResource>
|
||||
<directory>src/test/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</testResource>
|
||||
</testResources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.temyers</groupId>
|
||||
<artifactId>cucumber-jvm-parallel-plugin</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generateRunners</id>
|
||||
<phase>generate-test-sources</phase>
|
||||
<goals>
|
||||
<goal>generateRunners</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<featuresDirectory>src/test/resources/features</featuresDirectory>
|
||||
<format>pretty</format>
|
||||
<glue>org.airsonic.test.cucumber.steps</glue>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<compilerVersion>1.8</compilerVersion>
|
||||
<showWarnings>true</showWarnings>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-jar</id>
|
||||
<phase/>
|
||||
<configuration>
|
||||
<finalName>unwanted</finalName>
|
||||
<classifier>unwanted</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.21.0</version>
|
||||
<configuration>
|
||||
<parallel>all</parallel>
|
||||
<threadCount>2</threadCount>
|
||||
<includes>
|
||||
<include>**/Parallel*IT.class</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.airsonic.test;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("org.airsonic.test")
|
||||
@PropertySource("classpath:application.properties")
|
||||
public class SpringContext {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.airsonic.test.cucumber;
|
||||
|
||||
import cucumber.api.CucumberOptions;
|
||||
import cucumber.api.junit.Cucumber;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(Cucumber.class)
|
||||
@CucumberOptions(plugin = {"pretty"},
|
||||
features = "classpath:features/api/stream-mp3.feature",
|
||||
glue = "org.airsonic.test.cucumber.steps")
|
||||
public class RunCukesTest {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.airsonic.test.cucumber.server;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface AirsonicServer {
|
||||
String getBaseUri();
|
||||
|
||||
void uploadToDefaultMusicFolder(Path directoryPath, String relativePath);
|
||||
|
||||
default void addRestParameters(RequestBuilder builder) {
|
||||
builder.addParameter("c", "inttest");
|
||||
builder.addParameter("v", "1.15.0");
|
||||
builder.addParameter("u", "admin");
|
||||
builder.addParameter("s", "int");
|
||||
builder.addParameter("t", DigestUtils.md5Hex("admin" + "int"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.airsonic.test.cucumber.steps.api;
|
||||
|
||||
import cucumber.api.java8.En;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.xmlunit.builder.Input;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.xmlunit.matchers.CompareMatcher.isIdenticalTo;
|
||||
|
||||
public class PingStepDef implements En {
|
||||
|
||||
private CloseableHttpResponse response;
|
||||
private CloseableHttpClient client;
|
||||
|
||||
public PingStepDef(AirsonicServer server) {
|
||||
this.client = HttpClientBuilder.create().build();
|
||||
When("^A ping request is sent$", () -> {
|
||||
HttpGet httpGet = new HttpGet(server.getBaseUri() + "/rest/ping");
|
||||
this.response = client.execute(httpGet);
|
||||
});
|
||||
Then("^A required parameter response is received$", () -> {
|
||||
if(response == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
try {
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
assertEquals(statusLine.getStatusCode(), 200);
|
||||
HttpEntity entity = response.getEntity();
|
||||
String actual = EntityUtils.toString(entity);
|
||||
assertThat(
|
||||
actual,
|
||||
isIdenticalTo(
|
||||
Input.fromStream(
|
||||
getClass().getResourceAsStream("/blobs/ping/missing-auth.xml")))
|
||||
.ignoreWhitespace());
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.airsonic.test.cucumber.steps.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import cucumber.api.java8.En;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.junit.Assert;
|
||||
import org.subsonic.restapi.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ScanStepDef implements En {
|
||||
|
||||
private final AirsonicServer server;
|
||||
private CloseableHttpResponse response;
|
||||
private CloseableHttpClient client;
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public ScanStepDef(AirsonicServer server) {
|
||||
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
|
||||
this.client = HttpClientBuilder.create().build();
|
||||
this.server = server;
|
||||
|
||||
Given("a scan is done", () -> {
|
||||
Assert.assertFalse(isScanning());
|
||||
|
||||
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/startScan");
|
||||
server.addRestParameters(builder);
|
||||
response = client.execute(builder.build());
|
||||
System.out.println(EntityUtils.toString(response.getEntity()));
|
||||
Long waitTime = 30000L;
|
||||
Long sleepTime = 1000L;
|
||||
while(waitTime > 0 && isScanning()) {
|
||||
waitTime -= sleepTime;
|
||||
Thread.sleep(sleepTime);
|
||||
}
|
||||
|
||||
Assert.assertFalse(isScanning());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private boolean isScanning() throws IOException {
|
||||
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/getScanStatus");
|
||||
builder.addParameter("f", "json");
|
||||
server.addRestParameters(builder);
|
||||
response = client.execute(builder.build());
|
||||
|
||||
String responseAsString = EntityUtils.toString(response.getEntity());
|
||||
JsonNode jsonNode = mapper.readTree(responseAsString).get("subsonic-response");
|
||||
Response response = mapper.treeToValue(jsonNode, Response.class);
|
||||
return response.getScanStatus().isScanning();
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package org.airsonic.test.cucumber.steps.api;
|
||||
|
||||
import cucumber.api.java.Before;
|
||||
import org.airsonic.test.SpringContext;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
@ContextConfiguration(classes = SpringContext.class)
|
||||
public class SpringStepDef {
|
||||
|
||||
public SpringStepDef(AirsonicServer server) {
|
||||
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup_cucumber_spring_context(){
|
||||
// Dummy method so cucumber will recognize this class as glue
|
||||
// and use its context configuration.
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package org.airsonic.test.cucumber.steps.api;
|
||||
|
||||
import cucumber.api.java8.En;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.HexDump;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class StreamStepDef implements En {
|
||||
|
||||
private CloseableHttpResponse response;
|
||||
private CloseableHttpClient client;
|
||||
private boolean closed = false;
|
||||
private byte[] body;
|
||||
|
||||
public StreamStepDef(AirsonicServer server) {
|
||||
this.client = HttpClientBuilder.create().build();
|
||||
Given("Media file (.*) is added", (String mediaFile) -> {
|
||||
// TODO fix this
|
||||
server.uploadToDefaultMusicFolder(
|
||||
Paths.get(this.getClass().getResource("/blobs/stream/piano").toURI()),
|
||||
"");
|
||||
});
|
||||
|
||||
When("A stream request is sent", () -> {
|
||||
RequestBuilder builder = RequestBuilder.create("GET").setUri(server.getBaseUri() + "/rest/stream");
|
||||
builder.addParameter("id", "2");
|
||||
builder.addHeader("Range", "bytes=0-");
|
||||
builder.addHeader("Accept", "audio/webm,audio/ogg,audio/wav,audio/*;");
|
||||
server.addRestParameters(builder);
|
||||
response = client.execute(builder.build());
|
||||
});
|
||||
|
||||
Then("The response bytes are equal", () -> {
|
||||
ensureBodyRead();
|
||||
|
||||
FileUtils.writeByteArrayToFile(new File("/tmp/bytearray"), body);
|
||||
|
||||
byte[] expected = IOUtils.toByteArray(this.getClass().getResource("/blobs/stream/piano/piano.mp3").toURI());
|
||||
//
|
||||
// HexDump.dump(expected, 0, System.out, 0);
|
||||
|
||||
Assert.assertArrayEquals(expected, body);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
void ensureBodyRead() throws IOException {
|
||||
if(closed) {
|
||||
return;
|
||||
} else {
|
||||
this.body = EntityUtils.toByteArray(response.getEntity());
|
||||
closed = true;
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package org.airsonic.test.cucumber_hooks.docker;
|
||||
|
||||
import com.spotify.docker.client.DefaultDockerClient;
|
||||
import com.spotify.docker.client.DockerClient;
|
||||
import com.spotify.docker.client.exceptions.DockerException;
|
||||
import com.spotify.docker.client.messages.*;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.spotify.docker.client.DockerClient.RemoveContainerParam.*;
|
||||
|
||||
@Component
|
||||
@Profile("dynamic")
|
||||
public class DynamicDockerHook implements AirsonicServer, EnvironmentAware, InitializingBean, DisposableBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DynamicDockerHook.class);
|
||||
public static final String AIRSONIC_DOCKER_IMAGE = "airsonic.docker.image";
|
||||
public static final String AIRSONIC_DOCKER_PORT = "airsonic.docker.port";
|
||||
public static final String AIRSONIC_READY_MAX_WAIT = "airsonic.ready.max_wait";
|
||||
public static final String AIRSONIC_READY_SLEEP_TIME = "airsonic.ready.sleep_time";
|
||||
|
||||
private String serverUri = null;
|
||||
private final DockerClient docker;
|
||||
private String containerId;
|
||||
private String dockerImage;
|
||||
private Integer dockerPort;
|
||||
private Long readyMaxWaitTime;
|
||||
private Long readySleepTime;
|
||||
|
||||
public DynamicDockerHook() {
|
||||
logger.debug("Using hook for dynamically creating containers");
|
||||
docker = new DefaultDockerClient("unix:///var/run/docker.sock");
|
||||
testDockerIsAvail();
|
||||
}
|
||||
|
||||
private void testDockerIsAvail() {
|
||||
try {
|
||||
logger.trace("Trying to ping docker daemon");
|
||||
docker.ping();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUri() {
|
||||
if (serverUri == null) {
|
||||
throw new IllegalStateException("Server is not yet ready");
|
||||
} else {
|
||||
return serverUri;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadToDefaultMusicFolder(Path localDir, String relativePath) {
|
||||
try {
|
||||
// TODO ensure localDir is a directory
|
||||
docker.copyToContainer(localDir, containerId, "/airsonic/music/" + relativePath);
|
||||
} catch (DockerException | IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void startServer() {
|
||||
logger.debug("Starting server");
|
||||
final ContainerConfig config = ContainerConfig.builder()
|
||||
.image(dockerImage)
|
||||
.build();
|
||||
|
||||
final String name = "airsonic-it-" + RandomStringUtils.randomAlphabetic(10);
|
||||
try {
|
||||
final ContainerCreation containerCreate = docker.createContainer(config, name);
|
||||
containerId = containerCreate.id();
|
||||
docker.startContainer(containerId);
|
||||
Long waitTime = readyMaxWaitTime;
|
||||
while(true) {
|
||||
ContainerInfo containerInfo = docker.inspectContainer(containerId);
|
||||
ContainerState.Health health = containerInfo.state().health();
|
||||
if (health != null && StringUtils.equalsIgnoreCase(health.status(), "healthy")) {
|
||||
logger.trace("Container started early. Yay!");
|
||||
break;
|
||||
} else if(waitTime > readySleepTime) {
|
||||
if(logger.isTraceEnabled()) {
|
||||
String message;
|
||||
if(health != null) {
|
||||
message = "Container ("+name+") not yet ready. State was: " + health.status();
|
||||
} else {
|
||||
message = "Container ("+name+") state unknown. Waiting";
|
||||
}
|
||||
logger.trace(message);
|
||||
}
|
||||
waitTime -= readySleepTime;
|
||||
Thread.sleep(readySleepTime);
|
||||
} else if(health == null) {
|
||||
logger.trace("Max wait time with unknown container state. Hoping container is ready");
|
||||
break;
|
||||
} else {
|
||||
logger.trace("Container ("+name+") never became ready within max wait time");
|
||||
throw new RuntimeException("Container ("+name+") not ready");
|
||||
}
|
||||
}
|
||||
ContainerInfo containerInfo = docker.inspectContainer(containerId);
|
||||
try {
|
||||
Map.Entry<String, AttachedNetwork> next = containerInfo.networkSettings().networks().entrySet().iterator().next();
|
||||
String ipAddress = next.getValue().ipAddress();
|
||||
serverUri = "http://" + ipAddress + ":" + dockerPort;
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Could not determine container ("+name+") address", e);
|
||||
}
|
||||
} catch (DockerException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopServer() {
|
||||
if(containerId != null) {
|
||||
try {
|
||||
docker.removeContainer(containerId, forceKill(), removeVolumes());
|
||||
} catch (DockerException | InterruptedException e) {
|
||||
throw new RuntimeException("Could not remove container", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnvironment(Environment environment) {
|
||||
dockerImage = environment.getRequiredProperty(AIRSONIC_DOCKER_IMAGE);
|
||||
dockerPort = Integer.parseInt(environment.getRequiredProperty(AIRSONIC_DOCKER_PORT));
|
||||
readyMaxWaitTime = Long.parseLong(environment.getRequiredProperty(AIRSONIC_READY_MAX_WAIT));
|
||||
readySleepTime = Long.parseLong(environment.getRequiredProperty(AIRSONIC_READY_SLEEP_TIME));
|
||||
if(readyMaxWaitTime <= 0L || readySleepTime <= 0L) {
|
||||
throw new IllegalArgumentException("Max wait time and sleep time must be greater than 0");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
stopServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
startServer();
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package org.airsonic.test.cucumber_hooks.docker;
|
||||
|
||||
import com.spotify.docker.client.DefaultDockerClient;
|
||||
import com.spotify.docker.client.DockerClient;
|
||||
import com.spotify.docker.client.exceptions.DockerException;
|
||||
import com.spotify.docker.client.messages.AttachedNetwork;
|
||||
import com.spotify.docker.client.messages.ContainerInfo;
|
||||
import org.airsonic.test.cucumber.server.AirsonicServer;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Profile("existing")
|
||||
public class ExistingDockerHook implements AirsonicServer, EnvironmentAware, InitializingBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExistingDockerHook.class);
|
||||
public static final String AIRSONIC_DOCKER_CONTAINER = "airsonic.docker.container";
|
||||
public static final String AIRSONIC_DOCKER_PORT = "airsonic.docker.port";
|
||||
|
||||
private String serverUri = null;
|
||||
private final DockerClient docker;
|
||||
private String containerId;
|
||||
private Integer dockerPort;
|
||||
|
||||
public ExistingDockerHook() {
|
||||
logger.debug("Using hook for existing docker container");
|
||||
docker = new DefaultDockerClient("unix:///var/run/docker.sock");
|
||||
testDockerIsAvail();
|
||||
}
|
||||
|
||||
private void testDockerIsAvail() {
|
||||
try {
|
||||
logger.trace("Trying to ping docker daemon");
|
||||
docker.ping();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUri() {
|
||||
if (serverUri == null) {
|
||||
throw new IllegalStateException("Server is not yet ready");
|
||||
} else {
|
||||
return serverUri;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadToDefaultMusicFolder(Path localDir, String relativePath) {
|
||||
try {
|
||||
// TODO ensure localDir is a directory
|
||||
docker.copyToContainer(localDir, containerId, "/airsonic/music/" + relativePath);
|
||||
} catch (DockerException | IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void connectToServer() {
|
||||
logger.debug("Connecting to server");
|
||||
|
||||
try {
|
||||
ContainerInfo containerInfo = docker.inspectContainer(containerId);
|
||||
if(!containerInfo.state().running()) {
|
||||
throw new IllegalStateException("Container is not running " + containerId);
|
||||
}
|
||||
Map.Entry<String, AttachedNetwork> next = containerInfo.networkSettings().networks().entrySet().iterator().next();
|
||||
String ipAddress = next.getValue().ipAddress();
|
||||
if(StringUtils.isBlank(ipAddress)) {
|
||||
throw new IllegalStateException("No address found for container " + containerId);
|
||||
}
|
||||
serverUri = "http://" + ipAddress + ":" + dockerPort;
|
||||
} catch (DockerException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnvironment(Environment environment) {
|
||||
containerId = environment.getRequiredProperty(AIRSONIC_DOCKER_CONTAINER);
|
||||
dockerPort = Integer.parseInt(environment.getRequiredProperty(AIRSONIC_DOCKER_PORT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
connectToServer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
airsonic.docker.port=4040
|
||||
airsonic.ready.max_wait=80000
|
||||
airsonic.ready.sleep_time=5000
|
||||
|
||||
# Use for dynamically generating a container as needed
|
||||
spring.profiles.active=dynamic
|
||||
airsonic.docker.image=airsonic/airsonic:${project.version}
|
||||
|
||||
# Use for testing against an existing/running container
|
||||
#airsonic.docker.container=1212be8a94e0
|
||||
#spring.profiles.active=existing
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="failed" version="1.15.0">
|
||||
<error code="10" message="Required parameter is missing."/>
|
||||
</subsonic-response>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.15.0"/>
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
Feature: Ping API
|
||||
|
||||
Scenario: Airsonic responds to ping requests
|
||||
When A ping request is sent
|
||||
Then A required parameter response is received
|
||||
@@ -0,0 +1,11 @@
|
||||
Feature: Stream API for MP3
|
||||
|
||||
Background:
|
||||
Given Media file stream/piano/piano.mp3 is added
|
||||
And a scan is done
|
||||
|
||||
Scenario: Airsonic sends stream data
|
||||
When A stream request is sent
|
||||
Then The response bytes are equal
|
||||
# TODO check length
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger level="trace" name="org.airsonic.test.cucumber_hooks.docker.DockerHook" />
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -116,12 +116,22 @@
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>2.8.11</version>
|
||||
<version>2.9.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.8.11.1</version>
|
||||
<version>2.9.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<version>2.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>20.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
@@ -303,6 +313,16 @@
|
||||
<module>install/docker</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>integration-test</id>
|
||||
<modules>
|
||||
<module>subsonic-rest-api</module>
|
||||
<module>airsonic-sonos-api</module>
|
||||
<module>airsonic-main</module>
|
||||
<module>install/docker</module>
|
||||
<module>integration-test</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>sign</id>
|
||||
<build>
|
||||
|
||||
Reference in New Issue
Block a user