Multicast search request tested
This commit is contained in:
parent
f5a8a6e982
commit
c3dadfd845
12 changed files with 109 additions and 37 deletions
|
@ -1,7 +1,7 @@
|
||||||
package net.schowek.nextclouddlna.controller
|
package net.schowek.nextclouddlna.controller
|
||||||
|
|
||||||
|
import net.schowek.nextclouddlna.dlna.DlnaService
|
||||||
import net.schowek.nextclouddlna.dlna.media.MediaServer
|
import net.schowek.nextclouddlna.dlna.MediaServer
|
||||||
import org.jupnp.support.model.DIDLObject
|
import org.jupnp.support.model.DIDLObject
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
@ -14,11 +14,14 @@ class UpnpControllerIntTest extends UpnpAwareSpecification {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MediaServer mediaServer
|
private MediaServer mediaServer
|
||||||
|
@Autowired
|
||||||
|
private DlnaService dlnaService
|
||||||
|
|
||||||
def uid
|
def uid
|
||||||
|
|
||||||
def setup() {
|
def setup() {
|
||||||
uid = mediaServer.serviceIdentifier
|
uid = mediaServer.serviceIdentifier
|
||||||
|
dlnaService.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
def "should serve icon"() {
|
def "should serve icon"() {
|
||||||
|
|
|
@ -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> T header(OutgoingSearchRequest request, UpnpHeader.Type type, Class<? extends UpnpHeader<T>> clazz) {
|
||||||
|
return clazz.cast(request.headers.get(type).find()).value
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,17 +7,20 @@ import org.springframework.boot.test.context.SpringBootContextLoader
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.boot.test.web.client.TestRestTemplate
|
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||||
import org.springframework.context.annotation.Import
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.test.annotation.DirtiesContext
|
||||||
import org.springframework.test.context.ActiveProfiles
|
import org.springframework.test.context.ActiveProfiles
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
import support.beans.TestConfig
|
import support.beans.TestConfig
|
||||||
|
|
||||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT
|
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)
|
@ContextConfiguration(loader = SpringBootContextLoader, classes = NextcloudDLNAApp.class)
|
||||||
@SpringBootTest(webEnvironment = DEFINED_PORT)
|
@SpringBootTest(webEnvironment = DEFINED_PORT)
|
||||||
@ActiveProfiles("integration")
|
@ActiveProfiles("integration")
|
||||||
@Import(TestConfig.class)
|
@Import(TestConfig.class)
|
||||||
|
@DirtiesContext(classMode = AFTER_CLASS)
|
||||||
class IntegrationSpecification extends Specification {
|
class IntegrationSpecification extends Specification {
|
||||||
@Autowired
|
@Autowired
|
||||||
private TestRestTemplate restTemplate
|
private TestRestTemplate restTemplate
|
||||||
|
@ -30,6 +33,6 @@ class IntegrationSpecification extends Specification {
|
||||||
private ServerInfoProvider serverInfoProvider
|
private ServerInfoProvider serverInfoProvider
|
||||||
|
|
||||||
protected String urlWithPort(String uri = "") {
|
protected String urlWithPort(String uri = "") {
|
||||||
return "http://localhost:" + serverInfoProvider.port + uri;
|
return "http://" + serverInfoProvider.host + ":" + serverInfoProvider.port + uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package support.beans.dlna.upnp
|
package support.beans.dlna.upnp
|
||||||
|
|
||||||
import org.jupnp.DefaultUpnpServiceConfiguration
|
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.DatagramIO
|
||||||
import org.jupnp.transport.spi.NetworkAddressFactory
|
import org.jupnp.transport.spi.NetworkAddressFactory
|
||||||
import org.jupnp.transport.spi.StreamClient
|
import org.jupnp.transport.spi.StreamClient
|
||||||
|
@ -11,19 +14,38 @@ import org.springframework.stereotype.Component
|
||||||
@Component
|
@Component
|
||||||
@Profile("integration")
|
@Profile("integration")
|
||||||
class UpnpServiceConfigurationInt extends DefaultUpnpServiceConfiguration {
|
class UpnpServiceConfigurationInt extends DefaultUpnpServiceConfiguration {
|
||||||
|
List<OutgoingDatagramMessage> outgoingDatagramMessages = new ArrayList<>()
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamClient createStreamClient() {
|
public StreamClient createStreamClient() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
|
public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Override
|
@Override
|
||||||
// public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory) {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ package net.schowek.nextclouddlna.controller
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
import mu.KLogging
|
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.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.InputStreamResource
|
||||||
import org.springframework.core.io.Resource
|
import org.springframework.core.io.Resource
|
||||||
import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE
|
import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package net.schowek.nextclouddlna
|
package net.schowek.nextclouddlna.dlna
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy
|
import jakarta.annotation.PreDestroy
|
||||||
import mu.KLogging
|
import mu.KLogging
|
||||||
import net.schowek.nextclouddlna.dlna.media.MediaServer
|
|
||||||
import org.jupnp.UpnpService
|
import org.jupnp.UpnpService
|
||||||
import org.jupnp.model.message.StreamRequestMessage
|
import org.jupnp.model.message.StreamRequestMessage
|
||||||
import org.jupnp.model.message.StreamResponseMessage
|
import org.jupnp.model.message.StreamResponseMessage
|
||||||
|
@ -22,7 +21,7 @@ class DlnaService(
|
||||||
upnpService.registry.addDevice(mediaServer.device)
|
upnpService.registry.addDevice(mediaServer.device)
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener
|
@EventListener(condition = "!@environment.acceptsProfiles('integration')")
|
||||||
fun handleContextRefresh(event: ContextRefreshedEvent) {
|
fun handleContextRefresh(event: ContextRefreshedEvent) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package net.schowek.nextclouddlna.dlna.media
|
package net.schowek.nextclouddlna.dlna
|
||||||
|
|
||||||
import mu.KLogging
|
import mu.KLogging
|
||||||
import net.schowek.nextclouddlna.util.ExternalUrls
|
import net.schowek.nextclouddlna.util.ExternalUrls
|
|
@ -2,13 +2,9 @@ package net.schowek.nextclouddlna.dlna.upnp
|
||||||
|
|
||||||
import org.jupnp.UpnpServiceConfiguration
|
import org.jupnp.UpnpServiceConfiguration
|
||||||
import org.jupnp.UpnpServiceImpl
|
import org.jupnp.UpnpServiceImpl
|
||||||
import org.jupnp.protocol.ProtocolFactory
|
|
||||||
import org.jupnp.registry.RegistryImpl
|
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class MyUpnpService(
|
class MyUpnpService(
|
||||||
upnpServiceConfiguration: UpnpServiceConfiguration
|
upnpServiceConfiguration: UpnpServiceConfiguration
|
||||||
) : UpnpServiceImpl(upnpServiceConfiguration) {
|
) : UpnpServiceImpl(upnpServiceConfiguration)
|
||||||
override fun createRegistry(pf: ProtocolFactory) = RegistryImpl(this)
|
|
||||||
}
|
|
||||||
|
|
|
@ -32,12 +32,12 @@ class MyUpnpServiceConfiguration(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) =
|
override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) =
|
||||||
MyNetworkAddressFactory(serverInfoProvider, multicastResponsePort)
|
MyNetworkAddressFactory(streamListenPort, multicastResponsePort)
|
||||||
|
|
||||||
inner class MyNetworkAddressFactory(
|
inner class MyNetworkAddressFactory(
|
||||||
private val serverInfoProvider: ServerInfoProvider,
|
streamListenPort: Int,
|
||||||
multicastResponsePort: Int
|
multicastResponsePort: Int
|
||||||
) : NetworkAddressFactoryImpl(serverInfoProvider.port, multicastResponsePort) {
|
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
|
||||||
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
|
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
|
||||||
addressesToBind.contains(address.hostAddress)
|
addressesToBind.contains(address.hostAddress)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package net.schowek.nextclouddlna.nextcloud.content
|
package net.schowek.nextclouddlna.nextcloud.content
|
||||||
|
|
||||||
import mu.KLogging
|
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.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
@ -100,8 +100,8 @@ class ContentTreeProvider(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getItem(id: String): ContentItem? = tree.getItem(id)
|
fun getItem(id: String) = tree.getItem(id)
|
||||||
fun getNode(id: String): ContentNode? = tree.getNode(id)
|
fun getNode(id: String) = tree.getNode(id)
|
||||||
|
|
||||||
companion object : KLogging() {
|
companion object : KLogging() {
|
||||||
const val REBUILD_TREE_DELAY_IN_MS = 1000 * 60L // 1m
|
const val REBUILD_TREE_DELAY_IN_MS = 1000 * 60L // 1m
|
||||||
|
@ -115,13 +115,11 @@ class ContentTree {
|
||||||
private val nodes: MutableMap<String, ContentNode> = HashMap()
|
private val nodes: MutableMap<String, ContentNode> = HashMap()
|
||||||
private val items: MutableMap<String, ContentItem> = HashMap()
|
private val items: MutableMap<String, ContentItem> = HashMap()
|
||||||
|
|
||||||
fun getNode(id: String): ContentNode? {
|
val itemsCount get() = items.size
|
||||||
return nodes[id]
|
val nodesCount get() = nodes.size
|
||||||
}
|
|
||||||
|
|
||||||
fun getItem(id: String): ContentItem? {
|
fun getNode(id: String) = nodes[id]
|
||||||
return items[id]
|
fun getItem(id: String) = items[id]
|
||||||
}
|
|
||||||
|
|
||||||
fun addItem(item: ContentItem) {
|
fun addItem(item: ContentItem) {
|
||||||
items["${item.id}"] = item
|
items["${item.id}"] = item
|
||||||
|
@ -130,8 +128,5 @@ class ContentTree {
|
||||||
fun addNode(node: ContentNode) {
|
fun addNode(node: ContentNode) {
|
||||||
nodes["${node.id}"] = node
|
nodes["${node.id}"] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
val itemsCount get() = items.size
|
|
||||||
val nodesCount get() = nodes.size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package net.schowek.nextclouddlna.nextcloud
|
package net.schowek.nextclouddlna.nextcloud.db
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
import mu.KLogging
|
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.ContentItem
|
||||||
import net.schowek.nextclouddlna.nextcloud.content.ContentNode
|
import net.schowek.nextclouddlna.nextcloud.content.ContentNode
|
||||||
import net.schowek.nextclouddlna.nextcloud.content.MediaFormat
|
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 net.schowek.nextclouddlna.nextcloud.db.Filecache.Companion.FOLDER_MIME_TYPE
|
||||||
import org.springframework.dao.InvalidDataAccessResourceUsageException
|
import org.springframework.dao.InvalidDataAccessResourceUsageException
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
|
@ -1,6 +1,6 @@
|
||||||
package net.schowek.nextclouddlna.nextcloud.content
|
package net.schowek.nextclouddlna.nextcloud.content
|
||||||
|
|
||||||
import net.schowek.nextclouddlna.nextcloud.NextcloudDB
|
import net.schowek.nextclouddlna.nextcloud.db.NextcloudDB
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
Loading…
Reference in a new issue