commit
08210ad3eb
30 changed files with 464 additions and 146 deletions
52
README.md
52
README.md
|
@ -6,7 +6,33 @@ DLNA addon for your self-hosted Nextcloud app instance that allows you to stream
|
|||
devices in your network.
|
||||
It supports the group folders as well.
|
||||
|
||||
Just edit the `application.yml` and rebuild the project with:
|
||||
## Running in Docker
|
||||
You can use the docker image with nextcloud-dlna e.g.:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name="nextcloud-dlna" \
|
||||
--net=host \
|
||||
-v /path/to/nextcloud/app/ending/with/data:/nextcloud \
|
||||
-e NEXTCLOUD_DATA_DIR=/nextcloud \
|
||||
-e NEXTCLOUD_DB_HOST='<your_nextcloud_db_host_ip_here>' \
|
||||
-e NEXTCLOUD_DB_PASS='<your_nextcloud_db_pass_here>' \
|
||||
nextcloud-dlna
|
||||
```
|
||||
|
||||
or, if used together with the official Nextcloud docker image using the docker-composer. See the [examples](./examples)
|
||||
directory. for more details about running nextcloud-dlna server in the docker container.
|
||||
|
||||
You can pass to the container other env variables that are listed below.
|
||||
|
||||
Note that it would not work on Mac OS since docker is a Linux container and the `host` networking mode doesn't actually
|
||||
share the host's network interfaces.
|
||||
|
||||
See https://hub.docker.com/r/thanek/nextcloud-dlna for more docker image details.
|
||||
|
||||
## Building the project
|
||||
|
||||
Build the project with:
|
||||
|
||||
`./gradlew clean bootRun`
|
||||
|
||||
|
@ -18,6 +44,8 @@ or, if you've already built the project and created the jar file:
|
|||
|
||||
`NEXTCLOUD_DLNA_SERVER_PORT=9999 java -jar nextcloud-dlna-X.Y.Z.jar`
|
||||
|
||||
## ENV variables
|
||||
|
||||
Available env variables with their default values that you can overwrite:
|
||||
|
||||
| env variable | default value | description |
|
||||
|
@ -26,6 +54,7 @@ Available env variables with their default values that you can overwrite:
|
|||
| NEXTCLOUD_DLNA_INTERFACE | eth0 | interface the server will be listening on |
|
||||
| NEXTCLOUD_DLNA_FRIENDLY_NAME | Nextcloud-DLNA | friendly name of the DLNA service |
|
||||
| NEXTCLOUD_DATA_DIR | | nextcloud installation directory (that ends with /data) |
|
||||
| NEXTCLOUD_DB_TYPE | mariadb | nextcloud database type (mysql, mariadb, postgresql) |
|
||||
| NEXTCLOUD_DB_HOST | localhost | nextcloud database host |
|
||||
| NEXTCLOUD_DB_PORT | 3306 | nextcloud database port |
|
||||
| NEXTCLOUD_DB_NAME | nextcloud | nextcloud database name |
|
||||
|
@ -33,27 +62,6 @@ Available env variables with their default values that you can overwrite:
|
|||
| NEXTCLOUD_DB_PASS | nextcloud | nextcloud database password |
|
||||
|
||||
|
||||
## Running in Docker
|
||||
You can use the docker image with nextcloud-dlna e.g.:
|
||||
|
||||
```
|
||||
docker run -d \
|
||||
--name="nextcloud-dlna" \
|
||||
--net=host \
|
||||
-v /path/to/nextcloud/app/ending/with/data:/nextcloud \
|
||||
-e NEXTCLOUD_DATA_DIR=/nextcloud \
|
||||
-e NEXTCLOUD_DB_HOST='<your_nextcloud_db_host_ip_here>' \
|
||||
-e NEXTCLOUD_DB_PASS='<your_nextcloud_db_pass_here>' \
|
||||
nextcloud-dlna
|
||||
```
|
||||
|
||||
You can pass to the container other env variables that are listed above.
|
||||
|
||||
Note that it would not work on Mac OS since docker is a Linux container and the `host` networking mode doesn't actually
|
||||
share the host's network interfaces.
|
||||
|
||||
See https://hub.docker.com/r/thanek/nextcloud-dlna for more docker image details.
|
||||
|
||||
### Code used
|
||||
|
||||
Some java code was taken from https://github.com/haku/dlnatoad
|
||||
|
|
|
@ -42,9 +42,11 @@ dependencies {
|
|||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.mariadb.jdbc:mariadb-java-client:3.2.0'
|
||||
implementation 'org.postgresql:postgresql:42.6.0'
|
||||
|
||||
implementation 'org.jupnp:org.jupnp:2.7.1'
|
||||
implementation 'org.jupnp:org.jupnp.support:2.7.1'
|
||||
implementation 'org.osgi:org.osgi.service.http:1.2.2'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
// to avoid snakeyaml-1.3 vulnerability CVE-2022-1471
|
||||
implementation 'org.yaml:snakeyaml:2.2'
|
||||
|
|
84
examples/docker-compose/docker-compose.yaml
Normal file
84
examples/docker-compose/docker-compose.yaml
Normal file
|
@ -0,0 +1,84 @@
|
|||
version: '2'
|
||||
|
||||
volumes:
|
||||
app:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ${PWD}/app
|
||||
app_etc:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ${PWD}/etc/apache2
|
||||
db_data:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ${PWD}/db
|
||||
db_etc:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ${PWD}/etc/mysql
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.5
|
||||
restart: always
|
||||
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- db_etc:/etc/mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=sql
|
||||
- MYSQL_PASSWORD=secret
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
|
||||
app:
|
||||
image: nextcloud
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
links:
|
||||
- db
|
||||
- redis
|
||||
volumes:
|
||||
- app:/var/www/html
|
||||
- app_etc:/etc/apache2
|
||||
environment:
|
||||
- PHP_MEMORY_LIMIT=1G
|
||||
- PHP_UPLOAD_LIMIT=4G
|
||||
- MYSQL_PASSWORD=secret
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- MYSQL_HOST=db
|
||||
|
||||
dlna:
|
||||
image: thanek/nextcloud-dlna
|
||||
restart: always
|
||||
volumes:
|
||||
- app:/nextcloud
|
||||
network_mode: "host"
|
||||
ports:
|
||||
- "9999:9999"
|
||||
environment:
|
||||
- NEXTCLOUD_DLNA_SERVER_PORT=9999
|
||||
- NEXTCLOUD_DLNA_FRIENDLY_NAME=Nextcloud
|
||||
- NEXTCLOUD_DATA_DIR=/nextcloud/data
|
||||
- NEXTCLOUD_DB_TYPE=mariadb
|
||||
- NEXTCLOUD_DB_HOST=localhost
|
||||
- NEXTCLOUD_DB_PASS=secret
|
||||
|
13
examples/docker-compose/readme.md
Normal file
13
examples/docker-compose/readme.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
This will run the nextcloud-dlna in docker together with the full Nextcloud installation (containing the app, database
|
||||
and redis) located in the `./app` directory.
|
||||
|
||||
Note: in order to enable network access to the MariaDB server, after the first run, you'll need to edit
|
||||
the `./etc/mariadb.cnf`, section `[client-config]` by adding the line:
|
||||
```
|
||||
port = 3306
|
||||
```
|
||||
and removing the line:
|
||||
```
|
||||
socket = /var/run/mysqld/mysqld.sock
|
||||
```
|
||||
, then restart the `db` (`nextcloud-db-1`) container.
|
3
examples/docker-compose/run.sh
Normal file
3
examples/docker-compose/run.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker-compose up -d
|
2
examples/docker-standalone/readme.md
Normal file
2
examples/docker-standalone/readme.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
This will run the nextcloud-dlna in docker and connect to the Nextcloud installation assuming it is located in the
|
||||
`/opt/nextcloud` directory.
|
14
examples/docker-standalone/run.sh
Normal file
14
examples/docker-standalone/run.sh
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker run -d \
|
||||
--name="nextcloud-dlna" \
|
||||
--restart=unless-stopped \
|
||||
--net=host \
|
||||
-p 9999:9999 \
|
||||
-e NEXTCLOUD_DLNA_SERVER_PORT=9999 \
|
||||
-e NEXTCLOUD_DLNA_FRIENDLY_NAME="Nextcloud" \
|
||||
-e NEXTCLOUD_DB_HOST='localhost' \
|
||||
-e NEXTCLOUD_DB_PASS='secret' \
|
||||
-v '/opt/nextcloud/data:/nextcloud' \
|
||||
-e NEXTCLOUD_DATA_DIR=/nextcloud \
|
||||
thanek/nextcloud-dlna
|
|
@ -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"() {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
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 spock.util.concurrent.PollingConditions
|
||||
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 conditions = new PollingConditions(timeout: 1)
|
||||
|
||||
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:
|
||||
conditions.eventually {
|
||||
assert configuration.outgoingDatagramMessages.any()
|
||||
assert 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
|
||||
}
|
||||
}
|
|
@ -6,15 +6,21 @@ import org.springframework.beans.factory.annotation.Autowired
|
|||
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
|
||||
|
@ -27,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;
|
||||
}
|
||||
}
|
||||
|
|
7
src/integration/groovy/support/beans/TestConfig.groovy
Normal file
7
src/integration/groovy/support/beans/TestConfig.groovy
Normal file
|
@ -0,0 +1,7 @@
|
|||
package support.beans
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
|
||||
@ComponentScan(["support", "net.schowek.nextclouddlna"])
|
||||
class TestConfig {
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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
|
||||
import org.jupnp.transport.spi.StreamServer
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@Profile("integration")
|
||||
class UpnpServiceConfigurationInt extends DefaultUpnpServiceConfiguration {
|
||||
List<OutgoingDatagramMessage> outgoingDatagramMessages = new ArrayList<>()
|
||||
|
||||
@Override
|
||||
public StreamClient createStreamClient() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
|
||||
return null
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package net.schowek.nextclouddlna.nextcloud.config
|
||||
|
||||
package support.beans.nextcloud.config
|
||||
|
||||
import net.schowek.nextclouddlna.nextcloud.config.NextcloudAppPathProvider
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package net.schowek.nextclouddlna.util
|
||||
package support.beans.util
|
||||
|
||||
import net.schowek.nextclouddlna.util.ServerInfoProvider
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
|
@ -1,4 +1,4 @@
|
|||
package net.schowek.nextclouddlna.util
|
||||
package support.beans.util
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory
|
|
@ -1,90 +0,0 @@
|
|||
package net.schowek.nextclouddlna
|
||||
|
||||
import jakarta.annotation.PreDestroy
|
||||
import mu.KLogging
|
||||
import net.schowek.nextclouddlna.dlna.media.MediaServer
|
||||
import net.schowek.nextclouddlna.dlna.transport.ApacheStreamClient
|
||||
import net.schowek.nextclouddlna.dlna.transport.ApacheStreamClientConfiguration
|
||||
import net.schowek.nextclouddlna.dlna.transport.MyStreamServerConfiguration
|
||||
import net.schowek.nextclouddlna.dlna.transport.MyStreamServerImpl
|
||||
import net.schowek.nextclouddlna.util.ServerInfoProvider
|
||||
import org.jupnp.DefaultUpnpServiceConfiguration
|
||||
import org.jupnp.UpnpServiceConfiguration
|
||||
import org.jupnp.UpnpServiceImpl
|
||||
import org.jupnp.model.message.StreamRequestMessage
|
||||
import org.jupnp.model.message.StreamResponseMessage
|
||||
import org.jupnp.model.message.UpnpResponse
|
||||
import org.jupnp.protocol.ProtocolFactory
|
||||
import org.jupnp.registry.RegistryImpl
|
||||
import org.jupnp.transport.impl.NetworkAddressFactoryImpl
|
||||
import org.jupnp.transport.spi.NetworkAddressFactory
|
||||
import org.springframework.context.event.ContextRefreshedEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.stereotype.Component
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
|
||||
|
||||
@Component
|
||||
class DlnaService(
|
||||
private val mediaServer: MediaServer,
|
||||
private val serverInfoProvider: ServerInfoProvider,
|
||||
) {
|
||||
private val addressesToBind: List<String> = listOf(serverInfoProvider.host)
|
||||
var upnpService = MyUpnpService(MyUpnpServiceConfiguration())
|
||||
|
||||
fun start() {
|
||||
upnpService.startup()
|
||||
upnpService.registry.addDevice(mediaServer.device)
|
||||
}
|
||||
|
||||
@EventListener
|
||||
fun handleContextRefresh(event: ContextRefreshedEvent) {
|
||||
start()
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun destroy() {
|
||||
upnpService.shutdown()
|
||||
}
|
||||
|
||||
fun processRequest(requestMsg: StreamRequestMessage): StreamResponseMessage {
|
||||
logger.debug { "Processing $requestMsg" }
|
||||
return with(upnpService.protocolFactory.createReceivingSync(requestMsg)) {
|
||||
run()
|
||||
outputMessage
|
||||
?: StreamResponseMessage(UpnpResponse.Status.NOT_FOUND).also {
|
||||
logger.warn { "Could not get response for ${requestMsg.operation.method} ${requestMsg}" }
|
||||
}
|
||||
}.also {
|
||||
logger.debug { "Response: ${it.operation.statusCode} ${it.body}" }
|
||||
}
|
||||
}
|
||||
|
||||
inner class MyUpnpService(
|
||||
configuration: UpnpServiceConfiguration
|
||||
) : UpnpServiceImpl(configuration) {
|
||||
override fun createRegistry(pf: ProtocolFactory) = RegistryImpl(this)
|
||||
}
|
||||
|
||||
private inner class MyUpnpServiceConfiguration : DefaultUpnpServiceConfiguration(serverInfoProvider.port) {
|
||||
override fun createStreamClient() =
|
||||
ApacheStreamClient(ApacheStreamClientConfiguration(syncProtocolExecutorService))
|
||||
|
||||
override fun createStreamServer(networkAddressFactory: NetworkAddressFactory) =
|
||||
MyStreamServerImpl(MyStreamServerConfiguration(networkAddressFactory.streamListenPort))
|
||||
|
||||
override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) =
|
||||
MyNetworkAddressFactory(streamListenPort, multicastResponsePort)
|
||||
}
|
||||
|
||||
inner class MyNetworkAddressFactory(
|
||||
streamListenPort: Int,
|
||||
multicastResponsePort: Int
|
||||
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
|
||||
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
|
||||
addressesToBind.contains(address.hostAddress)
|
||||
}
|
||||
|
||||
companion object : KLogging()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package net.schowek.nextclouddlna.dlna
|
||||
|
||||
import jakarta.annotation.PreDestroy
|
||||
import mu.KLogging
|
||||
import org.jupnp.UpnpService
|
||||
import org.jupnp.model.message.StreamRequestMessage
|
||||
import org.jupnp.model.message.StreamResponseMessage
|
||||
import org.jupnp.model.message.UpnpResponse
|
||||
import org.springframework.context.event.ContextRefreshedEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
|
||||
@Component
|
||||
class DlnaService(
|
||||
private val upnpService: UpnpService,
|
||||
private val mediaServer: MediaServer
|
||||
) {
|
||||
fun start() {
|
||||
upnpService.startup()
|
||||
upnpService.registry.addDevice(mediaServer.device)
|
||||
}
|
||||
|
||||
@EventListener(condition = "!@environment.acceptsProfiles('integration')")
|
||||
fun handleContextRefresh(event: ContextRefreshedEvent) {
|
||||
start()
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun destroy() {
|
||||
upnpService.shutdown()
|
||||
}
|
||||
|
||||
fun processRequest(requestMsg: StreamRequestMessage): StreamResponseMessage {
|
||||
logger.debug { "Processing $requestMsg" }
|
||||
return with(upnpService.protocolFactory.createReceivingSync(requestMsg)) {
|
||||
run()
|
||||
outputMessage
|
||||
?: StreamResponseMessage(UpnpResponse.Status.NOT_FOUND).also {
|
||||
logger.warn { "Could not get response for ${requestMsg.operation.method} ${requestMsg}" }
|
||||
}
|
||||
}.also {
|
||||
logger.debug { "Response: ${it.operation.statusCode} ${it.body}" }
|
||||
}
|
||||
}
|
||||
|
||||
companion object : KLogging()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.schowek.nextclouddlna.dlna.media
|
||||
package net.schowek.nextclouddlna.dlna
|
||||
|
||||
import mu.KLogging
|
||||
import net.schowek.nextclouddlna.util.ExternalUrls
|
|
@ -0,0 +1,10 @@
|
|||
package net.schowek.nextclouddlna.dlna.upnp
|
||||
|
||||
import org.jupnp.UpnpServiceConfiguration
|
||||
import org.jupnp.UpnpServiceImpl
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class MyUpnpService(
|
||||
upnpServiceConfiguration: UpnpServiceConfiguration
|
||||
) : UpnpServiceImpl(upnpServiceConfiguration)
|
|
@ -0,0 +1,44 @@
|
|||
package net.schowek.nextclouddlna.dlna.upnp
|
||||
|
||||
import net.schowek.nextclouddlna.dlna.upnp.transport.ApacheStreamClient
|
||||
import net.schowek.nextclouddlna.dlna.upnp.transport.ApacheStreamClientConfiguration
|
||||
import net.schowek.nextclouddlna.dlna.upnp.transport.MyStreamServerConfiguration
|
||||
import net.schowek.nextclouddlna.dlna.upnp.transport.MyStreamServerImpl
|
||||
import net.schowek.nextclouddlna.util.ServerInfoProvider
|
||||
import org.jupnp.DefaultUpnpServiceConfiguration
|
||||
import org.jupnp.transport.impl.NetworkAddressFactoryImpl
|
||||
import org.jupnp.transport.spi.DatagramIO
|
||||
import org.jupnp.transport.spi.NetworkAddressFactory
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
|
||||
@Component
|
||||
@Profile("!integration")
|
||||
class MyUpnpServiceConfiguration(
|
||||
private val serverInfoProvider: ServerInfoProvider
|
||||
) : DefaultUpnpServiceConfiguration(serverInfoProvider.port) {
|
||||
val addressesToBind = listOf(serverInfoProvider.host)
|
||||
|
||||
override fun createStreamClient() =
|
||||
ApacheStreamClient(ApacheStreamClientConfiguration(syncProtocolExecutorService))
|
||||
|
||||
override fun createStreamServer(networkAddressFactory: NetworkAddressFactory) =
|
||||
MyStreamServerImpl(MyStreamServerConfiguration(networkAddressFactory.streamListenPort))
|
||||
|
||||
override fun createDatagramIO(networkAddressFactory: NetworkAddressFactory): DatagramIO<*> {
|
||||
return super.createDatagramIO(networkAddressFactory)
|
||||
}
|
||||
|
||||
override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) =
|
||||
MyNetworkAddressFactory(streamListenPort, multicastResponsePort)
|
||||
|
||||
inner class MyNetworkAddressFactory(
|
||||
streamListenPort: Int,
|
||||
multicastResponsePort: Int
|
||||
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
|
||||
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
|
||||
addressesToBind.contains(address.hostAddress)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package net.schowek.nextclouddlna.dlna.transport
|
||||
package net.schowek.nextclouddlna.dlna.upnp.transport
|
||||
|
||||
import mu.KLogging
|
||||
import org.apache.http.HttpMessage
|
|
@ -1,4 +1,4 @@
|
|||
package net.schowek.nextclouddlna.dlna.transport
|
||||
package net.schowek.nextclouddlna.dlna.upnp.transport
|
||||
|
||||
import org.jupnp.transport.spi.AbstractStreamClientConfiguration
|
||||
import java.util.concurrent.ExecutorService
|
|
@ -1,4 +1,4 @@
|
|||
package net.schowek.nextclouddlna.dlna.transport
|
||||
package net.schowek.nextclouddlna.dlna.upnp.transport
|
||||
|
||||
import org.jupnp.transport.spi.StreamServerConfiguration
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package net.schowek.nextclouddlna.dlna.transport
|
||||
package net.schowek.nextclouddlna.dlna.upnp.transport
|
||||
|
||||
import mu.KLogging
|
||||
import org.jupnp.transport.Router
|
|
@ -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<String, ContentNode> = HashMap()
|
||||
private val items: MutableMap<String, ContentItem> = 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,57 @@
|
|||
package net.schowek.nextclouddlna.util
|
||||
|
||||
import mu.KLogging
|
||||
import net.schowek.nextclouddlna.util.NextcloudDBType.*
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource
|
||||
|
||||
@Configuration
|
||||
@Profile("!integration")
|
||||
@EnableConfigurationProperties(NextcloudDBConfigProperties::class)
|
||||
class DriverManagerDataSourceConfig {
|
||||
@Bean
|
||||
fun driverManagerDataSource(props: NextcloudDBConfigProperties): DriverManagerDataSource {
|
||||
logger.info { "Using Nextcloud DB connection parameters: $props" }
|
||||
return DriverManagerDataSource().also { dataSource ->
|
||||
when (props.type) {
|
||||
MARIADB, MYSQL -> {
|
||||
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
|
||||
dataSource.url = "jdbc:mariadb://${props.host}:${props.port}/${props.name}";
|
||||
}
|
||||
|
||||
POSTGRES -> {
|
||||
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
|
||||
dataSource.url = "jdbc:postgresql://${props.host}:${props.port}/${props.name}";
|
||||
dataSource.connectionProperties?.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect")
|
||||
}
|
||||
|
||||
else -> throw RuntimeException("Unsupported DB type")
|
||||
}
|
||||
dataSource.username = props.user;
|
||||
dataSource.password = props.pass;
|
||||
}
|
||||
}
|
||||
|
||||
companion object : KLogging()
|
||||
}
|
||||
|
||||
@Profile("!integration")
|
||||
@ConfigurationProperties(prefix = "nextcloud.db")
|
||||
data class NextcloudDBConfigProperties(
|
||||
val type: NextcloudDBType,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val name: String,
|
||||
val user: String,
|
||||
val pass: String
|
||||
)
|
||||
|
||||
enum class NextcloudDBType(val value: String) {
|
||||
MYSQL("mysql"),
|
||||
MARIADB("mariadb"),
|
||||
POSTGRES("postgres")
|
||||
}
|
|
@ -5,13 +5,15 @@ server:
|
|||
|
||||
nextcloud:
|
||||
filesDir: ${NEXTCLOUD_DATA_DIR}
|
||||
db:
|
||||
type: ${NEXTCLOUD_DB_TYPE:mariadb}
|
||||
host: ${NEXTCLOUD_DB_HOST:localhost}
|
||||
port: ${NEXTCLOUD_DB_PORT:3306}
|
||||
name: ${NEXTCLOUD_DB_NAME:nextcloud}
|
||||
user: ${NEXTCLOUD_DB_USER:nextcloud}
|
||||
pass: ${NEXTCLOUD_DB_PASS:nextcloud}
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: "jdbc:mariadb://${NEXTCLOUD_DB_HOST:localhost}:${NEXTCLOUD_DB_PORT:3306}/${NEXTCLOUD_DB_NAME:nextcloud}"
|
||||
username: ${NEXTCLOUD_DB_USER:nextcloud}
|
||||
password: ${NEXTCLOUD_DB_PASS:nextcloud}
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue