From 004b8bba37f0614c5bec178cbf8e42758d563211 Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Sun, 30 Sep 2018 22:53:12 -0600 Subject: [PATCH 01/10] Added docker based integration testing Signed-off-by: Andrew DeMaria --- .travis.yml | 4 +- install/docker/Dockerfile | 3 +- integration-test/pom.xml | 205 ++++++++++++++++++ .../java/org/airsonic/test/SpringContext.java | 12 + .../airsonic/test/cucumber/RunCukesTest.java | 13 ++ .../test/cucumber/server/AirsonicServer.java | 20 ++ .../test/cucumber/steps/api/PingStepDef.java | 50 +++++ .../test/cucumber/steps/api/ScanStepDef.java | 61 ++++++ .../cucumber/steps/api/SpringStepDef.java | 20 ++ .../cucumber/steps/api/StreamStepDef.java | 69 ++++++ .../docker/DynamicDockerHook.java | 158 ++++++++++++++ .../docker/ExistingDockerHook.java | 98 +++++++++ .../src/test/resources/application.properties | 12 + .../resources/blobs/ping/missing-auth.xml | 4 + .../src/test/resources/blobs/ping/ok.xml | 2 + .../resources/blobs/stream/piano/piano.mp3 | Bin 0 -> 102272 bytes .../test/resources/features/api/ping.feature | 5 + .../resources/features/api/stream-mp3.feature | 11 + .../src/test/resources/logback.xml | 14 ++ pom.xml | 24 +- 20 files changed, 781 insertions(+), 4 deletions(-) create mode 100644 integration-test/pom.xml create mode 100644 integration-test/src/test/java/org/airsonic/test/SpringContext.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java create mode 100644 integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java create mode 100644 integration-test/src/test/resources/application.properties create mode 100644 integration-test/src/test/resources/blobs/ping/missing-auth.xml create mode 100644 integration-test/src/test/resources/blobs/ping/ok.xml create mode 100644 integration-test/src/test/resources/blobs/stream/piano/piano.mp3 create mode 100644 integration-test/src/test/resources/features/api/ping.feature create mode 100644 integration-test/src/test/resources/features/api/stream-mp3.feature create mode 100644 integration-test/src/test/resources/logback.xml diff --git a/.travis.yml b/.travis.yml index f0db4f2c..f9183d47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/install/docker/Dockerfile b/install/docker/Dockerfile index 83068a77..d3a00260 100644 --- a/install/docker/Dockerfile +++ b/install/docker/Dockerfile @@ -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"] diff --git a/integration-test/pom.xml b/integration-test/pom.xml new file mode 100644 index 00000000..00f96fc3 --- /dev/null +++ b/integration-test/pom.xml @@ -0,0 +1,205 @@ + + + 4.0.0 + airsonic-integration-test + Airsonic Integration Test + + + org.airsonic.player + airsonic + 10.2.0-SNAPSHOT + + + + UTF-8 + 2.3.1 + + + + + io.cucumber + cucumber-core + ${cucumber.version} + test + + + io.cucumber + cucumber-junit + ${cucumber.version} + test + + + io.cucumber + cucumber-java + ${cucumber.version} + test + + + io.cucumber + cucumber-java8 + ${cucumber.version} + test + + + io.cucumber + cucumber-spring + ${cucumber.version} + test + + + org.springframework + spring-context + test + + + org.springframework + spring-test + + + junit + junit + 4.12 + test + + + org.apache.commons + commons-lang3 + test + + + commons-codec + commons-codec + test + + + commons-io + commons-io + test + + + org.apache.httpcomponents + httpcore + test + + + org.apache.httpcomponents + httpclient + test + + + com.spotify + docker-client + 8.13.1 + test + + + org.slf4j + slf4j-api + test + + + ch.qos.logback + logback-classic + test + + + com.google.guava + guava + test + + + com.fasterxml.jackson.core + jackson-databind + test + + + org.xmlunit + xmlunit-core + 2.6.0 + test + + + org.xmlunit + xmlunit-matchers + 2.6.0 + test + + + org.airsonic.player + subsonic-rest-api + ${project.version} + test + + + + + + + src/test/resources + true + + + + + com.github.temyers + cucumber-jvm-parallel-plugin + 5.0.0 + + + generateRunners + generate-test-sources + + generateRunners + + + src/test/resources/features + pretty + org.airsonic.test.cucumber.steps + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + 1.8 + true + UTF-8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + default-jar + + + unwanted + unwanted + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.21.0 + + all + 2 + + **/Parallel*IT.class + + + + + + + diff --git a/integration-test/src/test/java/org/airsonic/test/SpringContext.java b/integration-test/src/test/java/org/airsonic/test/SpringContext.java new file mode 100644 index 00000000..e38c6aef --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/SpringContext.java @@ -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 { + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java b/integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java new file mode 100644 index 00000000..821959ac --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/RunCukesTest.java @@ -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 { + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java b/integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java new file mode 100644 index 00000000..b3e652d4 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/server/AirsonicServer.java @@ -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")); + } +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java new file mode 100644 index 00000000..c66142db --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/PingStepDef.java @@ -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(); + } + }); + } + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java new file mode 100644 index 00000000..784e43e6 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/ScanStepDef.java @@ -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(); + } + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java new file mode 100644 index 00000000..b1ff303f --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/SpringStepDef.java @@ -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. + } +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java new file mode 100644 index 00000000..39aedfb4 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber/steps/api/StreamStepDef.java @@ -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(); + } + } + +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java new file mode 100644 index 00000000..4c08de5d --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/DynamicDockerHook.java @@ -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 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(); + } +} diff --git a/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java new file mode 100644 index 00000000..27592917 --- /dev/null +++ b/integration-test/src/test/java/org/airsonic/test/cucumber_hooks/docker/ExistingDockerHook.java @@ -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 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(); + } +} diff --git a/integration-test/src/test/resources/application.properties b/integration-test/src/test/resources/application.properties new file mode 100644 index 00000000..95c1423a --- /dev/null +++ b/integration-test/src/test/resources/application.properties @@ -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 + diff --git a/integration-test/src/test/resources/blobs/ping/missing-auth.xml b/integration-test/src/test/resources/blobs/ping/missing-auth.xml new file mode 100644 index 00000000..7199e614 --- /dev/null +++ b/integration-test/src/test/resources/blobs/ping/missing-auth.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/integration-test/src/test/resources/blobs/ping/ok.xml b/integration-test/src/test/resources/blobs/ping/ok.xml new file mode 100644 index 00000000..54302310 --- /dev/null +++ b/integration-test/src/test/resources/blobs/ping/ok.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/integration-test/src/test/resources/blobs/stream/piano/piano.mp3 b/integration-test/src/test/resources/blobs/stream/piano/piano.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..dd253d5d33b3e12783e79037859ebca57d20cc57 GIT binary patch literal 102272 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

J$FjW8(ihu`DMyRvT8+e!eu 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

XiF@UG|X8$Ji2|Dq{<;E z7IAUpdkt#jLI?fHihKN=OWNTY08@{9TPs+FfA9aCifI@%n-PhtDK)T|0R(cO0eCGr zXJn>a^N2ri^Ao0-nG6F-DRxQ*8{s_Hl9T~3LYE@Ba7`m4WfJ8=>6A0*1tv_^vL`o0 zxn(KG+WjSg%87BwT`vV5-D4{N`u3wFyg#Z`dsOgP7=jcVl4vWxJae;UVy*E@>5n5v zo97$!0#;qt9Vs2agHqZ5svh4wTf?QmZ%*A@B_DwXtO-FLlRxQ$*0Qqfb%zI-vVZ>z zY<6N74YY?uPe=~Ss29ghgMh&1|3Cd1d{bHZ>_6D5%WO*7P*x`Sqscl8Hxj7kIK=0W zRQFiZFvS)Fu*1Bvf;+DW%F^cC{bbBFA2zalpB36Y0xqe3e}>|a2}H>-)DI(+xcS1 z`YVkt$D*qYUf2Hvj2}iwW@}zW+n zHbi`yTKV53l}*z&mB-G+WjNf{sWczO1x9g&vB+Pd%AXauTnwn)ExD zm?c7n#zmsPG#q0z&Hj<7wQiC(Hj02ZYFK?-%o<^g_)r2byHEt4tbb>x(s;VUY>@nr zzWNY<12@L0f0iqjJSCBJYqfDl$+Sfo@b!T(mHHE8cF8B!)*1P{≪z z$Lejd*R<#pUFpKUbW#X}F9_Msp@Ov|MzhQ5*A&Aa=yWQsVx|-!&vG ze0pxe!yk2`>RTz$7X}$o4Xl-aa7_b~HnQ^YlrS(CnH7(G0!VyLLf}-ML*bl+H#VcU z7yk4=EcXBnfD{z1&l!KsI5_qLa08`MO`>);V3FX`tT9m`U7@vc$tf*Rck6Uua0tb{ zlI}ZUzvBmJL%Ot{36${9M!oJZCN2)=S0-Hg*K_TIsC~O3f@L+iyASOvAqhzF=Sn}g zC?&R%{D=#R~nXqw8lQ5s%W5V&IrL>2T3_~E^3Lq6^dzely-`;zlKybcwXt6p{_ z5pK_?!bv6tC~ZFC&tsCgJ&ureh-}V(V%_dg6VDmfzf;fOQRG0ut4PsP26~6$Syjw# zL~p?3hw3Ql0~OkkQg_NL91A1Kt_>C@FyE5@6<|tuAKC)MPsQ>WYQ@Y15q-U?se)G(teg;WCRP>vVTT@Y8hP!hSs0Y zM&V$K%1h5P4}J?yxT5K#vSgoVYABNxo<5#Q5}TJ9SB@^Gw#~8Qe-I)QoJf04ZQq)| z$%rH0Dm|<8%L;vD&~Xxf$s6xdGV49gd2o4FeOa-1aUjyz1wWYi8WowU$-l8xSqvuI z*J`--!>1(rA-_&_IpI}~fLTh7llzm2)f>RD(ZVln*$vTp7i-}vs8&xg5M5?1%Q3a4#fsBUemw;)JX+Cl7N9M)~4x%hSSv0pM+ z0xGzTK@_x7@JgJAa^sGM55fmxRzNs8O#Mii)_O&+K+W3~h0@Wyu)OuvinQ8deBo=u z$ouV#yJi2VGrMygIHRSbzcbmH|1rqO{k%ehM6ASwUxt zLm<6bpSAQrEV%om!j+L{uU}mIXa^+_EOsZSZO{9Lb3;jbyE^nbtJ)=2vKf?B!bt!9 z*GosGHfJFIbBqJ;X0J8+kvVEQZY2b+1to=!IOIT7F$RDu3*!5+?*I754Q}zT6u?S; z=v4aub%1O^LO(S;E%of=A^}Z&<{t&;TD33bU6uQ&p*ywJDK<*^r-TnX>tqTER@K!Y zkZK$AaMf(}&q~7c|LRp1VeXWI6qn1u8m~nHieNt_?TFW=5x#vI!hlHQA|}EJsG?~k z1@-@&&hv|+52SUkE_ApMrqp8*nrdk5QwY^6e|#{FF$KC!ftXLZbAQ`rilnSEHKk|k zx6QflhoEXr2D!U&)||@f$nB8s(n|=P|W8to0tt# zMIbpY>1f0BV(xHC4+8CX$tr_<*QNMH=-vmjo(;K`P|KS=4&%SfC$}|VW;j1wO z>Ueud>SK!i$NwC8a7*&D|3K)XH@{jbgL=*eT&>WT+~-{^Cg7hXj> z&^gA=G3J#~E~=GyW)0yR)^4a|&amG^GZ!T95(s&a=hO*IYi<0$KqU$IqT7wWS!5I1 zPCpMUc&6(-TF+P)wy-ZVS9MQE$w+r&+P#*gnIm~l-9zXq`ASL><;){MxC1Z?C}HBb z7nLPhs*vvjkw4FDGBCRd0%lY=;UNE|tznyjj%wf`HOHtX^H-4gG73D$pIHdBd{gQL z3wAD9q%4LMN^7R3>nIqUOIeA?Jdw*K*TZ1&3A9?MQ-(|TdzZ4OuCucYWU#zxh6sbBA(UjZbQGNYU|2?#) zl~Y891|32+Ho^)aAX;)(gF9YB4k&|<<4D~lx7QvVJ+6J|gaCmek>Nc^D;Y_nB3Jb{ zB=pmP2W^^_76KwlO=BF@#?^jU04>p}qeu{iEM=Al^x$7ENT$WHh>K~?UoCw0$i z#Mi3C)L4abm^9}$sssjYeR%&Avkx6pIdeg4@nmO|NqmBaN<51xo(#IFDe1vKee4(J z7jBhl{00Ug{F;^p-qUI%T-n4O`z4bc>}{`l11@ayT6`;IgHG&96~{dkiT~q&7B=`S zviRx0q^nbv;AuhSoB)C|l1oy0M){n7MHTTHhwmk&B}_~*Li<{^?Dat?=J z9^xI}a{_3<8c*}Vvkw)$H#s?XZ6mrn>zglKUis4Y4&AQ_Srjmn8vc3Y>Err!!MD^e zP+?nWT@c|G?AWFM%isJRHzX;lQNSiCYh5|f@;QH*9G}oV{Q~FDlzxHo$)C!$CO{Rk zT~R!3S`(7$TSNkE-c1;=q>t}I=Pz0;g6_^Hsx~|U|4StUk=&vV56=#dEEYO$b3)lD zqjWw%B{Dr$MFb5Z!NUMwY~kcK3U;8~Y(*;8qY5J%V0RepE-m1xH*T^rXY9!$n73Wf z!?-eIMU!MYs-bMT(m2s%Y%mWS7TX%T17xtECz&U;i@T(#oYqu zE*0_|I|8&sPrz9(gn)G=4w2ta3b^Sp3hR*Fdx;^k*`~+>*>#uze6z6%R&f}#yYkynie!>3EVYC za(Snn%sC7wF(jOdcX#_Unl&oxD-XSe#GIG;gg-(ZGC7Ns1~Hd7%^uBvJAh-YtlV%K zD!SAuF=A(nkU&vDo88HbRujj~i=}PFV;DM`F$a zU)!HHB9>hGA*@B^@*D8PDbvDs>QaztvsSNx24jx7!yl33%aUm4*NpFvc@}SreU)2tUdT>G^O6#>CbmepvbF!CCVzOH6VM4tPF!30uV6L{I zhkYs8!R3IGK(1yL7M@Osu)pI=#q_9xOdd`7yTR$#@b5`acsunkdd!;3xCg4sk@kJn z_Vo0#XntvabQ_=Y`aTY|_7JKc&)28dahmrq#8Qawg1Hc{N#|Tt+xYz(P|EZF`CnOI z!bghJ&lbqDP-;_eT3JMzC0O_ApJussG7>TxKO%%kgiR1~9l%}wA!#agK7iCf!OYA=Rtn_(BI1lbladEhm zeyf?M3DBCFvuVg^AK=q#Loo6deI}nYy*!peR>GE_@bI>h0hk#skKYeUp4y%P$>4rM ze%qrWa_BAL7Y@JxBL{>yQDj8NrXoMD?ERzMW}Z*Wog(jD7)J_OaP&F7CwW%Gz*hpKvR{yNLsX9ol zW6Mbm(xe)m%~I{QbjXIWFxk zl?FLcykXM3p%pnIb+jh()wDtC&%u@X0 zNupq2mP+H;26i>a;&?0(k4#$h79JYM1&E=BNPLMEs&x>e8IBU*rW`7vC1swC$&E-# z(?50W@HJIZ^m`$E(0$vyGO*y5?$=A-hci0X6N4?A$kalLggVD5@|xuG{UkPuP3HP3 z?nPO8O5l56#g{pW70yH;H&aTZn$9D@sS5mabRvk`CiE?;72#pgg9BXl4jx+bg zYq3!2yp~jn8mKjS@K8kW2|N(`;O%Nt7i|PJiKpR%S-upeerKP8utHy`HXd4+vN0-G z%Xiar+_Q_V>U+!VIY&`@`7AN4bNMu}(0_hb)d0%R8gsu0S{rKo0UNaluPE8@)g;;| z=fyT=$WYw_v?ebY#aD!luWoZix*@l4SXs=okE$aavK{m*yV;yqHfGZ7U?Ov%-pWPn zRqDGe9X+LDF!%AruiuaQxjS)%!|)}r=D0L*K)jX*G9{met<@Yvvh_Le>+kJw0DzqS z*f|mb1;b;V;0)^<>$wctyI4b4q$pN~xU09^AODzLW-0iX*t7Y3Z<1>-@*$En5o!D$ z5?dL?pXw=?w>riSys0C#%k@tFkpAB6 z%Sptxf8}UgWxDtfVSD*=c3D-og`WvCsXi%Vf@xzXiIHJMc}HyzQUB53W|1_CQ#f9o zVBb-}EMFlIA_f0-yj7O5@tP!`iQ9&G7Z%RsASpHSSc>`gfLORO^=%b#7%nchoEOns z95JJr=Q#>;F%ME;0Vp3M{XUNE@1K7Yg>0)?WG(5IbFYuu;W8Ac)>XhxYYR>*2x0q* zq;k_GI}K0TJ^3{}k24yejk4qBIuiBVP5q-(rKQJuPssuq><2}{&+;;cee%a?*hr`- zjVXBYah;fDWJwM|Bx*8Wup|I-3#j1oY6ROHW~m>-LZ}I;W4oForU@pC!Ob}g?6RHa z;a7v^d*($YQxPJSBei|R`rVTJ%n*qPcxK`O28>^?rk zlS95~me0Fj?KQaw^cu2rhs%lY)(NYq4F7@+{{4BaE7!JU+PL$oeDyb*ueuFU)J>w{ zh`{WJ>=hL;0KlQDL2ywplM08$1@PN0o+dzY8>v`dFR2|l94C&|kj07G ztLirx$YlJI2D8ekbG8r*+TRwR@L&a9Zj3S@^kIk{L|6t$dpv2;WM*e26?mbr$*vM` zEg{15<|3x!8jOQ*2)vgkHr&&wJlNrNk)=;N@JeT*Go=2PKYjp+C=oSupd=C2hRd%J zp)yOlf*N+n*v7#W|Z&A@YAWdo65gwn}0*CdTYpPxzL~NOmPN(zEuHQBD%!C1+H{nx+Vz zUHlC1f!=PdpunfG-ei<}-*;GDaRe=a*1V|B<2D+Saxm3?#tP>oALbWprSF`wSYARe?Fpd?Qxzn&R*1-PAeg zBlf@d|9P}6ETvIEPpqwFJqcxVsy}rsU?!dA?oOC}v3-vqs;xWILX=&^er+dd~t%nQh{n zOE-%+Zj!RBPKGl-eXEL(vT^xLW=lsppJG`d0rA!}`N;qx_kA11M=X@TrcGiL97lMB zEct#8j+7Ro9Gc#V*J`}dW&{8b1Za}K7gg4CLPi)bA2Wp69xPGJam(HRzMbUaNApnq z{1F@`rC^aYD=*3N{_UbdVd1)1kP+Ij%D1enR}SQkv*G+wJnC4P^4P_7(k)ayG20r? zQGP9_S|V+Z{QEDQw0`tAQp6kC>QDaCr(uu}s+w3Z#HJ>(U0oJbu9MklX3}v!&q^2< z$oFwz0{E{45zd&cN#CYuE;4q$=5l>=KnbHv`I&Unk*T%b37mqDB~4fvTFJ9u&|Udz z_^C+mP4r7LB+_vJ6|wV(Khbui4Wkr_`!+VLGNvr2iM{P6X{8NEL&?$hp#I5){hQRzZprMvGpNpWMjcM%xq(w^axe|31G_uvHI!1;e) z?$cF_oEeGJ3KQPb+z$P%&Huqy%!D^h;Jm#8FG?DGSxj{d>G&Z^&qQsVY9KN0s z0($5-qZZ?elL zRp2yZ_lm_3#;CQd<3|+g9Q8V0=+xY2eVXd19J9OzodsLoC&ofYcUnz;g}%sNsuD|3 zf(-m{8Ko!${90@_Bllhs6$C3(QO4_YKG?ENLbm%dw@Xs$sUu=+jf7BUC)LD1270#K zf~D@|OGP7pYW(%|i$%{R5lIeMve@n&GQL^gH45i8QmteZC9S3UsmFjsgxF3?A+h4M z+*N!fs92+omet!TqW8tT)_`{=$d+5g<(qe&mhuaCYF@BMhS*BgglcnDqD*f4$R+LH z*jc_%9q6}S^=Saq9gjHYT_jUNsU93$9zXfGn?(9FD649n^2RQrK`sDu4ba^dT$uUsR5raI=AQ7nWC*%D=<*)Q~NZ*M$*uLD&r z{!SRF|LQO4CFJ2fgJmpyJ?}Ga85LuD$ zpax0}%$c&w^WWgaphR-oR?oUSib*&anN(zbYyVc-_$O;kDEg{{gN0boyG7n^f}c12$Ex;^t3X7^drpHcShL6@NBBmIA(?z2^YE;@Acm z(`d48HUBT08-Ct-nZ#__Ixyf!gvAdos=V#^>oEzA{W(M)FoR34+!S@IUd|M=S?4>by^7YN1QIwEvQxxlohc(chnH~3>b z)1IM>)zPaoC>+z8PE~tpXjY`p>tDM`*QI45@KHmN^M(P)(IwE+^(pBwAgC?ogjsjA zWEm>b83@ev^C+(pbcTcVC3LD5`b26MfL7ovEg5r)VC@+v4CM;2Q+`I^1`!|X z;@Xdwe-2ge&I$G8Oe?G^HWx;nuCut=e;HJ01E+)DzCeLewIfOaKhIRD&?2YlG2tkp zq)OT2hRPtq_@iunT3^bw@D`rK_eOsc(Ew1;x2s_ZHme0l!^}7}32P_JeRqk|`GV~X z7GfZl-t>NvP2#_TN$SFl=L)pshaMMa@7$ld8EOMy%%lFw!whk1N>`4GBo{jTJ<)Aw zyO(*A!pbZN=`yEc?H@+LS6LbH<+fCo!j^Q)+D?>-o86AJ3F|{G503K_pPHxM4sn@Y zjfhb=cQka-xRj@7q2FosB_U5{5k%lYw7A7|mO97`JW;FA@~rVGK_j6`t)&oETz2KP1Mqz5I)V(VUaD1rve=+h~LBy0|umWf6>y z!~w?x$Mn4TX}XYrUi0+xC;;^U7iyZCnL7QAwDp6wVUsPy>wAO1BCsgEVCIBJ` z*sU!ci2VeCDE1n7tuwFU()ka}P3sv7ju!v@0u>Q^(T#(pywxIY>9lC2exze3`?FrO zt)8)p#zeKNNpyG$k~2p;I{mDK^8IhZE535FDwUH~?@v7lu1~l*|6r{?RsgVFBP1E{9qM?TEhnWM zrYBv+%=oSz|M5qPhxF!JUtj_{>A-HjSg@!h?Co2~inMaL5IJnLw&4;#N^T5Y!6fD; zndg6DGSdir(cqJNJ6C$oAi)W`l^d4SEQ#kU%Z9LV#2+HOlb#q>%|#ArFJ2 za+hRNmC%_pULsFE8X%u{%i(Wmqxtf^t8`BaG75#fU2T>h-CXC(s;9h~Zjn?c#*85G zZyl#{sWW?oZ+1i%p~$m9^iBkE$HRSwK={u5YqLEl2y74eb!91hXdHT18idhJrAwx;coNpmsOf5w6W z?>%G@sp|aYIIeH;P2NT_9Jc;VO2fC!vF(A|*VN|B;t-|23aJUS?;hev*V1umZD0@J<>oI(0hW}B*-yT^q*R~am4nV{fm9TI^%~tE zr7cGIwaY(@Z=CoP#l5LL#G3^7(iMiIPl1Utfx-6;BM&#uK#AU5Jz)tI#1y&)MY7FK z^rWw5cq*Zfrzn4@$;>M|XvMOdO>4xq(MJ6di9cv*qky??2#wT7mg-w{hdt}gG7hDG zb=3%3eI|J|pH2J>3jgHhC~3sn!t%0ZmVQ;qnWT-40i)}I4Uj%8jV>=WTvg=T!$K4s z2)H<5%@_L7xySHGt8w*JFRgv@XLSJFtPI2%sDlhUe#0#;7{a#9NVcdf|MefFk>Vg* zxYo3gfFJrNFEI{4lS$zU6J;9EH*-QDD2GRkqH%Ogx3qMON8&N36PfO53{GC(>%97Y zl8VfPd4qRzcRgxg`Ts7r-npLAy9d$-iOVS?!KdW=pc7KG**JIIcOW$+Vrbml^&|@T$g1zE!e%3TnkV8+ zH1BF8A;7|yKQl=el*+dE0d_9(>=3;kaOOWs(*CZQK_ikyYX~`SA{oc@4+8wvyIFx< z^>52^i<_*@{IWTpvmuk$aLtcz=>*^g(c}Z2ugh@stOr#CV;#@hmFki48SJpA29q5= z&WHd2L33hq%prnx%Hp3Wq2{5UKt_L z9%!wRqh7Hfn}J^NpZ=i0LkrFg+G|w?JGs05<%|8KR-oWl+pUVPl@sjK!A26f|BLPD^J7sooy&w~|5^XMB=CE4 zhK^SB*X2Lj(ZFS~1Ct~{LFCS2g@;@Js3dNtEUJJOJIpVbaoY$&}^nO2OMCyiU z81KR9PL%v%2?Y;0pymtVH+0Ssrh1L~B1!;y3|d-`s{eVD<^=p%eG)OpFW*Xred5G! zk+&5<(Sh)bL`*q!6c~Qb^m6F#(^({xXf{!jDyJp_bvVCYil1$r`PZf!u3UhE{^%wG z5ln<5QzFoJ22apM=DGgn5Nv*j?+u5rv2x$QFN>D5c^1Rcv98>YIe7Hpt`%g{coOdH zTkXmG!2uI4Rpy-;8N61Rafko9D`WODv`;cy?yQ)fu8CY7(i%Ukd$(Vf$hz9hSs}2> zHR)ZK-z#++3z3G?FHAquJTfo0yb*V*09sD@Xa}YI#~&3r;ybri`Zafuu57-hbF$GC z_g{Hs9*k#8g7c!9HeSwj<%Imo{oPA*t4n0{?&k4I3TmOh&gkGO!N zVa>F(Jzf)SQ9OZLD?s-|{^}He$5u@MvTsI_YuD)DZT~?g1GR%~SlUXp=7^l_N@zu2YbeGF*xd;EI1XwauA5AME>& zKXM$TD%YAuyu+Jvb-;zWicx~~hO%9iLONS6qd_K*>lZjzVy=|LGHhxs$^!6g=%v()f%;0%=3xdFDm1<`8nxI95`On&Xi9sKZvMzXe{6|jl^gW!mvDZ1gB)|ltg3E=K2sR* z9kK5_vD@z-EYq9S-DHv%)H(UA74!0O=A?R9g@!vbCjs-WC8kRl? zuPB^Yd7Md34U+z>2_^FL?=E{2BjuY=U%FKW69K{El9z*y2BmW3K zk`Rquia4^*z1@me=zU2s```ULSr}xRnBGFHNsq#4!i)Kg%B20AnJl-S0Nt01olG8H zz4saEzKx}dj$c5=ftbzrkVzl_7sx}BhP?0QWZ00nF0@^Q$eB|+iIkew_ z_xMUmm?8)%i*TchbJx|Ih9?J_%suu}R5Ti??t(A>Cc%X);26$?FG@QXESejOZvU>vuVoi?#z~#url{oN=k_CB9YH&K$FAU9!4PBTCRTB`k@rj&X_hQnl4# zJ?fGYRZL6ocg?S|_krK7k~B2aL#-8&y9|qJV9V@mv;m3V#;u&es{jm|j_u^gHcRn6 z+-$9iyCi1YYWLCE)mG_^!$45K9vN*8Hwn6Q4clJiyQTQ=9}!$veV{;{yww?!a^f`| z9o>)Yf?y%htdBOzb6+NU^3%S!gr1B}buD!d4|_P#G?D+^UvmH?y;W8hA~kzeU|(`< z>`dbGmQ19})Sp?192BbB_ylJN7`S4lrgHGE(g+MR$$-Y&gSCcx;}g@|h_Y_E$Q=%et^wXWDYlpEdj@@aS)t2E-fMP;Nd2^4#Nf{Wpb>Gmonw+w0edV_9ktlOz^Bf|#>YH7YZlC87V+E# zcYuiNElTJqjODM}Va-6N%jVTwXM33#t5_JL32aN9%%&ej^zDGb<{k0T0tP^g^5FV+ z7-OW$e|3mCr)y>A{>XRRIijluz^(1w=KLLQj6uzZ5O~=DmI;{Zx_s@os4nnS`+cCy z#ee!E1Bg)KTGfcva#FHQuyjaJiI-opkWN++;3!}$$}aK=+vlrFbP_~5nQj7bpKa&J z!z+^u@>&p8GrfCQvQE8oQFd6CUVas?2~~mAC^?c${TG0{X6lWyJpB=!8v8N( z^lWwiF>P4hRJL)v<=eRQW-K$hj#(u}^^etqJ@_sJ86jF$uutC2W~3;!m@Awje!jfB0Bbhiz#b)w{4Nxt2<1Wl{>7%Lbt5a1#m2>M3(Qx-)Cqd8@^13lGb%1gP>96f-Lw~B zA0*|WFTjeRK0hYT#^eX@}F+y^S-8K}S#ZWu|K1A>7B3gA(534fy!_PZTROqJ?uipstv8 z2dVt1ol)l`aFW$$_u9WqK(AAvaJ2BxG2*O#fR|Ch0}FK&F+654ABg-1)1!EtL_mHo zMKaycd*#PE?7G_x5KSCFsKQ?%y4VPtt_6%{-P`N@yb00KVx{G0)3vm@KqfSWlJ7KCYbTp)PL{a9RO+f`Tox|n-pyb zV{MXECX5&Kd9oVl|MKvL;h0#rPe3O%6O^(Kc9X+|r`);RGXy>Pjvo7oBxF{c z8DazrZl}XU8?0_K>0R1+a-bI%P}LfsQ`m`SPVug06kDjrU6(2vWadXHt_yi1Iq+e*@ z7Pl9Cw{%p*oq*iX4_=wp8an22)D_xYcX@vn?-poD)kBng(a76t0!z&dP<$d>}5I`fYz{ zq?*Be@|TWL1bL8i`VQybVZ{29t!=nIzT0=CUC2kR^BXFQSC%n0X$}aTUlM4d z1DxJ6rl_#F77+g}7-h~yr@Wb@l&I_ZSAG+<_r3W^xJ?-b(e!p*CJV2&^8hL7d-I5_ z^lrq6>r!BV%)?c;lekL4;q=eW5f{_$|B-YSUQw`36knF^?(RmULAtxU8 zqbvQ%EtQ5xq7^-s>M^-4vVuZa2^LNd9eomrk@m}^i1?+sI4#5J0Z47%LIsR?)zy=} zJRn7!IsUFKJo*$QCnpn3n?UY265sHf=e7Kw|8t^%&TK1c5q1tra5<-Dc1G{@M^;ft zuKq*zad-tO8{i`yv8_xpIz`17KP$mcB!(7c+kraz(9K&@b7ty16?k&DzJjiP z8FwgO-`?;I0_(t+d2FV$E99BziJv@OHJC(C_bO_u%QyaPGnGZ*An2ZtkZT{*WPY$H zThgsowOj1}b^$j2q`9g%33t*NQu5iEUFqI2WdGGRXd%(e|KXYcc@re_K4s<0d5p*a zg?Ml5;L0Zf9G0DssgY7L0m1c@+C>296!BE9j^c;FJL4vyP1Rql)@k2<*=3|!Q_c6< zj=#PAI+ZBc1S{!td;Qi>qW38ek(lj04hT-czRp~-CXM0Bi3iMA7k~U~$x>f9VwM`g zXOv#aR1PQ6jkAf8w>^tW#qK)W*_Q<*s7^k|4I-ET}$mPJ}~|wXZ)vs9bmvi ze5w6{BtXI1PaVXh6n%ndAlc;okry8{T?Xc16y&;?6NS?1_+Esia`%>p65gn1j-t_@&POi-Y}E%?J!YoXz0oAfNJ>%OiM#W~$x z;B$oEvV;Srv2Z~u`K#gcRa>6Oc`<^*2RX8l5LUbGd<8wXA|ul+ljdhn-`_{S=yzvu z0I+uSr17!q(h=lfPZlLS4JO>d)*U#>)rQTEX+_5aBB@P=a`Jc^aQUEnUTE2R=?6_NFL|9 zlc3wAB)+R{FY9~Yoo(CdN->tr<11b$28EN(U{Fi&!M%N*sVOnBjkX z+y3HtU}s_r0)RrS*mi`Ec83*Kq*pCxqi@hTTI?_C4cNz(JTSDLri6~S>*0i6p682s zzCnKb!AGj%*I35+PyfDPU@P~^yt7uEGVR2KnWM^t&wus5T_T4Ou@WqDXixLf{l=6p zC+N37$Z6Meh1!B%7kF_W*r@U1B4gkr4ZVoMDDB+&#kEEho}wRU>vUA1`qFcDq2IB~ z&p4zk2ly5FsWuNg4ZqQe{=?}KH;%xI*sp}#PA~oV2QPj3_UH~}(+$uM*S}YVI^)_4 ziGSnNj{n4rF&)%MQUE1{!WNKOc6pdLX)ht9n!Q+Z0= z8e7n~9jw94V;$uLNNKj$Nm|qG86H)P+g>`6{JrBtR!>dPSelE>Xxj#QVWEF0Lj zQO$^TXlbT?kXIKEN=-*V99Tj;hNEZtZ`{c#bE>Mlq+i^_W^Y2G_|&}8%1HsnAwO!y zq+>Fu_jYga;~x4Rj?RJtH+1D1DB z6OPh2#?680hQ-4@@x*lFE;q3(jNF->0{s*HCV&T39{9%}fb_>-gIgEO5yi|;k2i49 zlbNC&vbTt}zx{;W{BH2K)8zLbh;1(;mcDj>nrhCg0E7sJ!5#jFhTe*tOI;h8ou+$;7o(hQunnnk!7^x zwG}0NVO`D%T5uzMX356j)6=VTLD0E6nJUCyPQe3J!Z=pfvErD{k>3(9-cD`H!`aSL zJ-h6AOFL*CxlHhLS#RZ;xDZ21twIZqBG{RK8wXxtrEb)aTb?X+*5y`M!oF1ND|co+ z0CEBcq_x6HWwZ~m_dh#hI^O`j&}4`*Sgo3E;@qY%61<<7snTz;9-L9g9uS7fa04-R z1xhBEk?UXm6MpO{)C@;%s^PvYPfa`Z)CG!Yoa7tm+Uf_!eXhS7WkTb(F!QV+z+7}N z!!ei2qNF8ee~%p7Pn$iPX3dJgZczcBHCwxu7uC;Xd|_v+h^Bmp0T)3nm7F(M&s%0z zU)P43I58zBS!PTcF!?ssp1^2&CmT!pD!Z4j&&(PL+_*1~t>*J|;n66yq@XSN;x?kF)g^gYQQZt3edYZ8(h0D`+ zM-EJ!9rOg3U0YQ%X}!IaJ0}8}jtjc{v>OylPpYTD%s!r{%gJ_KR49Nsmn~A8({-2w9 zW&}Tyr`cECa64_K_})0@ddT1Y#^$wZ85^4W&70^X+&O$CL}KqqXwtsv zviH73Kytwj)E3#Spew2p^6zLznGIS}2ro4eT=YIbI&Cvfj|`dcA*d@EK@dD0ixV#+ z#jBf+O|V=8gskW?JK#jjvnywzacK6H;mT1feQzcp&jqcrEee7&!9O*8gi$ndZ+HCvx>q|MOsI z4(|CzWP4+SBR`;HyitrxrIA##&qro_rW5uL5dlIsIMX33E2Dc6OVOGdEJEZ~3>)QG zrd`K^<7b>mz0L9pfzL$*Q2A5S(}rUkIdJO?cjy_Y+*;2mMS;C9J^d*)DbjO^q!d8~HA+>)JTj2`31Uxc`JHt!rQHv~SrsbaJ)*ohja-r1n|4 zb#6;AV2T((=Pbh-A3j;8@e|LchXPyXP`c(rolWx8fAt3d&Bi&K%i_SKdlWrr<02$q zRccgT|E>LF8nef3Gfz-9t53~R4rQ_dPi7ST-~1PVb#Lx7vdIhotq>~uz1FsRd8#{; zq*Lk?6nhSGTb0!;=Ao@}aRS-Ufpko|Ix9Osb2DR;n!p=%D?3#UcD@07aDL74m0Yt` z0W8XmGck|AYo$=N{Z^!_yIOxT7{Y?hI3u3nuvH#K#PrcJ1p5Prm687ntCd1jS~BG= z9UWEGxO^~fbCbcyy+wEeFCuabyX`Cuz0@4X_gx81KbHnGBaRs9)| zT1Z14sFFIJVSL=)MwhjnhnyuA9SM9~&7#o!CAH~?{E?fbOsSgy4yut&+9t10EX0UkaHB&gRw){$)JP`o?4u0f@pM`~k6?L`My+Y{@)Rr%KUe=+9})-J zr1vv0bpzyd&2+*x?^5Ts#e9W#-u4(mF-N`8?JJvCtrYlcusy#qAU9PJ;I5kSK1uNT zu@)*L@I~~Q5G8jXoxf4%^^1k?y-PJT{O?#FM1m7%YLo%8S6jb|I4+lQZw7-@+pLo2fo2#Pci za>!v|LXcaWEoyD8ZXNBU7z*O@p2iGFh|)YXELyT*&({k;*;&vhSANBwLB;0&K_hOb zJT;y)PA%Yd4;%D)H4FGKv}DBMNU2brR9+iATrYM2%tzdsmQgr#nx2Sl+()4zcaybe zF3;=ZANCp<`B&eK7*^G8-`@c5{Lf%$C^ltcMY}$ek<%EnD(7+4asz2p`D<><8rxei z@9$sGqb>XB-zmQS$*OtB@7!*_kA}u$Hfhw!voxMONiAEvjOZ%|$>jxf40x4~@vuYa zXV3*uf=dUa0Eqf2at31IC#s&riw)961yoMj4r()`>A6drB1=)x>EW_Hu9x8XDzgP+ zPeXnc<4zhSaI?qYDI=A`m%v#L>&n^oUp+~mCDPqN+5 zp>+>iqaW+YO#<3Bk9kfXKu5fEh9D0LRL$~?vEenJSx*WfohHSmm9pdP*5sacwsQ#gCrLvAzk$U(HEn~Iu?$jMV+Ya%1< zFfpn`A0Nj7=7Bs*u@R>~4K*YhK4^&IXI(_%FB)UjxeJXU%4}R1>4TJ%+vU+wI<+GP zYqas3<33ZZCJQnji5Cosw-{)Rd+N^1!u0dXGnEm?(1*3ux@-V|dOT>Ob$%3sFgyiQ z@v`ZJFxtKOknp_!uPA<;n~?m{-atTsIIP7guLR)C|5x4Seb({$^KSLMwGaYWyHr4+ zqxRSIHz7?Ips2KgHB&@))5OiF=)4cGk4aXgou znn)ydrK;ydUTNzL8qZ?6Kqi{VJO6v2}Co{EUKn z)?_mJY5OLBCx2ZPo-WQYk;7;cT=*2WB_|~^2Ea%+YLE__1R(^KezB4$tTDJz*t zSz3r?)m-Z-o6XLuU0>qYYHUD6DmKi?&EE*)tVqeBbJk9!5M@F|vIyv3fL+;>fUT-CB#t$Wj4i`_Ub}9p~@Qlxt<{=wA3xsn4-C z;i44Z3!p_L4vi-+)-(y(Q-$J2&fj6tc=E=?G2Q(%&{eWgzAv-wd#qSLV6{#hYDJeu zAaMux)RpXVCFMiq%?Pm?WUU8EW@W1aQ{1Xhxtfg)5r7VjRnfXTf9?_3^`O`shgBuR zyjCj3hqp&M{h#vK8gT0X9C{gA5Td#AchS7Lwk+r&sh^uYC)sqD;G8Qyr|ac5++%e5 zVp@lhdZsfjbk+q=Gk>8i!igg1Z*P3s&vx}i2wfOIPOMWFLSo>>bqtdFR z6Svpao$37QA+_{xIQ7A}0OVRcQDkZA2)p6z65xoUA_8aJm^h-vKmFhL|CiZxPU5BN zw1r6z}iPvXg5ze5T`Du-Atgl3<217mP8SEMyoyf4|}`3dYI@2R|I4tRh@B zci+C;@3DC}=<|+nhzq(N;56`X-w%5g!A2s)D+>l1HxL5_hJo6NXZ^S2Px5G6pIOoG zXyJ2G_v;=RUha8gg#h7<4YBVF?e>IWvs3Dx1N}mu&xB(IQW|HHqzEYJ_E8C(;C1Ou z8!yC=vk@SR=mc4#A9S$>R8=um8DfZ)q&|JvFOnp&pJPIJK1AE9akHW~-Uez3<0#f{ z?mqrK;Oe`@&(zds!0r$+WX57v>iiVn+#w>`DS)-RY$F>GU0JUNgVHlHPCBmHGuzeNm zTrZ`pDdT_nQ}6wEk@Cah{KaQBqXST=eT*QIW~o8N`~Dj}4!u$%6Y6aVE33r%R)Fz0b27RH5Dy;_W&daaXQ>6!8xu9cJxX?CUhg(Lz`z> z-}Kx>*JJzeY)27S{)DSb&F$WYW9|qX7#zVqW0kj878W}=g-KMtqc5Vzcr*j?z!>vo zK#ue077a~3bn$Ff1j0wJcM|@~xKNz^7H{o+=t@E^K3NEUKU!nph+bk3?mijL7u%$B_^MFVe{o}gQT^CT$^u0=Odn}($X+ra zS?#i$%5WQ!vVxEmY2JW<24z!?8x^o*enKPXV4#!Xh>p@;s%$EOlAah_@nf^S+W(nZ zpZ-WVnXq}=Q>e~wweyjSn5SeUM_o^zB570o;`7RyctJ$eGy}Rg-*XKSXR`cW{w`aR z2O~oqS~BJ8ueUJ;g(6hE675SxI)9~)soc~ovrfc3B+>M4!ftEH&)#m_O!f$Mld0o2 zLTI=+>_-@i7)-AvEu%^!EDeCTAB+~LTHVh|3t7l7F=Iwe033Qvdn?3NY}Z zto8|&_>a!8+@7Oj6AU(pqCpy`Fvo*Wv2S0R7Hx_z`)N>1DisCU z0pTpCc^gj<4e^k*0A(3{e9Ws_WGNmCW4eFFPnMD+={8@kSmM+xzJ{wBI% z+Y@MR?Fcs;wMinVwaluw+bxYJ(><zS;@7_ayX#q1ZLUCa-20O|=;`2e`mbnH zn~nWq`EQ*Q4gHb$i@R_P4aq!uH~<1N$3T(aII-p1h!H=E&SB& z6lx)N=iPwLQ}ZwK>d!%AoR>eVVj4nLy6)N=YQC_GIcY)pUHKTI{H6=y*;OZC)zl$jcu+HfN(BfzDXIouoDGtEmpJJ0@5GN$kX*4LS z(uBHD$E0W2+;t0Trl%IYprgMGIi6^?1yR8OjZ?FRzgjnEzLO0d?%_JkM=0G zyGot%4qn-4VqBrauMarbxrHvV^!5~UOTBbvur|1JGHvc3(EIwRv zOZL)WCd=8=z}lTE!k)Do^`l6l23+aHjBxPCY4wu4)^erV!nifxCAWjh`iD^++!o{y z>sR{xq9j^{o_D3>PXwe{<{W5yS9=0%i9{(3Nmv0{^Pwy;uuJB(Il99MFn|8Le?JSx zreT>OCel;Yp`M60V=|a%<`q4!tvO*tJGH%Gwcb7!Xq~7c>v3}T|9PXHKY6Y!d2o?4 zm2AH;_^V(^8UDsfNPdO-zPYs}wmoD*Qipct$XxNT`;{BbPhZPA{^Zl$a0Vkb2mqao zjHW;>pB4&g7lgM(i^J&J(Wj=`_PcpgxQG2i5j?DCyHrgN9tiGURBCkv#w_YInB%ZH zYZ*PSFcE|imHERrdvnS!r&4+SmY!2)0!yKoe$n#UV8P&S;2Uvja+t!bxNl>L)s}Y- z5{8r$>$@?IQ1}Wr8Wx4Yd-oKHiM-uN(LRnC@ct+0^u zB?WRydWM{cUyRElOc_G5??E=ag-=>NXAkIaqe@RJv{&y}#LnI`of7=>UWbiK z5CxUA{tbwA(;}S`>K-_qErCZP<`J@EbjnvKu%0O&f|g>|SN}z%flQ>JNCKA{b6O?Q zb8}bE3`tBzNQr~}HOn1mYCe69q*%A^<&0Z~5d zq=OVrHr(J%yFb5~<*7FP{V0p?KP#iPhbvrlhoV ze^|u7iMTyn_*`dekyBP4n9MXVU%6|SQ(%%VynS9#P_@Bst1s6kRI1b~%beiVagQ77 z+~9HU0gFns!$>YtLDt%`^z+tKeJ>MK3@?)mJ zx|qS13<;{sA}yx@X5;Zvy-sKP2e~E4+s?JCZL*;VVpFwNz2i6Sod>Ugfde0lsHUdFA14dniyicujhR57+q3vd*t_#4Fe=cUB+2 zoG)p6cn5-fU#)P?^lIF{<{w-G8PBiofXs0K-Lzrd@#9lVM&CF!?s$(rAXPvAf}^J^6_m@6|TBCRV)NrXCqBe z@UW<>>Zam+@mE4P0Z`y=HrGcF>lfHVqf~T3{pz2e&#!NW#i2wS9r7NK`8KIT00jTa zA$XVy6brolWIVDx$h|_#TZoJiTW)?~xzY@K&g+m_@snK@0F-TaXlWU$F8k{_8{a;H z1dR0wm;5R9oxs2(qhzgb_dops1I5a!#Bl0-a)*SWHgsW^ zD`N-5nC^I+m)O~&3+Y0gKsXMwLf+d&JQcul!;hEAa_=DQ&50G~lUt(lc6tiR(z^h? z!)D@YR)E}{R-X|S$C@vdDf#nNmr?xv-wE@NEoq;dP9OXGu!z`2qisAbN-L=K}G+AYnPAEQ2z$<+omD=68H|W}Ry>E5u_*MS-anmmq#GV&X zP>A6YKBmM6#TmGVL+3BycKhzb~X&P8hxu zWTTb*lVTo!Yrs?MkJv<#5|PN-k#HswmsvfzPtTRsd)BrWx}DwA?Iq39JR6Y%;JOb_k!U%g37C@>c0d>DVp@*cMt zH3N0eL1xIa&ELQ~CWP+XG4mhIm5ejZxFsgmx_RIC)@F!r!qsq5NV^!RU|ribXnMfh zv_F|hMsghhbL~MMhM93v8;#vRAqW`}q;)SkoD?BD14go##&pknw{^unPrK=r4^P=o zK?&7`o-HI%+RMgD1Bn(G?0rk{gO(~ymB$;^VleVN3(#NYixt(R*b8jB6YH^CCO$xQ z%+~qYYD{^(U!?BufA-%Yu(-nfgv3xbDk(g=wBazgx zbj@qridD4gIkd_01Czupt(*zP?-SXdKw=L83(~X#7bZRd#|@FL0=^`v_{oQ7JvJ>8 zP2*p#-~3XqhxchW_w$@a%0woLC?<<%F#t;~d{IzVpbndp`@`42oneJ^&LXb97P@$# zH*f!E{jR?vKJ`6sAHPv{Jum+`-fh}uB{q33Z*B+;;ihp}$4pTz>nAjD04Iv+mSrG0 z9!4aaj`x%=b(N8Au+7)Hml5dYkyXy)U{>^4a!=-*gF}rmTIC`nhZ_pS*BI8&M1kTnaMg#*2bPsa zOFu0I_J)(z5UBQ@xKWg5$`WGKENO|COJR^gp`q2W@JjL_(rR^xGxU7k2u7`*Une#v zT72MT!20t=*NwjV^gfLM0?+^OPyeG}@d5q?{v+`KjqvqRkhxPVO|e-iDW0zh4n1la zr0m#cCZ`BImMUEL;Sc58y$OZae$>j`9*;Hi3EYhU|5vsix8mfV3VH?KTRwM9XbE#= zUK*wU2@r?XgRis%G+Wi(N~a_*pgqpr{8 zPkb^P!0WKh3wLBRJbU79Vyb_{s%u4y!>TI^8E8#*0AqR@#wLEHxeM?JM85 z0U^|b{PNtcyJFxr>~fI*t%TOyXnN{V4Y- zn!GS*&na*nN;D?l=M-0q>c&ggNGDj4+cBcJRD@F5P=r5EMCpX-MELw#Lr~A|X9^dR zN?S1QkA_8aC~cgLmu;&_A3NW!{ChojUUvd-@CyS3ytM;DD)ll=WfI>4Iss5ba`vq8 zl{H+fBH@^$lGzZfsj7JCZ+pgn;Y*_WE;Ql+I1QpZKiGDrSyj>5L{h*-0a8YbhM1l& z>UU?gcZO)D-{Q#4QOH;@ljGcVp;`BRcv$c*V5ZtNk&$Uaw|s}@NJwb<<;8aJZFeJ-Z(Q}rwT^1DEtSO3F=cd3H-@~-DMHF+X zFEz!RB_AMq5VdbtI66y2(ZrXZ1_%P|FTT$#+1&TqJN)>DpnWiShtvF%noqpGpWxR&6z8>myasB;W}y?Q<^d^Y0+Vgxx)8X((6OkTk}h699$glBe5 z=t`Y2$2DrFSZDflE}G2J67;$vU+teXP0vZ2j3p&qn&K2s+3Zi=KBJVh5d7fgMgE~l z9G=Lv)}~Epc9H2DL0rD&AXb?kLibUXjyYtJifKLV_2!XYc${rDGV{&XwdLvu9~1^b z;C(T_r3(;K!HN=Bk7+n6)Ax@eC!+Bb{y+VX0l+6t&69lzL6b00sf_}^yrHk~c{Wlp zV{#4GP5ftLT=_%PzItmb)ftN(;y_K5tG0K%6I6FE-egKbq1RF?;nN%9Z6_&5ZiZ4i zo8*U7!@Nwcz@&6D<8;c%fM7V2mJ?!?EZ3I0(I4!tRe7g6P?rhYL)r)4+81c$ln>QQ zKkh=qHx{SJN@KWTF-+Og*tIHkD{>?!h_Eo&L$x=Ox2oAsylcykEiwZFLLmSOTwWrY zs1)wX=StS(-X%9|!N2tfeWFmQ1ltKb(Az&M#eiMuKtcti#y`xoZN`) zIfjX7FR(7d+{^Je+)yqEA z`yK-|nb+{a&KKTcV*-z#L?#(to#es!wde|Ew<{*wIDOUc#3h*|ejPsN

^w?~VkE)w5jseN8g>XceOiQ%@fx1m>HKfn$5e&-OJoqb9B?SDS&#Ps6 z(e8j?#A|nprErW~zdH5(I$Yx=Do6Zk%lf`Q`2#hw&>Q|1ju2!#cIxxg@hhoZgHIC3 zD7F0GB&#dW|M~w4Fi5JX17ao>Rx3wL1u-k3z{Crj)0{EN;Ls}N*Z%OIt!t?sAi~#d%JKqN z1PfOw)5_9H=D~MA_%OG{)^fvpbf$y^0I$VV?}t{~FU#W*OEAKx#Uy8yBe3ZjV_T{6 zWY+*vXQCJjk24Gx)v-%xR}vh7j76a$TF^2gczu75dujeSI3e$Vs5o-1s4Bhuyb{dH zz)}2Z_)^&CkVhl`d}HwP;|u6{-CH-B-hESuj*&Iz`E9A!!2SI!Yw?Br&PuV{^Cwtx z3}7SEno-<_r^^UUZSql+x`+joW5!Nn0sJ@rlso~1OtWi)D9KO^%JhC(F~}f(i&vA0 z1H1BI+Pw3(Nhl6QWlOb?U9hzXd{AhlKEOGVldZyp;B9nmtzr0E)!`DK_6U!+$_b;7 zXT6pu?zQ)~&Te7Pdj7ubg8_EFSvn!dG2A4ai#f8X`HXx4!_wXS+UMuLR&;nnc%zb5 z+_e664$bQZF(#?(EjO=}7DA6AD6x9atJzE_#IBh9ptH?iXscAo=j>rw_?A$7GF9q% zx$k(R(>rhuf#+HP+u#{NqybKY(0}^Fkc>?Y8_QM^M(O3&TGmRsR(zL+8_;)yw4Wqulj7nQe3uhuV1tTDdlenT3Hr;fTu>HK@G z0`R;;=I1)3|LNz3l{WkOwlUl1qG&ht!Jw0-$gc!fr5o)n5Z0|irf z?)~;q=_wCLT5$7kZs4!itbzVkwG4WkXeVAe4fU7B$vlvJThRU$S+{#vp#Xhvihe;5NGG#kbzg+c8F)%S^;QtLsj zMLIL3lqLFV$po@i;r`QqFAA8zFiVVp^+%<2LSC>+VR9;7M3ENZM33Ry}9;JBu>Z@m4rtu2eg*p#zI`NqkD;Tb4J}Na#Z}gRRNNl?2GxXXNB4^@O2` zY0*;UR1bD%D`pCoKkxmzpK?8#rLJo#ys1paC@f{QY4qI`;WB$U+>yd6Q+=(F$rDkF z%{O{bUYbQJ%3kjG&f81xTQv(MMOljfU4v|$r^TEQ>6hO)3|#zm1F`8Ar$j+hH#Ee) z-p^3Il!L%3iT~CA9TDzlo+%1#UV2FVrEVJW)^^ofNHXE6Jm7;lMr&_iwBImHz1xX@ zUyVXVC_|U>7bUthSaTje8=EBsbA{UHm)eXfJ!mQlo&dx8FT)y0ap-x;nb;RbR9mzb zy{9b<2&-F3-|U*JC4c0|Ypi?y1xr!bH@~@kCs<2*ph7Njz?r+!n_!)W|9Qvj{q zIJIPrKyZ9{R%H|~nFzrBpa<>Mvk_ggZ#I(qc1afP`%;uc^@xX>#Ci#*BadOQZzN2A z2=vk&#Xw)aU&-W~sv2@W%>}~*<53X>RLxfUwAgGAXVC2e8n^(CzgxW00Q=3YN<&xrFhEkOh z2v(Gz#Zq6??|u8W4o@@QAZurMfUiMDOhHMV5%|);kI0OaDs$Vx9#AUXo@Fef({d7Z zWhB}Be&s=SBo>8A-tHAHm9p9E>^51@QLZa(9dkx|lHF-S#ZetZMc?GruMIZsuiu^opN_jYE26;I zOt5c;wA0OV+R1LgWiz$D=cGH0RopC^(>hhLZf)HRsWObk79?bHaYz`}E+yKVbPy|~ zrmGZKef|EImZRCd&iaLCe{pqCJnj1rd1u1{NWGyzUL;SShQ5%X-fy z$zGN28$b_v8~05FKPLUte>WKDL8qa%&vY&w+MzEvDiy2wXi!$`cnwN8I?13U4NL4e zuBYGy#p7qAeK)(#DQ)ddo+!lreUXQ0kMq^Np2<*9 z*vzM*;wlljGxxTw)KlBl3H>O{aiM+M@v{E6xnptjuZGEo2U452H#(3ga5EYQ0H(CC zrWo=~0z{-kMPs=oTu1D~id*6;BZd8NIN%!5-A$#jeTfEsnkVyhQOJpyJQuWQqchTq z`drTXqFG6 zjO&jzb@WxHYtN0C=JcX6`DS~S%pK#~tf*k+ZGBz7iI_;3PtC|ACmm#0Duq00RuC*p z@-&wu-+G$~;KEr6H=;{4S4BSR22KmPr4f z{_pc22y$Z$(+GVcQbM052@rE1H) zLLE}9CUsc#`}rsji$2V$ej9e4&0+g+U%@#Ml@Lf;XKM4r7)Ux@FZPKx{0rq;L0Q!# z@Ce0^q*Ee5qoPR||2=u<90;LX5Nu7iTG#$je2;7?QJNDIEU zyM5a~A*GZGYqbLie%F#9B7Ju7ZdxKhNG&Ae+Y2u4XCOdO)l52|Z-*vb^-o-d(89>T zCCk-g77Z@9r)1<#{^Ji%I>^%AZ?-MyQD*|V2~c<~R!4u|aV8wd^JjH3Uy3p@O&sq- zANf0*ja4X&K%dGI{W7HjQXsb5+Zm4P+p?C%%9}V9?BQ)hBjp5&{@ME|3T@xi+?Qf( zrYyYC{(CqAq1Ns5C@1?6;Nv*JJ17M&F9CY)5=uzK-( zU*R3k&f_T6A{x}?%fd_G%Jrs%%oNjbI9Hp#qq7;+-d=uDzjKz%Cc2}!%5d7xWu66+ z7<8<9ooiFj2$byjwgh3nHj6ccetX^Djk@-cB5MnB9AzHUqf>{RDqj{;F)=PE`KF$# zl8=270b^NY`_uPgjZaNBGS?jDG_Jq;CZf0z(+14+tlk6#)>`+`Xdm=mIJz}hDJL=N z0%4G{<5{^oqYEfPXWQD!%Z_yAb;r~n>TgS!L?LFbbH+(+28dezsvy1R;nv~u5o*bMD4;~)NefzY{BQ}akn0t)?O zdLI}RV@y66f?>aU8|a-2ect}Nz6?gIC2Fe`v?DBxAq zG*jLR?^d2;8BI+~rL_;AA&|EVZ7GQf-Ap3*OfbySDi-6UPW|3EtCb5{J3Jzie8MJ6 zl{yYZJG7loP}h6N#Kah!K`|q1ti78C(@(B+1{#x=dVvzYzz%%NQ{K;Vou!M`7=|q> zD&)>mMF42SZy32pZk4`T*t&xGH*mS#iek^ zA?@jYtG^rp3Y|opEG5<|n z9Etg&x0C{ZR6C4_Szkzazo}A%K}Oq5rg)MjOq8f!ab#D73HLFSYpg5gi_Jb-tW{tz z62#;7o?GeH(L!rGXo488A*74?_y}p#{ChhGRNR{#HK-!#&~~#V-+C|ZbC>cZamx2> zPhIqw|LQ*ydVyR?7>S8V8#E^O0XiY-ubHp{5#kP4Dr5P5+E*BLAO$UT9#Id5E#`LOe6W8(LZt)85@!f@d#U_-DE}cd{9@w@X-@XbWR)!H*^ufA0On^$)~H-n_XTK;g>i)nX5~#9>Y&r)we<4o#7*+!KupV>gMsN} z0N5Gb!X3`t3T!HiUSJTV0L`8zRa>W+b?x?$pU$k^6u1$ywCfi%nl80$=zz_;t zvinekwuW?S+XMq#i?!W2<4(J6>bxyr>vR_+j8buKH}~ED&=+ipP>zb4W1e1Zd_MV> zwh(Qlx&Y(BT$Q>bidxtlC zk25#ia>NS&fXXbEq*ufphH3p(N8a(5yad3I398#fk{H<{FzJ1ub{bvr1WEoVbuq>} zKE99Z_KE^a{ro_obY7S_a`T2@?QuT_&B_z~wZ?*CvEJV*G;gqlj?4ggT=mF(UjBJD zfXx1r&QuqiKWf3B2SflR$yv2*8nXu?{@Ag(D#7HVVB=R51VscPygbA1y@EX~`WRh= zYKKYZCE&ebt9>||>!sPo_(>nQb%|lp@22$G_uy?D3EpgQ6K^FD>E&UI&;Rhg>tM4d znSAMU!OH>9yUSW_OO!b_8o8Hy`Hw-9a*n~pwZ1>`ZLQ8VLsYGHqM4(~NGuPSNj@ZcyQa;y5e~Cr}dv6^Kx5^cYX?lF}0-9SgDBxVJG@2Ik~I_`ut z(f%*_Qlq&!HiAc9-jUI+G7H!KUmQXs({%<{IsnRL*)>{f*By(hDUFrSq~D5Yi80|u z%5bqIPdVu>$sfN>>4-yl?-2og2JFrrvKWHB`tm4P{g=|-*11vR&=(c;P+L#^e zAT=HGO@G{l*NK;&$O4*kn#b`O1JAykdDM((^m2^~P@(pV=_X!#OHRt)R}5IB=8ghOnLi@x;vmK1u;AQ?UH|2$<*9ND~T5=^L?H~ z%>i2SH1X^I!+#e5T}n2uGA&lCaA@;n@}7S&;mxm5f_S2tX4{+`KXuU+gte2Tj}99* zJ;5=j(b(EZL7wpXo}p505J@x?v}EcTP1&Cqy3~uzfPo9kB~L8H5~>H1tc#wN{sm%S z5m>0y@063B3!P#7u-J54W;kMd;uEQXL+f(2FLo(~)9<6C=$8AuUm<%k{dY4_Nja@+ z$EQpFMW=_thK-Z_i$Czkbo;(WQN9H;j>Mmx10g)T8RV)s$|Q*c)0lF2j?*X`Ai0x3 zm4vyCd@v>&DjhC(kM?ttPFiCpZ8mcU_NnN+yHOXdQ73;>WNsO$aM}nbSrFVIbM-^j z9%q$ZGE3OWH_O)+043VEfn*Y)GqFg0Suw2U<+nm-!e%>~A4eIj$AsiQy~R9bJ~}Oo zIqsBx@fEG&iw^W671IjQIAh|91h-#1XCm7Kb>weom`5>KZvxH=3cNBFx_(S0jQD}5 zpKsfYfY61%!8i2S0+49KMJn|*Ivj5Fr4gk>6WYg;KOtPSUrb$9Nk67;@zVkwd z9kmSeQE*yj^V+d3h*!8~CgF__ag#|2GZDnMqw4&fwwr+L(d9~ov+fmNvvBe}$=&k8BPnE9=(yGw9 zHO~(A>4T#)QXNxrTB1N$eOw`jXGdUoKJI`TX%gQ+J6V38)TSI3o6!_nICCc@#8 zP=D};8k|-|b>aojL%6uMmDVG`ZQ2YOc@ih+8mmVuUvR0nJFJ51&mL+N5~}3$z`hy` z-l^T1sPm=g_6dvrt^Ausud#LS$3zalPCF$jNs^UYTH0V+6={dVqFu4E|JDBq0ESU< zQk{iZNR~#o%>>#3g%=4V6|4SeP0GxZg4VY0)JEngZNkQ$rh=K|LXAHgzAou>LSE7) zh%79RmXukgh3LV{zgAVN?|E-UeHXO-A#Z*R>uU(BGLM%{p8Lg#C*5Sha z`2oYnndOl_PFJ6S&fsm=3{K-Hh(Xr%UWiikkRO{33V{j{zd|?Mf;)!HTxRPW!;C|%_eE1a6vWQ{ajdHbVK213A zc@*;SqFQOy`TOs%{xRq3RMG4^xmEmd3<2!eX;{1Pv9^0(NmNz3$U^@u`t z6{;92u?Ngef_?lC|HDA&XEhBQYWyh`+OZl+>e;CJS9xhv`QOy274E;&92fJ5HL>8) z66c^}z}cr>%NH5e{(pQwU1~boIGEkU+4Bu`%%jeZSUI8%XRFfL7$qfQj1b(^ID@d- z+28r!u9cEJ%Z#K*Ei$obzu0lYf5B6oxnm8p*k1w$fnV2xoOVI_oYx>bwZXN0okYM? zUT{RJlj-`-z<|yyoqZv;l{A?6(rww&KB>_tiPt<|_<4zYC#`(2ok! ze4m*&U-+nWM81(6E8TsPcDNZF9Mo{p-biKf@n&WurJlS zO;BK=aO2mjE2zYHtmjOfGU;Ss3vx>%5@zpX|K%^3{|>}K;#5#)CKfj|)AQ#%RGsk0 zQw4*q9zm$KJn5**IJ!V}74MnT*pLw(hTQUD(uvzz@KNQ3-kYjmZw?wq2&>Z zMMPhXk{&h(Y>$sj&ZU==j@^iquQ}WaF!PW2uu6wJG71{HGXSdFaDrudB#kx0S;H)H~bk${H?n!+& zMuh0Vr6Ck@hj<^?(BzkSBVY?VPWk*FW z|H$JO7Ru}o0~W@fbbqdlX_V|ulr-;C6fs^#9L}=`JaD4uB|<|{B#+I>x*FSFDCTs6 z9Jm9+cYHu5Et8G51CXssd8}M&6%B1QQXl)lBw?nAZ%1zVJ_K!7GfIEb0TNc&L0?Tdq9b!Q!QnY%k&x7|)`FhZl{;<(gsB zGW*WU^c3a0Xx6c*P~D6mKB>HCS@%urig-|+dnzX4>KPu$ZWS-xlv2_e;+k7FCtksR z38^a0zG=gPLbrfRPRzWj4DWUM8#RU2Rw0&LD9Wv|PgJ!&-8|edtPrdHn&E7&lk&-d zP2*1}bz(f`@%b+r4b6qWvKtfEqEVOFsvo*XL>pp~b^O5&4=w4N!+IH!JvRm1mM%__ zj#21!jnXTMhy(k%MlApF4*O1&vJ09RnwJJEAqWXTSS$afb!5I2NHwHH4k(h|R+3vH+ra1|iC;>~6zgpB|3-VF zQIMY1Cp4-hf3RIrfz|p2r8LDX{bz(VrTwo^Lm61yJoU2cts`p+_7QAiTm$h$X!why z)Om&=M`0^^tM$Hjt0(nWjuwu5ewth~ft&H4R^~6q7gnn>$U<|vZ5@z zW1`qQJ_Z|fi+JTXY+q0u z>SxRqFg5kD8Bfx4@gW`>C}`ekXPOdUgD;2ICyNLAAOFJu`6qJdh)>mzh~+r?4WnbX zHNl8pE_CQe48NoN%&Mz2D2C^z=8=)^$`_kCp3{VsB61lHON!HI2*~pRO-lLd$L$-Evx>st#fllu?8w9S07~T1>kLwp z99RRgt=uG1`U!%J9?Z$?<|d)i-kT_mId(0pS@NAwuxyeJp)>bmCMN$Z|9RjozL#^Y_5PHjREAs0T{Q(3GWH%AakQ2#{ z7AZog05i|949JT5fINmK<}Vo;P(C)0-fx%M|62QFf`RKzM< z27~9kuTcuJzL{i&wEVi#*y=@{WDZ|&3*TK$G3Ygtm>ed2dvlOXZA!E)J|L{MF zgke`wv_m46(JkV_0f=)J@dnK*4;B z9~^)F)IuxduiHl=*n;lXdb9pxjy-xn2PA!&cR!i;Pq_Q~C*4L!Jxf?e`4Z1I1v5dv zGE%8FB#;7iD4)4wgdZ)ev}EkLMn-pYsrYWVil3!EED`#iB-5+#6y!~_ag2JajvJw6 z@NVuB^uj82S6+rMJ>=&033sSfhy+-1XgKhQEQE@UhLGK)BxQOq3U31JyTElPRbe?G z3nVJLu>~ZqC%wx-bVWsQwPOKdSfVisf2Za@t<{lsBGdg%chje9j$LSRj;?;{!1OA& zpUVps;RX$S%=Q22N|H2}-OEAW){!&5@o@Rv6MNGt;tru=>-f$H|J@!1a|S8_a(>oE zG~P14KTkc-*}LzvEVKI$|BFZ%UQsz51!8e6L}Gs$3A32cO@0L$>l5*0i^>Bdj&4$dc0h5y5BGaDlS0!CnKhM91q!(1{TqZ1E8)+^E9bMfi4`)qu z<5a5#0$?%083Hp55iadC{T}v>r~2VD7`go`wiqIygGltUDfhx7h1YnD>LpA%mymXm z4#vmDS}*H4wm+mMpLWDiujh>Siy35%?efKyPxYpHPwJD<4!miJ0mu z&Szrg- z+Z*i8+YPFPEj&a=&#j(n-Qem`iQ7Hst_G)8``j{DAL?x(?>w2J`x)}tj!Mjkwq@(Q$hrB z!rY_221o_)IAxBz7Tz*E9sp>*w$4#fFEThb7jQ8;xs()YSlO_ECxjy+fh-}vg5lrzQPz{l9mYD@3NX7Dqv&-%kxQO ziu!)0?CVjZ#9~50|HP~e5wi?-?`4)xPeXgC!ChI}h=RVPYp{iTPd;4&(Z~h{Mz3ZH zYAp&S`Y0?-B~1|;@J)Wj>6xFL+JwW}3mqN^oy?_5Gw}{S`D*s0#e}SufE&3$@cANI zN{y5H896k2E3S5*-U1Orh|lYIT)M!4_2~uEs@=8DOJjXKd+h5_=U+ss3fiSrV&@Vl zdactLb-vHNc9^xlQ;oZ67vT(~@?YR2TbdLVQzCTP;B&;8=rV}CoY%x)sk$hNlUyu$ zeP|?DZ8*h$CgO1V%K@-Rk|D2+0t#y?p`4Vy(oKP~NQj(rBg9_B{Gh&ky`3ebM<8)0 zTT`e*B%{t><%|dAlZ|wZ#2EY5x3SNRLPOFU&u4b&Dr8m9`WHgosW+b|STB74Knn?9 zTZ{t$i~^etp(!l>f_dH3Xx#tm|9$^|RZ7E#iZoUMC|5&hsvXC3%dSvwgT(aS{~O1{ z^JWuT0k=P2QSxC}^P$FsAdw3>Gzf+q!cn{dS=Pc5i+uj%>rbT3I5a;)V5Ly8P1576O$JCKkkJK*pwe{j1bJZ_NYnof1tbs?nd+U1_n;0J zK6Df?{}9jB;xo2Fsb(*aMT_1&9cJw7m{vM3F~}Tgd=A+grZG?}q#wzNIKVzc@>S12 znQ)8mnh((H`Q23i#_^{us%7M$@$30dM#PQQaUI10{$w4M)&Ix;L_+!GkeFBs+Q*Ov zC=rLa&8z^n0Vxoj-ch5{E$EexM)3B@@__7){E>;w|7e7uGr%K=^~XO2K$I2ulDvQA zNH=P1B5F0yBGQUm#I#}>A0$`<;J836Alk8ZIb53Bz5{_cWOyubYRouJ7Wv9;JgZvW ztjb}A(wUj@;oGg%R!{MYuhr2ZyM7-d36sK#mw6+P5`$F3wJ@>d=T9f;mqbcrlm@e% zCi1vnr(G|<;>RH`C8-6Ht4TU}00NKGPl zjA5pz%AWFu3)U%JO~N4Z$9KJX*FEoCwiKyxIGsjQDuFPI43nV*DOodQ2DyWEL%aCi zfB3)i&xN4+@hIV&Bn)yKhpB$7*DU)x{z&4n7Mr1Qgqbt~wzyyYaM2_sTGH-bGpowJ zhwduH=O06w91y3qGrBlcb2ZhtUVQ@a|CFEz!zJnucdmQfnBy0AjKD@B0K+GfbP@qD zteY)Me2GVlQ|kK=t;X)}MB;@hFIsEYNuR<(LbV?__AE=gvMnEvAN#o%v)8FG7oK}M zVlnUEre;BhQP(jnLwg9$tbC>L2IsyH#p2F|hB5K2@sIqoZfpQ3@Ok4ZjzSr?oZ}C~ z2SN12OpQ{Hn!C|u2NO4D*^aXu0$QOUEpbVoISi4NNIt>ywsCX@d_H?2QBpA~&Q1^4 zX>p-C%qHw404I2kCJBhf=@%S}2@lVLs#HurE@oZmqq<=}j@&HwLz3v18NC1uIaAmh zQ=|14`-<$@X)YllKI?-?ehLd((U-+{!)c8ey~~x1`t#D>lDr+hk9ya?atH(AKemZ% zXJpi)nkQ!icNM&m$3IDn=rAQ36L}LbHU+GHn z49hR6PKl=4g`*hf4i=Qj-1tppD;DwI)}6k%wm^qT>Ffr5Wb|CWeo7zUJquDMz||ws zm+sO&Rww6d)(F}M({wsPT4m_^Zq=@YX^?1jWh^IFfraiYa^e5+=b;%BW+CR1+qWg-FpY>A z<<|vgs&bN{S~7-4;pz(^Hf5`+`0r&g4x=916jQau64v9Ol7El63I559ydlcpDn}z; z76D|K!9)%T#U2*qiA>}O=_{2*+%Kl@ZlcAd&A#~4Dubf$q;$U-f)+-;sT3R`t@#xE z)%+eXgNEaNtBOrUAfO)1g;T|Cp0B8-CL`yywX3(U5ox4q!)cc-=X_E$RkJk5=5ut@ ztGiL>FuhBCa_m1|UNO__g9s%>wzLdThARo97ldwq|K2Y64alNOteZ;oz{1ujj@U%= z4p%J?njrOMA*C7?t~Jm54$3p*tbvD;Cc&^1AaL9>;J%^NH|;QcY|L6>(8X`bC(0;Lh*H~Q$dMaY+BgvFk0 z?w&FMFLG8|M1W290ROHB_fn;1cPUBM9Z$BQS zWk7fX=|i9}jRBhH8~Avp0T9SRuFCGxeRQlU7(S59qYx@oU;;r8S|?1>SX`|{jA~Ww zz`juD^LRwLUd(7f{V`Q{`@A}~N=zQ7a~*QZw{(=<#&vl@VwvIRFM~_?Wd>G;2HF86 zW}gEH_yQOg?+Wv7WV$v$wV7gf;j2Y^sramv)+zwxjrdU#hT^_2nM9Te&Wwc$Va%2R zp+KN~s-dV60yJfGlUxPRT3V_Am_= z(Nii&zOn{tk(L8iEUC4=~`^{HRXaXMiByw3kE5@Fe&`s3aj_Lx-o0FnH2DRk< z%+IdRTO{xlX8cGJoAxZ@&b5F|P7Xtf5GE$NlF618E|tSEM-`oJ_`s}w( z-U3jrRDn}-g%+|#e9w!Gkg|||LKOJ24)Zco{fBU_46c8>g88hwos@wHdaljMCA8U` z^#<2F{aAMWQBb;_O2()0XE&V{fjIwHZ0s!DLsLcF$(dYVd+DODp1P(>YU9&1w?WnY zMu(=Bi>?5GDbdF*4KhCYq{2e_v4aI2YQbmS8+6eNtTDWF8)y&Qx73j>6gr(_7pgxv z_lVg=*0|9-+ee=vGaaSaJYoLwsHg;_x=5lkkAnax`SpO;2Czb$rrc8sJN}x~q6KY~ zkG`A%62mJhd8!L7R3-|&qQbTHff!L?;=x4vF7+PNp7*=}?+;<#l-6C>jIMpH0h7w9 z<+|D*IXyl{W}PctqYXVxN=w~=05o0;N18}DiO7lQnSc220bl^+N;ZeYPZB^MVpeL5 zxWEUxiA*OX);N0i-)IK3KPZd(Z>&gPejTg7lrE=Wq}m&PaHwTP|8dArH`rADkxG|P7f++Msv>zxkM?qvo*q#)3o%we{ z3JW~e!Qp{H5^Enrm8>$j1Z2?u9w}wXs%Ui8rL|pmh*OSi{U$tlO#9A@|wx_PkHg zVx#Cf=Nh_$j731JBa$<2LF@9VsrYU64K+zRgF=@=+4}n-0bM8ybSSAs003>3Ix-0} zasfz;I3nJ=^Tz>4P$ZN>fo>sMot)TO7?HruKaD%Ik{?YEM}}ijhVguKt@>qJH>N^Ip$bBIUDZZ}M4gwBFTtH@{)k{s{`iBlkTs03dH}T^#=3 z{{MUbb55fCj(>*){3$Ch|S2tcBxi7yEjZ^aMBQHiTS zqK;CA)i$ZQpWP$0{gI!K%;Y*J!BeDKenJ%ZDC?)>Ld_#$Y9zrOHDFER)v;jxnmS zT<0PH(Y>C9ExlZ{jSh}U&HKkQzol;rbVMRixc23uc-*{H4dQ6At+~Xzbt%k?2k>06 z8OQEcxq3*azIJ{*kn5WdSn_+R0KN(n#?2PQD>=-azj<3)lv9HOo&qfGBUU=Z=loa% zXd$a0rZw)gnI8GKdY+WM5K3(s+S;^*EdbLznkp4A=0^&=LXr+~1QC4;&1M-#XT(;J zNdcs-^EtWNF(f6fwFG(wyp{;_iG>V4vYQ6Vl#~j$hUn2&TQ0D*)cT30sGM)2HkOcs zGmZ+Bt2=)Rs|-e@7!-N+?FA2_H(S`cAngtWxaZg28A-+y;Bjw^v1rPI_YZ`a$ z5Uy(YElkyRuxw`jJI6~puoVgBQPK!|}lXq=I_QF#wkBhoL1 zG8&``PIYX63O_gqjl!fyk&dTM!Ljj-T^z;ddL(@f)ld5bm1w}>oUMWn7g2)HM8QRFS1Dj%4RSaP;wM9K3r-B9ONQ?_+Cx7h7oz@8yy2c zC=l@*eX=E*5*)u7Vw7PRrGHNFymExP-znI3>am0TNTfkx&d{e;>=wz*OtX?)Gj^Fs zDgXXDCCw2OR4gf3tZH5P;P2!>A0qcKH z$-|Kq&tcIUw3-ND=-Xk@c#|17kOdQ zz_@1A{2(7dXR#vOcHBVqNH>LI$kVFGTrFC0Lkp@)KX*s}$?)Ljb+B)NTS%J3p)|{N7spNP zm_mTVF=#xic4)Wf$9x3pOB1jo*yA5;Z_m}9MMzAT#i|%D!qk6!K7{V+(IS%`N-aVZ>{ z{MEuj$d(*Pq_Jqk4|yCXp4E4bnRvs1e0aS5RjrE=o>pb!Q|Dns^c zN&u2FDHokWop<>-N#NM(P=F~a_ed%mSV*d9stzLgPyasyz=%nLn&?P?N_~1CXv0-w zOC)F%GVP-TVr_X6_YmS&zAEL4zoKefgh?{Ri z^4@lyxf);hGvZ0egWsPSBAR#_+0eZ$jZt!O-3$xmDUxW8S(a_yr2V%a%VpzocT z7t`-bBfR4FJ%Vr$`1HSGf6E(3}9gKE=XjlyB66sl?@uH?P1q;a8%bZ5`S@mLuhkf?+D=jQ%5sjjw2rLUXWpdqF8{0l z_x|UQQ0brkCI%f%Od}17bwOjTrm+zdjiI}H?|+PcIWzf{VNwLy7amr6wkp_*vn{P3 zbqH)}Lp9G33mncetj zg4lc}i1ZNWemrG|DHfD12fJ!4#r4(46Q;DONv8vO*`lQRGn?NCRFJ} zXk;C-Y+!CWW(`0(KKDCdyBrya4M+NOABt|>F-+GdVFPUF;y|GO9IgL=nf09L_#4Aw2cH{ixO%ip-0K{796`o?Sbo zkw591OE}p|6D)xuGLbIyXh(mx@0csdYI<}mYz0T?GLhg4JpR8?z)F0ux9%n0{}MiEH(gaV;nHP8R|d$-}y(=cvPN|xKBA)PLZacHKqcF z|DFF$xM7ZXdb41Ev+t>1XO6<*PFqv`bXGHQ+2EPjh#>snA_v3ejESAhfr=nsB9-p0 z)K(u=q~%Dkja8-$fxJTP6v5RK(?J*e1B{_iD<|9jv}Og4N*(ZQ_Js|arU;B6rwi`C zA{%ngf%Xlft-d9N)3+r~Dct5{-*P<(z=!G5#cf`mKRQ%i_!98pj`Y@9czi;^vK@l5Mw6z(;MyC8t(Tw$>Em14=y1XA046|5psP5OlgaGwe^bU)S__yt<_@a0)PmtKM& zOWwc-`OTsv71Hc}UN+J%xoG?N)6YqK8z_k6NzWly9QHdR){SbP6+J0nW?Z#x2ddwk z>jL6Sd$+q!iN2N+TpB5nS4H%g2%C%T9RHyaEiW6>O*-nZ&g* zZbP{55(Y&GJwws{ozWhUX<}G+6MwOoODSRdu5<2KEk%&t`xHrejwO8~qLCU#u&_{J zJpeL$R|#OC(@iAk=b6OTaRZmkA^H(|Vg?yY;1)am%ENMRH>Z=;I5bavNzTIBsbEPw zIrmn0ZejT_;z}kXmBXmkop^YtKi#gpMiIB9gv)aNR%?@?ZR$x`_FL*&`ymp4@As$B zL2u|lzSZhvK3V&YJkpQ<@IOTi!RH_4Mk3}?>jw#%P)no6$3#7%+o}aRpv{C_~c8PzXEFpc_Y+YBq3-CJkb*-n5vo_%DmR^+En z(sSo1CwNr52C@B)BloL69_zW>q>61+=WTUTnn!m>)Y69$r-#*4%f>E>ce5+YO%cCA zZbEw<*=D`x%*51aZ2`o2-`6A&jc#2c9YFZ=87yM>#a~c{Np3yIo*#%sj zy*+6gYp3SSHL^XQlo3rk-rzfxy=w!oX-lIl4%GL%PenAz-iKs*jBr=wd<@O}T2hri zkxDq~BS*I6^Z7ST^{GhuE5;)!^Cvp4-Qg-`hLB0D+QWT>$t6sC zyMy>57KOY3^rA{<0OSl(clb7?_A}E-FMzEaK*SC)G`Akdn{8%TCg^46R=bTg)l~Y2 z|2Y5#ih4|SmpEBFa#cjc^sD@dAp86L1xYW7?k*{+1ncKV$_d2x0dt^nW*^kK8u)#G zy+4Q0LH)gm}CKO<@t&2^##{OEdBCW^-7N#+!EcAB|tRkEW zd34Au0Du{7OYCb+dw)8H2184=+?El^!U@8_N@_)QnYl7^NKBN~>N-j{f#G4Vw_ z!N&cbcQudH2JlgGIQL=i_ng=jTiNpq?E2nceLub{MS~5ftMu$kovF5GmbqZNLI4!I zSXv<_F_lRpI0`de;7%aqzxQ_!0G)lD5 zVe7O*At##H-pg%j_{(3u=?tZ!(FEk#$j*UT665mYtP;*kq6P7Bu3c z=mfbv&k2R<`3;tt>HXpnsbQ4>zy{~oUK9Yt-7(W;b6SB##E2kW#VaI0&sP6Ob65g( z-%rMSQClDkp0Q{7%XtmK6;wn>X;KrV^Xv11UXxs9FH`5BEGv=C#pT=9%iP23oClVU zy3rxTZQidJ;EqU={45iE-&4=kmLet#kEV%fl}L>E{NMiVd;bZ~B=@d=VjGnr{x#X- zklXmi4t}qHlne{^pK%;rS=H+fG;bvrbZ^v5BshG#dF!9Xo)=&GKaQie5Wt9YW7X@NPTO*!eQ3Y^;8JN zf3M}XqQ<8eKOx2oO^oTL|6}QAc$d;g` z7jWyllLqbC_^2J#oNs=yD3FHo&PLfN4I#f6cE2901b3I4OMsmA zAOD;LLhrK6orDty>zaH0H3{L6%S;j6&opDCNG)l;iR~SuvP29KR)2Z>l3(h6QL?M? zEf^okh$4w_HjbT)WvPj7m{81N0CkS`kaqTfXE(+}B~`-hNuScLpevhJ%U0c;-pc)v&QDSQ*!zr1){vn zU8oTTA#a5vks_4dNmwtb#qYnr4U=PlCk(2*2(d9RGP+S{0L87~wVM z6DN|HDy@C|L|kW^XK5t6NBrifeR-)L@m8Dj;oe1ZC*9zvSchssz}MfC!jN7S2@Cwd`i!+3=O~bEeI3y zq~cTU$hOkf|2 z@b2g#A*VerF6HqMn5r}T3t-UNi|jRlY^K>lODgQdrNBwa5tyfxP0`t$WiE@Cg}`ad zX<{Re;U(#v4igeH6PI3|#LD+_dal{@Ouo}qolwF;sz+?ykM0{6&IEuy>`*}xrIFlG zAtf&E)pBJ@+in!%M~q6oDWSQerwz)jXtH@_YpQU`&zH6r;#wkStbTsWc_b+@#O~RB zW1Z%KssqLNb;L7DMy0SHyi8#-^s2U$5oKtn)#I}SBKjtNf-z(ac2` z)BfXM{hvj`09ipMAw1$rv?oj={T!%b5wvtP=A+y(VDsxZ0CR?3lyM^{-!oHyIhzy+(F+qLP{X}29J;pDq4!(GhN5NI=&e{@ z!@Ms^4cMtWXG$qIJl^{CRM}l9tWhXZokrHwK6YwKtfC-wEHTh@K;_1?T6C=8y98VgkQ-s z;ioRpAAk{avp+X>F!;!|g#e!X=E7#fcAdmP>_8mJ{{`FKO*n-e{8|}jnD+dg>Qb{ z8@J*(tmr+XQG47Q+vfgEON2wWA40QS!&hcUYtzEBda-5OCf)MZU0ZS7*|M-sX(~J&lM}#7eM5>R^k!; zgcZE^l$;|Jj%|~SfZl(>2}Bd&<05pGN31~SlfxlLpL>EzqQ*wMoQ zqoM!be2*Sra%@uNXkpmT!4teAVcG;v4z)~R1ZtY|ZZ=XEa?hOZKh4+H;8>>L#?&dN zBp)dqIc9M9>g=17(x=YXIdL!CPPRBUcHO`BnNRplI9K;Py{OnRc#=8UFHrq0jvJ&s-;y{fitV?4E+s zKj{8Veb0+8C4z#xD>-bEe0VB(Roz$$IO~-bKl#javv-O|kV}APNK4eah0i4T=9jIQ z#od!DpqbfM`TSAJVVlcu-(PQkceyU!+hgap-+2)Y37gi;D4+WF%&mK^_wwq_Km5LW zomdfn@BYQWk@eM+c1Cwhs^IXQ-1uPYr40wfHmA(kdagylQk?6`-b6p4SzU@u94nWV z_5a`Q8!|I$;?a}G&M{_4OU^qlbDMGI+NEz(i*5(M{V}b`uI6d-|G81$kD49tNK%mb zWP8$Phk~Zh0j0J%4{T4Fh;>ORrcT+T=ECK}E_G>DK@G>^93D=o9Gle|cT>)7f4)Q| zWpxO{Ml)8MD!ta2@G7sUuRAQdG^!<9-BvNMK3+9J%x8+r2Z=eK-tJtsF>UE#wU=*R zuh}M-b=&NEP-5Jg#Q86s<5p^$e@}H*`}FGU!W(XDuTJ@Vd)1LWr@lP9e(PU0-&^l1 z+phMmOv{RYwWgd0c*bB?*ONOB3p!W7&OLI&qq}8^%Z5oElR)8rmx1YwwCBrCi6Z|d z&RP}bK<_K7wU^9DIVr`qWXbWKhWtB|O@p6Zx?{QgQ)c{HmeU>+BNpACeP)qqX1(gc z_WDEn|KI>9MtXSs2pfY{(hP#SZGOj92m5Y{Y&up51|JrJw z!`_#Fzn=N`&!;OF4*Q-xdo;+}S~B?m|3Aw@UEkezb#*B#GYD>(2AnqExNK%t%$!Wu z7@NGZ^xjjGyZ8Jy$vlvH^zgI)|DO6v1-c)5_SAE2Xed8J6IbWyOooMzOgJw1aW3cW zS+r?sTgL@n6J@*Qy^0Z=GS!wm%+}e>tLWmS#O~%HxZ(ChMV+akp`l7yK90Vw#(I|K zs|E^y!vArH(4B}ACp9HDxiakZk~k>h8(8 + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index be66c6d3..2dcd4e7b 100644 --- a/pom.xml +++ b/pom.xml @@ -116,12 +116,22 @@ com.fasterxml.jackson.core jackson-core - 2.8.11 + 2.9.6 com.fasterxml.jackson.core jackson-databind - 2.8.11.1 + 2.9.6 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.0 + + + com.google.guava + guava + 20.0 @@ -303,6 +313,16 @@ install/docker + + integration-test + + subsonic-rest-api + airsonic-sonos-api + airsonic-main + install/docker + integration-test + + sign From f6b248495ca851ec0d09a37b52293434fc8b163c Mon Sep 17 00:00:00 2001 From: Carlos Galindo Date: Sat, 13 Oct 2018 17:25:42 +0200 Subject: [PATCH 02/10] Fixed github link opening in frame and not loading --- .../org/airsonic/player/i18n/ResourceBundle_en.properties | 2 +- .../org/airsonic/player/i18n/ResourceBundle_zh_TW.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties index f2945d5c..c54a3d18 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties @@ -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 #airsonic on Freenode. Technical issues can be submitted to the issue tracker on GitHub. +help.contact.text=Airsonic is a community project. You can find us in #airsonic on Freenode. Technical issues can be submitted to the issue tracker on GitHub. help.log=Log help.logfile=The complete log is saved in {0}. # settingsHeader.jsp diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties index 1ec91fc2..2cd97a38 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties @@ -319,7 +319,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 Freenode \u4E0A\u7684 #airsonic \u627E\u5230\u6211\u5011\u3002\u6280\u8853\u554F\u984C\u53EF\u4EE5\u63D0\u4EA4\u5230 Github \u4E0A\u7684\u554F\u984C\u8FFD\u8E64\u5668\u3002 +help.contact.text = Airsonic \u662F\u793E\u7FA4\u5C08\u6848\u3002 \u60A8\u53EF\u4EE5\u5728 Freenode \u4E0A\u7684 #airsonic \u627E\u5230\u6211\u5011\u3002\u6280\u8853\u554F\u984C\u53EF\u4EE5\u63D0\u4EA4\u5230 Github \u4E0A\u7684\u554F\u984C\u8FFD\u8E64\u5668\u3002 help.log = \u8A18\u9304 help.logfile = \u5B8C\u6574\u7684\u7D00\u9304\u5B58\u653E\u5728 {0}\u3002 From 609ca71307691059e49bbb92c30ec8351a2b4a78 Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Thu, 18 Oct 2018 16:41:39 -0400 Subject: [PATCH 03/10] Skip another irrelevant CVE Signed-off-by: Andrew DeMaria --- airsonic-main/cve-suppressed.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/airsonic-main/cve-suppressed.xml b/airsonic-main/cve-suppressed.xml index 4e3266b3..e947d376 100644 --- a/airsonic-main/cve-suppressed.xml +++ b/airsonic-main/cve-suppressed.xml @@ -124,6 +124,11 @@ ^org\.postgresql:postgresql:.*$ CVE-2018-1115 + + Does not affect the postgres client + ^org\.postgresql:postgresql:.*$ + CVE-2016-7048 + This is for nodejs ^org\.mariadb\.jdbc:mariadb-java-client:.*$ From 377f68543dd7da5cc1f22f79fb40e7cbd1226759 Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Sat, 20 Oct 2018 16:40:52 -0400 Subject: [PATCH 04/10] Added profile to make running within a ide easier Signed-off-by: Andrew DeMaria --- airsonic-main/pom.xml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index 7204c4fd..1d94de0b 100755 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -15,6 +15,7 @@ 3.1.0 1.2.1-RELEASE + provided @@ -445,12 +446,12 @@ org.springframework.boot spring-boot-starter-tomcat - provided + ${tomcat.server.scope} org.apache.tomcat.embed tomcat-embed-jasper - provided + ${tomcat.server.scope} @@ -599,5 +600,11 @@ spring-boot-starter-tomcat + + ide-tomcat-embed + + compile + + From f8161f5184441c9d9fd17a576a2c903d8b792c9d Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Sat, 20 Oct 2018 16:42:23 -0400 Subject: [PATCH 05/10] White list jars that are scanned for tlds to prevent spurious logs Signed-off-by: Andrew DeMaria --- .../main/java/org/airsonic/player/TomcatApplication.java | 6 ++++++ airsonic-main/src/main/resources/application.properties | 3 +++ 2 files changed, 9 insertions(+) diff --git a/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java b/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java index 8cf23390..d62dea83 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java +++ b/airsonic-main/src/main/java/org/airsonic/player/TomcatApplication.java @@ -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 diff --git a/airsonic-main/src/main/resources/application.properties b/airsonic-main/src/main/resources/application.properties index 3ab7c4db..a536e667 100644 --- a/airsonic-main/src/main/resources/application.properties +++ b/airsonic-main/src/main/resources/application.properties @@ -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 From f8686d9638a4d24aef8e563a18a061310bddb24f Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Sat, 20 Oct 2018 15:14:56 -0600 Subject: [PATCH 06/10] Tweaked logging around servlet container and added warning about jetty Signed-off-by: Andrew DeMaria --- .../main/java/org/airsonic/player/Application.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/airsonic-main/src/main/java/org/airsonic/player/Application.java b/airsonic-main/src/main/java/org/airsonic/player/Application.java index 3f476853..25857afe 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/Application.java +++ b/airsonic-main/src/main/java/org/airsonic/player/Application.java @@ -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) { From 6b4874f33ce92e14f102b6bf216ca5ea207fe780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Cocula?= Date: Sun, 30 Sep 2018 11:14:28 +0200 Subject: [PATCH 07/10] archetype code for rest api integration tests --- airsonic-main/pom.xml | 10 + .../controller/SubsonicRESTController.java | 4 +- .../org/airsonic/player/dao/PlayerDao.java | 6 +- .../player/dao/PlayerDaoPlayQueueFactory.java | 12 + .../player/service/JukeboxJavaService.java | 73 ++--- .../service/JukeboxLegacySubsonicService.java | 5 +- .../player/service/TranscodingService.java | 2 + .../service/jukebox/AudioPlayerFactory.java | 14 + .../service/jukebox/JavaPlayerFactory.java | 17 ++ .../org/airsonic/player/TestCaseUtils.java | 9 + .../player/api/AirsonicRestApiIntTest.java | 59 ++++ ...AbstractAirsonicRestApiJukeboxIntTest.java | 279 ++++++++++++++++++ .../AirsonicRestApiJukeboxIntTest.java | 38 +++ .../AirsonicRestApiJukeboxLegacyIntTest.java | 62 ++++ .../service/TranscodingServiceIntTest.java | 41 +++ .../src/test/resources/application.properties | 13 + pom.xml | 2 + 17 files changed, 599 insertions(+), 47 deletions(-) create mode 100644 airsonic-main/src/main/java/org/airsonic/player/dao/PlayerDaoPlayQueueFactory.java create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/jukebox/AudioPlayerFactory.java create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/jukebox/JavaPlayerFactory.java create mode 100644 airsonic-main/src/test/java/org/airsonic/player/api/AirsonicRestApiIntTest.java create mode 100644 airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AbstractAirsonicRestApiJukeboxIntTest.java create mode 100644 airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxIntTest.java create mode 100644 airsonic-main/src/test/java/org/airsonic/player/api/jukebox/AirsonicRestApiJukeboxLegacyIntTest.java create mode 100644 airsonic-main/src/test/java/org/airsonic/player/service/TranscodingServiceIntTest.java create mode 100644 airsonic-main/src/test/resources/application.properties diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index 7204c4fd..7a33e986 100755 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -86,6 +86,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:* From 8c6ddb1aba562d4f8175ec803ab192fa8d3cb296 Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Sun, 21 Oct 2018 14:31:52 -0600 Subject: [PATCH 08/10] Dependency tweaks and remove extraneous code Signed-off-by: Andrew DeMaria --- airsonic-main/pom.xml | 4 ++++ .../java/org/airsonic/player/service/JukeboxJavaService.java | 3 --- pom.xml | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index 6c7183f5..f64b7576 100755 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -314,6 +314,10 @@ 1.10.19 test + + org.assertj + assertj-core + 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 908dc43c..8b6c8b7e 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 @@ -329,9 +329,6 @@ public class JukeboxJavaService { this.statusService = statusService; } - public void setSettingsService(SettingsService settingsService) { - } - public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } diff --git a/pom.xml b/pom.xml index b63145f6..be66c6d3 100644 --- a/pom.xml +++ b/pom.xml @@ -255,8 +255,6 @@ 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:* From c6130e94a0e925fc2ff4fb73088df00667e963b6 Mon Sep 17 00:00:00 2001 From: jamescochran Date: Tue, 30 Oct 2018 07:55:22 -0400 Subject: [PATCH 09/10] Fix small mispelling (#823) improvments to improvements --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105f04c9..6543c6ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From ffd84460163922fc527df9bfc4d45848de82e6f7 Mon Sep 17 00:00:00 2001 From: jamescochran Date: Tue, 30 Oct 2018 07:55:54 -0400 Subject: [PATCH 10/10] Fix small spelling error (#824) `ubiquitious` to `ubiquitous` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f589bc3..858cf46a 100644 --- a/README.md +++ b/README.md @@ -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.