diff --git a/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy b/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy index dcbfbbf..c41c7bd 100644 --- a/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy +++ b/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy @@ -1,7 +1,7 @@ package net.schowek.nextclouddlna.controller - -import net.schowek.nextclouddlna.dlna.media.MediaServer +import net.schowek.nextclouddlna.dlna.DlnaService +import net.schowek.nextclouddlna.dlna.MediaServer import org.jupnp.support.model.DIDLObject import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus @@ -14,11 +14,14 @@ class UpnpControllerIntTest extends UpnpAwareSpecification { @Autowired private MediaServer mediaServer + @Autowired + private DlnaService dlnaService def uid def setup() { uid = mediaServer.serviceIdentifier + dlnaService.start() } def "should serve icon"() { diff --git a/src/integration/groovy/net/schowek/nextclouddlna/dlna/DlnaServiceIntTest.groovy b/src/integration/groovy/net/schowek/nextclouddlna/dlna/DlnaServiceIntTest.groovy new file mode 100644 index 0000000..f79b0de --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/dlna/DlnaServiceIntTest.groovy @@ -0,0 +1,55 @@ +package net.schowek.nextclouddlna.dlna + +import org.jupnp.UpnpService +import org.jupnp.model.message.discovery.OutgoingSearchRequest +import org.jupnp.model.message.header.HostHeader +import org.jupnp.model.message.header.MANHeader +import org.jupnp.model.message.header.STAllHeader +import org.jupnp.model.message.header.UpnpHeader +import org.jupnp.model.types.HostPort +import org.springframework.beans.factory.annotation.Autowired +import support.IntegrationSpecification +import support.beans.dlna.upnp.UpnpServiceConfigurationInt + +import static org.jupnp.model.Constants.IPV4_UPNP_MULTICAST_GROUP +import static org.jupnp.model.Constants.UPNP_MULTICAST_PORT +import static org.jupnp.model.message.UpnpRequest.Method.MSEARCH +import static org.jupnp.model.message.header.UpnpHeader.Type.* +import static org.jupnp.model.types.NotificationSubtype.ALL +import static org.jupnp.model.types.NotificationSubtype.DISCOVER + +class DlnaServiceIntTest extends IntegrationSpecification { + @Autowired + private UpnpService upnpService + @Autowired + private MediaServer mediaServer + + def "should send initial multicast Upnp datagrams on start"() { + given: + def configuration = upnpService.configuration as UpnpServiceConfigurationInt + def sut = new DlnaService(upnpService, mediaServer) + + expect: + configuration.outgoingDatagramMessages == [] + + when: + sut.start() + + then: + configuration.outgoingDatagramMessages.any() + configuration.outgoingDatagramMessages[0].class == OutgoingSearchRequest + with(configuration.outgoingDatagramMessages[0] as OutgoingSearchRequest) { + assert it.operation.method == MSEARCH + assert it.destinationAddress == InetAddress.getByName(IPV4_UPNP_MULTICAST_GROUP) + assert it.destinationPort == UPNP_MULTICAST_PORT + + assert header(it, MAN, MANHeader.class) == DISCOVER.headerString + assert header(it, ST, STAllHeader.class).headerString == ALL.headerString + assert header(it, HOST, HostHeader.class) == new HostPort(IPV4_UPNP_MULTICAST_GROUP, UPNP_MULTICAST_PORT) + } + } + + def T header(OutgoingSearchRequest request, UpnpHeader.Type type, Class> clazz) { + return clazz.cast(request.headers.get(type).find()).value + } +} diff --git a/src/integration/groovy/support/IntegrationSpecification.groovy b/src/integration/groovy/support/IntegrationSpecification.groovy index 0f51d52..ff4001d 100644 --- a/src/integration/groovy/support/IntegrationSpecification.groovy +++ b/src/integration/groovy/support/IntegrationSpecification.groovy @@ -7,17 +7,20 @@ import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.context.annotation.Import +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import support.beans.TestConfig import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT +import static org.springframework.test.annotation.DirtiesContext.ClassMode.* @ContextConfiguration(loader = SpringBootContextLoader, classes = NextcloudDLNAApp.class) @SpringBootTest(webEnvironment = DEFINED_PORT) @ActiveProfiles("integration") @Import(TestConfig.class) +@DirtiesContext(classMode = AFTER_CLASS) class IntegrationSpecification extends Specification { @Autowired private TestRestTemplate restTemplate @@ -30,6 +33,6 @@ class IntegrationSpecification extends Specification { private ServerInfoProvider serverInfoProvider protected String urlWithPort(String uri = "") { - return "http://localhost:" + serverInfoProvider.port + uri; + return "http://" + serverInfoProvider.host + ":" + serverInfoProvider.port + uri; } } diff --git a/src/integration/groovy/support/beans/dlna/upnp/UpnpServiceConfigurationInt.groovy b/src/integration/groovy/support/beans/dlna/upnp/UpnpServiceConfigurationInt.groovy index 4d95c90..91b269e 100644 --- a/src/integration/groovy/support/beans/dlna/upnp/UpnpServiceConfigurationInt.groovy +++ b/src/integration/groovy/support/beans/dlna/upnp/UpnpServiceConfigurationInt.groovy @@ -1,6 +1,9 @@ package support.beans.dlna.upnp import org.jupnp.DefaultUpnpServiceConfiguration +import org.jupnp.model.message.OutgoingDatagramMessage +import org.jupnp.transport.impl.DatagramIOConfigurationImpl +import org.jupnp.transport.impl.DatagramIOImpl import org.jupnp.transport.spi.DatagramIO import org.jupnp.transport.spi.NetworkAddressFactory import org.jupnp.transport.spi.StreamClient @@ -11,19 +14,38 @@ import org.springframework.stereotype.Component @Component @Profile("integration") class UpnpServiceConfigurationInt extends DefaultUpnpServiceConfiguration { + List outgoingDatagramMessages = new ArrayList<>() + @Override public StreamClient createStreamClient() { return null } - @Override public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) { return null } -// @Override -// public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory) { -// -// } + @Override + public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory) { + return new MockDatagramIO(this, new DatagramIOConfigurationImpl()) + } + + private void onOutgoingDatagramMessage(OutgoingDatagramMessage message) { + outgoingDatagramMessages.add(message) + } + + class MockDatagramIO extends DatagramIOImpl { + private final UpnpServiceConfigurationInt upnpServiceConfiguration + + MockDatagramIO(UpnpServiceConfigurationInt upnpServiceConfiguration, DatagramIOConfigurationImpl configuration) { + super(configuration) + this.upnpServiceConfiguration = upnpServiceConfiguration + } + + @Override + void send(OutgoingDatagramMessage message) { + upnpServiceConfiguration.onOutgoingDatagramMessage(message) + } + } } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpController.kt b/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpController.kt index 0aabcc8..3256c01 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpController.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpController.kt @@ -2,9 +2,9 @@ package net.schowek.nextclouddlna.controller import jakarta.servlet.http.HttpServletRequest import mu.KLogging -import net.schowek.nextclouddlna.DlnaService +import net.schowek.nextclouddlna.dlna.DlnaService import net.schowek.nextclouddlna.dlna.StreamMessageMapper -import net.schowek.nextclouddlna.dlna.media.MediaServer +import net.schowek.nextclouddlna.dlna.MediaServer import org.springframework.core.io.InputStreamResource import org.springframework.core.io.Resource import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE diff --git a/src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/DlnaService.kt similarity index 92% rename from src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt rename to src/main/kotlin/net/schowek/nextclouddlna/dlna/DlnaService.kt index 4c76a91..5e1c8e1 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/DlnaService.kt @@ -1,8 +1,7 @@ -package net.schowek.nextclouddlna +package net.schowek.nextclouddlna.dlna import jakarta.annotation.PreDestroy import mu.KLogging -import net.schowek.nextclouddlna.dlna.media.MediaServer import org.jupnp.UpnpService import org.jupnp.model.message.StreamRequestMessage import org.jupnp.model.message.StreamResponseMessage @@ -22,7 +21,7 @@ class DlnaService( upnpService.registry.addDevice(mediaServer.device) } - @EventListener + @EventListener(condition = "!@environment.acceptsProfiles('integration')") fun handleContextRefresh(event: ContextRefreshedEvent) { start() } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/MediaServer.kt similarity index 97% rename from src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt rename to src/main/kotlin/net/schowek/nextclouddlna/dlna/MediaServer.kt index 5c0a1a6..208a64a 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/MediaServer.kt @@ -1,4 +1,4 @@ -package net.schowek.nextclouddlna.dlna.media +package net.schowek.nextclouddlna.dlna import mu.KLogging import net.schowek.nextclouddlna.util.ExternalUrls diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpService.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpService.kt index 99996e9..52051c7 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpService.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpService.kt @@ -2,13 +2,9 @@ package net.schowek.nextclouddlna.dlna.upnp import org.jupnp.UpnpServiceConfiguration import org.jupnp.UpnpServiceImpl -import org.jupnp.protocol.ProtocolFactory -import org.jupnp.registry.RegistryImpl import org.springframework.stereotype.Component @Component class MyUpnpService( upnpServiceConfiguration: UpnpServiceConfiguration -) : UpnpServiceImpl(upnpServiceConfiguration) { - override fun createRegistry(pf: ProtocolFactory) = RegistryImpl(this) -} +) : UpnpServiceImpl(upnpServiceConfiguration) diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpServiceConfiguration.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpServiceConfiguration.kt index 9edff5c..a8b65c4 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpServiceConfiguration.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/upnp/MyUpnpServiceConfiguration.kt @@ -32,12 +32,12 @@ class MyUpnpServiceConfiguration( } override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) = - MyNetworkAddressFactory(serverInfoProvider, multicastResponsePort) + MyNetworkAddressFactory(streamListenPort, multicastResponsePort) inner class MyNetworkAddressFactory( - private val serverInfoProvider: ServerInfoProvider, + streamListenPort: Int, multicastResponsePort: Int - ) : NetworkAddressFactoryImpl(serverInfoProvider.port, multicastResponsePort) { + ) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) { override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) = addressesToBind.contains(address.hostAddress) } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProvider.kt b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProvider.kt index a57e5e5..9712178 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProvider.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProvider.kt @@ -1,7 +1,7 @@ package net.schowek.nextclouddlna.nextcloud.content import mu.KLogging -import net.schowek.nextclouddlna.nextcloud.NextcloudDB +import net.schowek.nextclouddlna.nextcloud.db.NextcloudDB import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import java.time.Clock @@ -100,8 +100,8 @@ class ContentTreeProvider( } } - fun getItem(id: String): ContentItem? = tree.getItem(id) - fun getNode(id: String): ContentNode? = tree.getNode(id) + fun getItem(id: String) = tree.getItem(id) + fun getNode(id: String) = tree.getNode(id) companion object : KLogging() { const val REBUILD_TREE_DELAY_IN_MS = 1000 * 60L // 1m @@ -115,13 +115,11 @@ class ContentTree { private val nodes: MutableMap = HashMap() private val items: MutableMap = HashMap() - fun getNode(id: String): ContentNode? { - return nodes[id] - } + val itemsCount get() = items.size + val nodesCount get() = nodes.size - fun getItem(id: String): ContentItem? { - return items[id] - } + fun getNode(id: String) = nodes[id] + fun getItem(id: String) = items[id] fun addItem(item: ContentItem) { items["${item.id}"] = item @@ -130,8 +128,5 @@ class ContentTree { fun addNode(node: ContentNode) { nodes["${node.id}"] = node } - - val itemsCount get() = items.size - val nodesCount get() = nodes.size } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/db/NextcloudDB.kt similarity index 97% rename from src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt rename to src/main/kotlin/net/schowek/nextclouddlna/nextcloud/db/NextcloudDB.kt index 5eae2f2..8292cad 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/db/NextcloudDB.kt @@ -1,4 +1,4 @@ -package net.schowek.nextclouddlna.nextcloud +package net.schowek.nextclouddlna.nextcloud.db import jakarta.annotation.PostConstruct import mu.KLogging @@ -6,7 +6,6 @@ import net.schowek.nextclouddlna.nextcloud.config.NextcloudConfigDiscovery import net.schowek.nextclouddlna.nextcloud.content.ContentItem import net.schowek.nextclouddlna.nextcloud.content.ContentNode import net.schowek.nextclouddlna.nextcloud.content.MediaFormat -import net.schowek.nextclouddlna.nextcloud.db.* import net.schowek.nextclouddlna.nextcloud.db.Filecache.Companion.FOLDER_MIME_TYPE import org.springframework.dao.InvalidDataAccessResourceUsageException import org.springframework.stereotype.Component diff --git a/src/test/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderTest.groovy b/src/test/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderTest.groovy index 3bbecce..cd41fb7 100644 --- a/src/test/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderTest.groovy +++ b/src/test/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderTest.groovy @@ -1,6 +1,6 @@ package net.schowek.nextclouddlna.nextcloud.content -import net.schowek.nextclouddlna.nextcloud.NextcloudDB +import net.schowek.nextclouddlna.nextcloud.db.NextcloudDB import spock.lang.Specification import java.time.Clock import java.time.ZoneId