diff --git a/.gitignore b/.gitignore
index 584675da285..08771cbb106 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ Californium.properties
generated/
userdata/
+
diff --git a/bundles/org.openhab.core.io.rest.core/.classpath b/bundles/org.openhab.core.io.rest.core/.classpath
index 4b18de0f966..fcbfd6cf1e9 100644
--- a/bundles/org.openhab.core.io.rest.core/.classpath
+++ b/bundles/org.openhab.core.io.rest.core/.classpath
@@ -8,14 +8,14 @@
-
+
-
+
diff --git a/bundles/org.openhab.core.io.rest.media/.classpath b/bundles/org.openhab.core.io.rest.media/.classpath
new file mode 100644
index 00000000000..fcf75521fe5
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/.classpath
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.io.rest.media/.project b/bundles/org.openhab.core.io.rest.media/.project
new file mode 100644
index 00000000000..205024c91e5
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.io.rest.media
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.io.rest.media/pom.xml b/bundles/org.openhab.core.io.rest.media/pom.xml
new file mode 100644
index 00000000000..a9c71d37f18
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 5.1.0-SNAPSHOT
+
+
+ org.openhab.core.io.rest.media
+
+ openHAB Core :: Bundles :: Media REST Interface
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.media
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.thing
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.rest
+ ${project.version}
+
+
+
+
diff --git a/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaDTO.java b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaDTO.java
new file mode 100644
index 00000000000..6b32d7f2e99
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaDTO.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.io.rest.media.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * A DTO that is used on the REST API to provide infos about {@link AudioSource} to UIs.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaDTO {
+ public String id;
+ public String path;
+ public String type;
+ public @Nullable String artUri;
+ public @Nullable String label;
+ public @Nullable String complement;
+
+ public MediaDTO(String id, String path, String type, String label) {
+ this.id = id;
+ this.path = path;
+ this.type = type;
+ this.label = label;
+ }
+
+ public void setArtUri(String artUri) {
+ this.artUri = artUri;
+ }
+
+ public void setComplement(String complement) {
+ this.complement = complement;
+ }
+}
diff --git a/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaDTOCollection.java b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaDTOCollection.java
new file mode 100644
index 00000000000..63ac864d32a
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaDTOCollection.java
@@ -0,0 +1,21 @@
+package org.openhab.core.io.rest.media.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaDTOCollection extends MediaDTO {
+ private final List childs;
+
+ public MediaDTOCollection(String id, String path, String type, String label) {
+ super(id, path, type, label);
+ childs = new ArrayList<>();
+ }
+
+ public void addMediaDTO(MediaDTO mediaDTO) {
+ childs.add(mediaDTO);
+ }
+
+ public List getChilds() {
+ return childs;
+ }
+}
diff --git a/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaResource.java b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaResource.java
new file mode 100644
index 00000000000..e4ec7406174
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaResource.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.io.rest.media.internal;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.auth.Role;
+import org.openhab.core.io.rest.LocaleService;
+import org.openhab.core.io.rest.RESTConstants;
+import org.openhab.core.io.rest.RESTResource;
+import org.openhab.core.items.Item;
+import org.openhab.core.items.ItemRegistry;
+import org.openhab.core.media.MediaDevice;
+import org.openhab.core.media.MediaListenner;
+import org.openhab.core.media.MediaService;
+import org.openhab.core.media.internal.MediaServlet;
+import org.openhab.core.media.model.MediaCollection;
+import org.openhab.core.media.model.MediaEntry;
+import org.openhab.core.media.model.MediaRegistry;
+import org.openhab.core.media.model.MediaSearchResult;
+import org.openhab.core.media.model.MediaSource;
+import org.openhab.core.media.model.MediaTrack;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
+import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
+import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
+import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
+import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+/**
+ * This class acts as a REST resource for audio features.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@Component
+@JaxrsResource
+@JaxrsName(MediaResource.PATH_MEDIA)
+@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
+@JSONRequired
+@Path(MediaResource.PATH_MEDIA)
+@RolesAllowed({ Role.USER, Role.ADMIN })
+@Tag(name = MediaResource.PATH_MEDIA)
+@NonNullByDefault
+public class MediaResource implements RESTResource {
+
+ private final Logger logger = LoggerFactory.getLogger(MediaResource.class);
+
+ /** The URI path to this resource */
+ public static final String PATH_MEDIA = "media";
+
+ private final MediaService mediaService;
+ private final LocaleService localeService;
+ private final ItemRegistry itemRegistry;
+ private final ItemChannelLinkRegistry itemChannelLinkRegistry;
+
+ @Activate
+ public MediaResource( //
+ final @Reference MediaService mediaService, final @Reference LocaleService localeService,
+ final @Reference ItemRegistry itemRegistry,
+ final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry) {
+ this.mediaService = mediaService;
+ this.localeService = localeService;
+ this.itemRegistry = itemRegistry;
+ this.itemChannelLinkRegistry = itemChannelLinkRegistry;
+ }
+
+ @GET
+ @Path("/sources")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(operationId = "getMediaSources", summary = "Get the list of all sources.", responses = {
+ @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = MediaDTO.class)))) })
+ public Response getSources(@Context UriInfo uriInfo,
+ @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
+ @QueryParam("path") @Parameter(description = "path of the ressource") @Nullable String path,
+ @QueryParam("query") @Parameter(description = "a search query") @Nullable String query,
+ @QueryParam("start") @Parameter(description = "start index to get") long start,
+ @QueryParam("size") @Parameter(description = "number of element to get") long size) {
+ final Locale locale = localeService.getLocale(language);
+
+ MediaRegistry registry = mediaService.getMediaRegistry();
+
+ URI uri = uriInfo.getRequestUri();
+ String scheme = uri.getScheme();
+ String host = uri.getHost();
+ int port = uri.getPort();
+
+ if (port == -1) {
+ if (scheme.equals("http")) {
+ port = 80;
+ } else if (scheme.equals("https")) {
+ port = 443;
+ }
+ }
+
+ // Handle specific dev case
+ if (port == 8081) {
+ port = 8080;
+ }
+
+ String baseUri = scheme + "://" + host + ":" + port + MediaServlet.SERVLET_PATH;
+ mediaService.setBaseUri(baseUri);
+
+ if (path == null || path.isEmpty()) {
+ path = "/Root";
+ }
+ MediaEntry entry = registry.getEntry(path);
+
+ if (path.startsWith("/Root/Search") || path.startsWith("/Root/CurrentQueue")) {
+ Map allMediaListenner = mediaService.getAllMediaListenner();
+ for (String key : allMediaListenner.keySet()) {
+ if (key.equals("/Root")) {
+ continue;
+ }
+ if (entry instanceof MediaSearchResult) {
+ ((MediaSearchResult) entry).setSearchQuery("" + query);
+ }
+ MediaListenner mediaListenner = allMediaListenner.get(key);
+ if (entry != null) {
+ mediaListenner.refreshEntry(entry, start, size);
+ }
+ }
+
+ } else {
+ MediaSource mediaSource = entry.getMediaSource(false);
+ String key = "/Root";
+ if (mediaSource != null) {
+ key = mediaSource.getKey();
+ }
+
+ MediaListenner mediaListenner = mediaService.getMediaListenner(key);
+ if (mediaListenner != null) {
+ mediaListenner.refreshEntry(entry, start, size);
+ }
+ }
+
+ if (entry instanceof MediaCollection) {
+ MediaDTOCollection dtoCollection = constructResponse(entry, start, size);
+ return Response.ok(dtoCollection).build();
+ } else {
+ MediaDTO dto = new MediaDTO(entry.getKey(), entry.getPath(), entry.getClass().getTypeName(),
+ entry.getName());
+ return Response.ok(dto).build();
+ }
+
+ }
+
+ public MediaDTOCollection constructResponse(MediaEntry entry, long start, long size) {
+ MediaCollection col = (MediaCollection) entry;
+
+ MediaDTOCollection dtoCollection = new MediaDTOCollection(entry.getKey(), entry.getPath(),
+ entry.getClass().getTypeName(), entry.getName());
+ String artUriCol = col.getExternalArtUri();
+ dtoCollection.setArtUri(artUriCol);
+
+ // for (String key : col.getChilds().keySet()) {
+
+ List colEntries = col.getChildsAsArray();
+ for (long idx = start; idx < start + size && idx < colEntries.size(); idx++) {
+ MediaEntry subEntry = colEntries.get((int) idx);
+
+ MediaDTO dto;
+ if (entry instanceof MediaSearchResult) {
+ dto = constructResponse(subEntry, start, size);
+ } else if (subEntry instanceof MediaCollection) {
+ dto = new MediaDTOCollection(subEntry.getKey(), subEntry.getPath(), subEntry.getClass().getTypeName(),
+ subEntry.getName());
+ dto.setArtUri(((MediaCollection) subEntry).getExternalArtUri());
+ } else {
+ dto = new MediaDTO(subEntry.getKey(), subEntry.getPath(), subEntry.getClass().getTypeName(),
+ subEntry.getName());
+
+ if (subEntry instanceof MediaTrack mediaTrack) {
+ dto.setArtUri(mediaTrack.getArtUri());
+ dto.setComplement(mediaTrack.getArtist());
+
+ }
+ }
+
+ dtoCollection.addMediaDTO(dto);
+ }
+
+ return dtoCollection;
+ }
+
+ @GET
+ @Path("/sinks")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(operationId = "getMediaSinks", summary = "Get the list of all sinks.", responses = {
+ @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = MediaSinkDTO.class)))) })
+ public Response getSinks(
+ @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
+ @QueryParam("path") @Parameter(description = "path of the ressource") @Nullable String path) {
+ final Locale locale = localeService.getLocale(language);
+
+ Map devices = mediaService.getMediaDevices();
+
+ Collection- colItem = itemRegistry.getItemsOfType("Player");
+ for (Item item : colItem) {
+
+ Set r1 = itemChannelLinkRegistry.getBoundChannels(item.getName());
+ Set r2 = itemChannelLinkRegistry.getBoundThings(item.getName());
+
+ logger.debug("");
+ }
+
+ MediaSinkDTOCollection dtoCol = new MediaSinkDTOCollection();
+ for (MediaDevice device : devices.values()) {
+ MediaSinkDTO dto = new MediaSinkDTO(device.getId(), device.getName(), device.getType(),
+ device.getBinding());
+
+ for (Item item : colItem) {
+ Set r1 = itemChannelLinkRegistry.getBoundChannels(item.getName());
+ for (ChannelUID ruid : r1) {
+ if (ruid.getBindingId().equals(device.getBinding())
+ && ruid.getThingUID().getId().equals(device.getId())) {
+ dto.setPlayerItemName(item.getName());
+ }
+ }
+ }
+
+ dtoCol.addMediaSinkDTO(dto);
+ }
+
+ return Response.ok(dtoCol).build();
+ }
+}
diff --git a/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaSinkDTO.java b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaSinkDTO.java
new file mode 100644
index 00000000000..4f353c6126d
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaSinkDTO.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.io.rest.media.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A DTO that is used on the REST API to provide infos about {@link AudioSource} to UIs.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaSinkDTO {
+ private String id;
+ private String name;
+ private String type;
+ private String binding;
+ private String playerItemName;
+
+ public MediaSinkDTO(String id, String name, String type, String binding) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ this.binding = binding;
+ this.playerItemName = "";
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public String getBinding() {
+ return this.binding;
+ }
+
+ public String getPlayerItemName() {
+ return this.getPlayerItemName();
+ }
+
+ public void setPlayerItemName(String playerItemName) {
+ this.playerItemName = playerItemName;
+ }
+}
diff --git a/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaSinkDTOCollection.java b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaSinkDTOCollection.java
new file mode 100644
index 00000000000..033f99d697c
--- /dev/null
+++ b/bundles/org.openhab.core.io.rest.media/src/main/java/org/openhab/core/io/rest/media/internal/MediaSinkDTOCollection.java
@@ -0,0 +1,21 @@
+package org.openhab.core.io.rest.media.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaSinkDTOCollection extends MediaSinkDTO {
+ private final List childs;
+
+ public MediaSinkDTOCollection() {
+ super("id", "name", "type", "binding");
+ childs = new ArrayList<>();
+ }
+
+ public void addMediaSinkDTO(MediaSinkDTO mediaSinkDTO) {
+ childs.add(mediaSinkDTO);
+ }
+
+ public List getChilds() {
+ return childs;
+ }
+}
diff --git a/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/internal/UpnpIOServiceImpl.java b/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/internal/UpnpIOServiceImpl.java
index 5fd6f8507ee..80617fbb7b2 100644
--- a/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/internal/UpnpIOServiceImpl.java
+++ b/bundles/org.openhab.core.io.transport.upnp/src/main/java/org/openhab/core/io/transport/upnp/internal/UpnpIOServiceImpl.java
@@ -148,13 +148,13 @@ protected void established(GENASubscription subscription) {
@Override
protected void eventReceived(GENASubscription sub) {
Map values = sub.getCurrentValues();
- Device deviceRoot = sub.getService().getDevice().getRoot();
+ Device device = sub.getService().getDevice();
String serviceId = sub.getService().getServiceId().getId();
logger.trace("Receiving a GENA subscription '{}' response for device '{}'", serviceId,
- deviceRoot.getIdentity().getUdn());
+ device.getIdentity().getUdn());
for (UpnpIOParticipant participant : participants) {
- if (Objects.equals(getDevice(participant), deviceRoot)) {
+ if (Objects.equals(getDevice(participant), device)) {
for (Entry entry : values.entrySet()) {
Object value = entry.getValue().getValue();
if (value != null) {
diff --git a/bundles/org.openhab.core.media/.classpath b/bundles/org.openhab.core.media/.classpath
new file mode 100644
index 00000000000..fcf75521fe5
--- /dev/null
+++ b/bundles/org.openhab.core.media/.classpath
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.media/.project b/bundles/org.openhab.core.media/.project
new file mode 100644
index 00000000000..1b38ba300f9
--- /dev/null
+++ b/bundles/org.openhab.core.media/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.media
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.media/pom.xml b/bundles/org.openhab.core.media/pom.xml
new file mode 100644
index 00000000000..ea0051ee9e0
--- /dev/null
+++ b/bundles/org.openhab.core.media/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 5.1.0-SNAPSHOT
+
+
+ org.openhab.core.media
+
+ openHAB Core :: Bundles :: Media
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core.config.core
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.console
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.http
+ ${project.version}
+ compile
+
+
+ org.openhab.core.bundles
+ org.openhab.core.io.net
+ ${project.version}
+ compile
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.8.1
+
+
+ embed-dependencies
+
+ unpack-dependencies
+
+
+ runtime
+ jar
+ javax.activation,org.apache.karaf.features,org.openhab.core.bundles
+ ${project.build.directory}/classes
+ true
+ true
+ true
+ jar
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/BaseDto.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/BaseDto.java
new file mode 100644
index 00000000000..7c307030a6f
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/BaseDto.java
@@ -0,0 +1,61 @@
+package org.openhab.core.media;
+
+import java.util.List;
+
+import org.openhab.core.media.model.MediaEntry;
+
+/**
+ * A base class for all DTO use in Media Service
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+public class BaseDto {
+ private String id;
+ private String type;
+ private String uri;
+ private String name;
+ private List images;
+
+ public String getKey() {
+ return id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public List getImages() {
+ return images;
+ }
+
+ public String getArtwork() {
+ try {
+ List imagesList = getImages();
+ if (imagesList != null && imagesList.getFirst() != null) {
+ return imagesList.getFirst().getUrl();
+ }
+ return "";
+ } catch (Exception ex) {
+ return "";
+ }
+ }
+
+ public void initFields(MediaEntry entry) {
+ }
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/Image.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/Image.java
new file mode 100644
index 00000000000..9c42d53e801
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/Image.java
@@ -0,0 +1,19 @@
+package org.openhab.core.media;
+
+public class Image {
+ private Integer height;
+ private String url;
+ private Integer width;
+
+ public Integer getHeight() {
+ return height;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public Integer getWidth() {
+ return width;
+ }
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaDevice.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaDevice.java
new file mode 100644
index 00000000000..c60910535f1
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaDevice.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is an interface that is
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaDevice {
+ private String id;
+ private String name;
+ private String type;
+ private String binding;
+
+ public MediaDevice(String id, String name, String type, String binding) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ this.binding = binding;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public String getBinding() {
+ return this.binding;
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaHTTPServer.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaHTTPServer.java
new file mode 100644
index 00000000000..7e7e95d6f67
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaHTTPServer.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is an interface that is
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface MediaHTTPServer {
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaListenner.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaListenner.java
new file mode 100644
index 00000000000..9ed9f332125
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaListenner.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.media.model.MediaEntry;
+
+/**
+ * This is an interface
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface MediaListenner {
+ void refreshEntry(MediaEntry mediaEntry, long start, long size);
+
+ String getStreamUri(String val);
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaService.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaService.java
new file mode 100644
index 00000000000..002ed8f6ae9
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.media.model.MediaEntry;
+import org.openhab.core.media.model.MediaRegistry;
+
+/**
+ * This is an interface that is
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface MediaService {
+ public MediaRegistry getMediaRegistry();
+
+ public Map getMediaDevices();
+
+ public void addMediaListenner(String key, MediaListenner mediaListenner);
+
+ public Map getAllMediaListenner();
+
+ public @Nullable MediaListenner getMediaListenner(String key);
+
+ public void registerDevice(MediaDevice device);
+
+ public @Nullable String getProxy(String key);
+
+ public String handleImageProxy(String uri);
+
+ public void setBaseUri(String baseUri);
+
+ public void RegisterCollections(MediaEntry parentEntry,
+ List collection, Class allocator);
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaServiceFactory.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaServiceFactory.java
new file mode 100644
index 00000000000..20b6ce62cae
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/MediaServiceFactory.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is an interface
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface MediaServiceFactory {
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/Test.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/Test.java
new file mode 100644
index 00000000000..7c8fe09beae
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/Test.java
@@ -0,0 +1,67 @@
+package org.openhab.core.media;
+
+import org.openhab.core.media.internal.MediaServiceImpl;
+import org.openhab.core.media.model.MediaAlbum;
+import org.openhab.core.media.model.MediaArtist;
+import org.openhab.core.media.model.MediaCollection;
+import org.openhab.core.media.model.MediaRegistry;
+import org.openhab.core.media.model.MediaSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Test {
+ private final Logger logger = LoggerFactory.getLogger(Test.class);
+
+ public static void main(String args[]) {
+
+ new Test().Go();
+ }
+
+ public void Go() {
+ MediaService mediaService = new MediaServiceImpl();
+ MediaRegistry mediaRegistry = mediaService.getMediaRegistry();
+
+ MediaSource mediaSource = mediaRegistry.registerEntry("Spotify", () -> {
+ return new MediaSource("Spotify", "SpotifyName");
+ });
+
+ MediaCollection mediaAlbums = mediaSource.registerEntry("Albums", () -> {
+ return new MediaCollection("Albums", "Albums");
+ });
+
+ MediaCollection mediaArtists = mediaSource.registerEntry("Artists", () -> {
+ return new MediaCollection("Artists", "Artists");
+ });
+
+ @SuppressWarnings("unused")
+ MediaCollection mediaPlaylist = mediaSource.registerEntry("Playlists", () -> {
+ return new MediaCollection("Playlists", "Playlists");
+ });
+
+ @SuppressWarnings("unused")
+ MediaAlbum mediaAlbum = mediaAlbums.registerEntry("Album_1", () -> {
+ return new MediaAlbum("Album_1", "Another day to die");
+ });
+
+ @SuppressWarnings("unused")
+ MediaAlbum mediaAlbum2 = mediaAlbums.registerEntry("Album_2", () -> {
+ return new MediaAlbum("Album_2", "Samedi soir sur terre");
+ });
+
+ @SuppressWarnings("unused")
+ MediaArtist mediaArtiste = mediaArtists.registerEntry("Artist_1", () -> {
+ return new MediaArtist("Artist_1", "Dire Straits");
+ });
+
+ @SuppressWarnings("unused")
+ MediaArtist mediaArtiste2 = mediaArtists.registerEntry("Artist_2", () -> {
+ return new MediaArtist("Artist_2", "Francis Cabrel");
+ });
+
+ mediaRegistry.print();
+
+ logger.debug("");
+
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServiceFactoryImpl.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServiceFactoryImpl.java
new file mode 100644
index 00000000000..ba7765b3db8
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServiceFactoryImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.media.MediaServiceFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Implementation of
+ *
+ * @author Laurent Arnal - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class MediaServiceFactoryImpl implements MediaServiceFactory {
+
+ @Activate
+ public MediaServiceFactoryImpl() {
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServiceImpl.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServiceImpl.java
new file mode 100644
index 00000000000..6a1882918c0
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServiceImpl.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.internal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.media.BaseDto;
+import org.openhab.core.media.MediaDevice;
+import org.openhab.core.media.MediaListenner;
+import org.openhab.core.media.MediaService;
+import org.openhab.core.media.model.MediaEntry;
+import org.openhab.core.media.model.MediaQueue;
+import org.openhab.core.media.model.MediaRegistry;
+import org.openhab.core.media.model.MediaSearchResult;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of
+ *
+ * @author Laurent Arnal - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class MediaServiceImpl implements MediaService, MediaListenner {
+ private final Logger logger = LoggerFactory.getLogger(MediaServiceImpl.class);
+
+ private Map mediaListenner = new HashMap();
+ private Map mediaDevices = new HashMap();
+
+ public @Nullable MediaListenner listenner = null;
+ public MediaRegistry registry = new MediaRegistry(this);
+
+ private Map proxyRegistry = new HashMap();
+ private String baseUri = "none";
+
+ @Activate
+ public MediaServiceImpl() {
+ this.addProxySource("lyrionUpnp1", "http://192.168.254.1:9000/");
+ this.addProxySource("lyrionUpnp2", "http://192.168.0.1:9000/");
+ this.addProxySource("emby", "http://192.168.254.1:8096/");
+ this.addMediaListenner("/Root", this);
+
+ }
+
+ @Override
+ public void setBaseUri(String baseUri) {
+ this.baseUri = baseUri;
+ }
+
+ @Override
+ public String handleImageProxy(String uri) {
+ String result = uri;
+ for (String key : proxyRegistry.keySet()) {
+ String proxyUri = proxyRegistry.get(key);
+ if (proxyUri != null) {
+ result = result.replace(proxyUri, baseUri + "/proxy/" + key + "/");
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void refreshEntry(MediaEntry mediaEntry, long start, long size) {
+ if (mediaEntry instanceof MediaRegistry) {
+ mediaEntry.registerEntry("Search", () -> {
+ MediaSearchResult searchResult = new MediaSearchResult("Search", "Search");
+ return searchResult;
+ });
+ mediaEntry.registerEntry("CurrentQueue", () -> {
+ MediaQueue currentMediaQueue = new MediaQueue("CurrentQueue", "CurrentQueue");
+ return currentMediaQueue;
+ });
+ }
+ }
+
+ @Override
+ public @Nullable String getProxy(String key) {
+ if (!proxyRegistry.containsKey(key)) {
+ return null;
+ }
+
+ return proxyRegistry.get(key);
+ }
+
+ @Override
+ public String getStreamUri(String cmdVal) {
+ return "";
+ }
+
+ public void addProxySource(String source, String uri) {
+ proxyRegistry.put(source, uri);
+ }
+
+ @Override
+ public MediaRegistry getMediaRegistry() {
+ return registry;
+ }
+
+ @Override
+ public void addMediaListenner(String key, MediaListenner listenner) {
+ mediaListenner.put(key, listenner);
+ }
+
+ @Override
+ public Map getAllMediaListenner() {
+ return this.mediaListenner;
+ }
+
+ @Override
+ public @Nullable MediaListenner getMediaListenner(String key) {
+ // TODO Auto-generated method stub
+ if (mediaListenner.containsKey(key)) {
+ return mediaListenner.get(key);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void registerDevice(MediaDevice device) {
+ mediaDevices.put(device.getId(), device);
+ }
+
+ @Override
+ public Map getMediaDevices() {
+ return mediaDevices;
+ }
+
+ public Map getMediaListenners() {
+ return mediaListenner;
+ }
+
+ @Override
+ public void RegisterCollections(MediaEntry parentEntry,
+ List collection, Class allocator) {
+ for (T dto : collection) {
+ String key = dto.getKey();
+ String name = dto.getName();
+
+ parentEntry.registerEntry(key, () -> {
+ try {
+ MediaEntry mediaEntry = allocator.getDeclaredConstructor().newInstance();
+ mediaEntry.setName(name);
+ mediaEntry.setKey(key);
+
+ // Let mediaEntry and dto subclass handle specific fields initialization
+ mediaEntry.initFrom(dto);
+ dto.initFields(mediaEntry);
+
+ return mediaEntry;
+ } catch (Exception ex) {
+ return null;
+ }
+ });
+ }
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServlet.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServlet.java
new file mode 100644
index 00000000000..f776809979d
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/internal/MediaServlet.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.internal;
+
+import java.io.IOException;
+import java.io.Serial;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.io.net.http.TrustAllTrustManager;
+import org.openhab.core.media.MediaHTTPServer;
+import org.openhab.core.media.MediaListenner;
+import org.openhab.core.media.MediaService;
+import org.openhab.core.media.model.MediaAlbum;
+import org.openhab.core.media.model.MediaArtist;
+import org.openhab.core.media.model.MediaCollection;
+import org.openhab.core.media.model.MediaEntry;
+import org.openhab.core.media.model.MediaPlayList;
+import org.openhab.core.media.model.MediaRegistry;
+import org.openhab.core.media.model.MediaSource;
+import org.openhab.core.media.model.MediaTrack;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletName;
+import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A servlet that
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@Component(service = { MediaHTTPServer.class, Servlet.class })
+@HttpWhiteboardServletName(MediaServlet.SERVLET_PATH)
+@HttpWhiteboardServletPattern(MediaServlet.SERVLET_PATH + "/*")
+@NonNullByDefault
+public class MediaServlet extends HttpServlet implements MediaHTTPServer {
+
+ @Serial
+ private static final long serialVersionUID = -3364664035854567854L;
+
+ private static final List WAV_MIME_TYPES = List.of("audio/wav", "audio/x-wav", "audio/vnd.wave");
+
+ // A 1MB in memory buffer will help playing multiple times an AudioStream, if the sink cannot do otherwise
+ private static final int ONETIME_STREAM_BUFFER_MAX_SIZE = 1048576;
+ // 5MB max for a file buffer
+ private static final int ONETIME_STREAM_FILE_MAX_SIZE = 5242880;
+
+ public final MediaService mediaService;
+
+ public static final String SERVLET_PATH = "/media";
+
+ private final Logger logger = LoggerFactory.getLogger(MediaServlet.class);
+
+ private final ScheduledExecutorService threadPool = ThreadPoolManager
+ .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
+
+ private final HttpClientFactory httpClientFactory;
+ private final HttpClient httpClient;
+
+ private String baseUri = "";
+
+ @Nullable
+ ScheduledFuture> periodicCleaner;
+
+ private static final int REQUEST_BUFFER_SIZE = 8000;
+ private static final int RESPONSE_BUFFER_SIZE = 200000;
+
+ @Activate
+ public MediaServlet(@Reference MediaService mediaService, @Reference HttpClientFactory httpClientFactory) {
+ super();
+ logger.info("constructor");
+ this.mediaService = mediaService;
+ this.httpClientFactory = httpClientFactory;
+
+ SslContextFactory sslContextFactory = new SslContextFactory.Client();
+ try {
+ SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, new TrustManager[] { TrustAllTrustManager.getInstance() }, null);
+ sslContextFactory.setSslContext(sslContext);
+ } catch (NoSuchAlgorithmException e) {
+ logger.warn("An exception occurred while requesting the SSL encryption algorithm : '{}'", e.getMessage(),
+ e);
+ } catch (KeyManagementException e) {
+ logger.warn("An exception occurred while initialising the SSL context : '{}'", e.getMessage(), e);
+ }
+
+ this.httpClient = httpClientFactory.createHttpClient("media");
+ // , sslContextFactory);
+ this.httpClient.setFollowRedirects(false);
+ this.httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
+ this.httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
+
+ }
+
+ @Activate
+ protected void activate(ComponentContext componentContext) {
+ logger.info("activate");
+ try {
+ httpClient.start();
+ } catch (Exception e) {
+ logger.warn("Unable to start Jetty HttpClient {}", e.getMessage());
+ }
+ }
+
+ @Deactivate
+ protected synchronized void deactivate() {
+ }
+
+ private void handleProxy(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
+ throws ServletException, IOException {
+ ServletOutputStream stream = resp.getOutputStream();
+
+ try {
+ String urlPath = req.getPathInfo();
+ int idxProxy = urlPath.indexOf("proxy/");
+ urlPath = urlPath.substring(idxProxy + 6);
+
+ int idxProxyName = urlPath.indexOf("/");
+ String proxyName = urlPath.substring(0, idxProxyName);
+
+ if (mediaService.getProxy(proxyName) == null) {
+ throw new Exception(String.format("proxyName %s not registered", proxyName));
+ }
+ String targetUri = mediaService.getProxy(proxyName);
+
+ urlPath = urlPath.substring(idxProxyName + 1);
+
+ String uri = targetUri + urlPath;
+
+ Request request = httpClient.newRequest(uri);
+
+ request = request.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0");
+ request = request.method(HttpMethod.GET);
+ ContentResponse result = request.send();
+
+ if (result.getStatus() == HttpStatus.OK_200) {
+ byte[] contents = result.getContent();
+ resp.setContentType(result.getMediaType());
+ stream.write(contents, 0, contents.length);
+ } else {
+ resp.setStatus(404);
+ }
+
+ } catch (
+
+ Exception ex) {
+ throw new ServletException("Error", ex);
+
+ }
+
+ }
+
+ @Override
+ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
+ throws ServletException, IOException {
+ String requestURI = req.getRequestURI();
+ if (requestURI == null) {
+ resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "requestURI is null");
+ return;
+ }
+
+ baseUri = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + req.getServletPath();
+
+ String qs = req.getQueryString();
+ String path = req.getParameter("path");
+ String urlPath = req.getPathInfo();
+ if (urlPath != null && urlPath.startsWith("/proxy")) {
+ handleProxy(req, resp);
+ return;
+ }
+
+ ServletOutputStream stream = resp.getOutputStream();
+ final StringBuilder sb = new StringBuilder(5000);
+
+ if (path == null) {
+ path = "/Root";
+ }
+
+ MediaRegistry mediaRegistry = mediaService.getMediaRegistry();
+ MediaEntry currentEntry = mediaRegistry.getEntry(path);
+
+ if (currentEntry != null) {
+ MediaSource mediaSource = currentEntry.getMediaSource(false);
+ if (mediaSource != null) {
+ MediaListenner mediaListenner = mediaService.getMediaListenner(mediaSource.getKey());
+ if (mediaListenner != null) {
+ mediaListenner.refreshEntry(currentEntry, 0, 0);
+ }
+ }
+ }
+
+ MediaEntry recurseEntry = currentEntry;
+ while (recurseEntry != null) {
+ sb.insert(0, " > " + recurseEntry.getName()
+ + "");
+ recurseEntry = recurseEntry.getParent();
+ }
+
+ sb.append("
");
+
+ if (currentEntry instanceof MediaCollection) {
+ MediaCollection col = (MediaCollection) currentEntry;
+ int idx = 0;
+
+ if (currentEntry instanceof MediaAlbum) {
+ MediaAlbum album = (MediaAlbum) currentEntry;
+ sb.append("Album:" + album.getName());
+ sb.append("
");
+ }
+
+ for (String key : col.getChildsAsMap().keySet()) {
+ MediaEntry entry = col.getChildsAsMap().get(key);
+
+ if (entry instanceof MediaAlbum) {
+ MediaAlbum album = (MediaAlbum) entry;
+ sb.append(
+ "");
+ idx++;
+ } else if (entry instanceof MediaArtist) {
+ MediaArtist artist = (MediaArtist) entry;
+ sb.append(
+ "");
+ idx++;
+ } else if (entry instanceof MediaTrack) {
+ MediaTrack track = (MediaTrack) entry;
+ sb.append("");
+ idx++;
+ } else if (entry instanceof MediaPlayList) {
+ MediaPlayList playList = (MediaPlayList) entry;
+ sb.append(
+ "");
+ idx++;
+ } else if (entry instanceof MediaCollection) {
+ MediaCollection collection = (MediaCollection) entry;
+ sb.append(
+ "");
+ idx++;
+ } else {
+ sb.append(
+ "" + entry.getName() + "
");
+ }
+ }
+ }
+
+ resp.setContentType("text/html; charset=utf-8");
+ stream.write(sb.toString().getBytes(StandardCharsets.UTF_8));
+
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaAlbum.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaAlbum.java
new file mode 100644
index 00000000000..4efedff0652
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaAlbum.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaAlbum extends MediaCollection {
+
+ private String artist = "";
+ private String genre = "";
+
+ public MediaAlbum() {
+
+ }
+
+ public MediaAlbum(String key, String albumName) {
+ super(key, albumName);
+
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public String getGenre() {
+ return this.genre;
+ }
+
+ public String getArtist() {
+ return this.artist;
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaAllocator.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaAllocator.java
new file mode 100644
index 00000000000..d009dd68738
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaAllocator.java
@@ -0,0 +1,7 @@
+package org.openhab.core.media.model;
+
+@FunctionalInterface
+public interface MediaAllocator {
+ T alloc();
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaArtist.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaArtist.java
new file mode 100644
index 00000000000..38c1d015fb3
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaArtist.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaArtist extends MediaCollection {
+
+ public MediaArtist() {
+
+ }
+
+ public MediaArtist(String key, String artistName) {
+ super(key, artistName);
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaCollection.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaCollection.java
new file mode 100644
index 00000000000..7ac648e85d0
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaCollection.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.media.BaseDto;
+import org.openhab.core.media.MediaService;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaCollection extends MediaEntry {
+ private Map maps;
+ private List list = new ArrayList<>();
+ public String artUri = "/static/playlist.png";
+
+ public MediaCollection() {
+ maps = new HashMap();
+ list = new ArrayList();
+ }
+
+ public void Clear() {
+ list.clear();
+ maps.clear();
+ }
+
+ public MediaCollection(String key, String name) {
+ super(key, name);
+
+ if (name.indexOf("Artistes") >= 0) {
+ artUri = "/static/Artists.png";
+ } else if (name.indexOf("Albums") >= 0) {
+ artUri = "/static/Albums.png";
+ } else if (name.indexOf("Dossiers") >= 0) {
+ artUri = "/static/Folder.png";
+ }
+
+ maps = new HashMap();
+ list = new ArrayList();
+ }
+
+ public MediaCollection(String key, String name, String artUri) {
+ super(key, name);
+
+ this.artUri = artUri;
+ maps = new HashMap();
+ }
+
+ public Map getChildsAsMap() {
+ return maps;
+ }
+
+ public List getChildsAsArray() {
+ return list;
+ }
+
+ @Override
+ public void print() {
+ super.print();
+
+ for (MediaEntry child : list) {
+ child.print();
+ }
+ }
+
+ @Override
+ public void addChild(String key, MediaEntry childEntry) {
+ if (!maps.containsKey(key)) {
+ maps.put(key, childEntry);
+ list.add(childEntry);
+ }
+ }
+
+ public String getArtUri() {
+ return artUri;
+ }
+
+ public String getExternalArtUri() {
+ MediaService mediaService = this.getMediaRegistry().getMediaService();
+ String result = mediaService.handleImageProxy(artUri);
+ return result;
+ }
+
+ public void setArtUri(String artUri) {
+ this.artUri = artUri;
+ }
+
+ @Override
+ public void initFrom(BaseDto dto) {
+ this.artUri = dto.getArtwork();
+
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaEntry.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaEntry.java
new file mode 100644
index 00000000000..50b677f24f4
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaEntry.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.media.BaseDto;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaEntry {
+ private final Logger logger = LoggerFactory.getLogger(MediaEntry.class);
+
+ private @Nullable MediaEntry parent;
+ private @Nullable MediaRegistry registry;
+ private String key;
+ private String name;
+
+ public MediaEntry() {
+ key = "";
+ name = "";
+ }
+
+ public MediaEntry(String key, String name) {
+ this.name = name;
+ this.key = key;
+ }
+
+ public T registerEntry(String key, MediaAllocator allocator) {
+ registry = getMediaRegistry();
+
+ String entryPath = getPath() + "/" + key;
+ T result = (T) registry.getEntry(entryPath);
+ if (result == null) {
+ result = allocator.alloc();
+ result.setParent(this);
+ if (registry != null) {
+ registry.addEntry(result);
+ }
+
+ addChild(key, result);
+ } else {
+ addChild(key, result);
+ }
+
+ return result;
+ }
+
+ public void addChild(String key, MediaEntry childEntry) {
+
+ }
+
+ public @Nullable MediaSource getMediaSource(boolean first) {
+ if (this instanceof MediaSource) {
+ if (parent != null && parent instanceof MediaSource && !first) {
+ return parent.getMediaSource(first);
+ } else {
+ return (MediaSource) this;
+ }
+ }
+
+ if (getParent() != null) {
+ return getParent().getMediaSource(first);
+ }
+
+ return null;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public @Nullable MediaEntry getParent() {
+ return parent;
+ }
+
+ public void setParent(MediaEntry parent) {
+ this.parent = parent;
+ }
+
+ public @Nullable MediaRegistry getMediaRegistry() {
+ MediaEntry entry = this;
+ if (entry instanceof MediaRegistry) {
+ return (MediaRegistry) this;
+ }
+
+ while ((entry = entry.getParent()) != null) {
+ if (entry instanceof MediaRegistry) {
+ return (MediaRegistry) entry;
+ }
+ }
+ return null;
+ }
+
+ public String getPath() {
+ if (parent != null) {
+ return parent.getPath() + "/" + getKey();
+
+ } else {
+ return "/Root";
+ }
+ }
+
+ public String getSubPath() {
+ if (parent != null && !(parent instanceof MediaSource)) {
+ return parent.getSubPath() + "/" + getKey();
+
+ } else {
+ return "/" + getKey();
+ }
+ }
+
+ public int getLevel() {
+ if (parent != null) {
+ return parent.getLevel() + 1;
+ } else {
+ return 0;
+ }
+ }
+
+ private String empty = " ";
+
+ public void print() {
+ int level = getLevel();
+ logger.debug(String.format("%s %d - MediaEntry %s - %s", empty.substring(0, level * 4), level, key, name));
+ }
+
+ public void initFrom(BaseDto dto) {
+
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaPlayList.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaPlayList.java
new file mode 100644
index 00000000000..2e7416c19bf
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaPlayList.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaPlayList extends MediaCollection {
+
+ public MediaPlayList() {
+
+ }
+
+ public MediaPlayList(String key, String playListName) {
+ super(key, playListName);
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaPodcast.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaPodcast.java
new file mode 100644
index 00000000000..cef54fff762
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaPodcast.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaPodcast extends MediaCollection {
+
+ public MediaPodcast() {
+
+ }
+ public MediaPodcast(String key, String albumName) {
+ super(key, albumName);
+
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaQueue.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaQueue.java
new file mode 100644
index 00000000000..5d3bad3f231
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaQueue.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaQueue extends MediaCollection {
+
+ public MediaQueue() {
+
+ }
+
+ public MediaQueue(String key, String albumName) {
+ super(key, albumName);
+
+ }
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaRegistry.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaRegistry.java
new file mode 100644
index 00000000000..bdb869932f6
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaRegistry.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.media.MediaService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaRegistry extends MediaCollection {
+ private final Logger logger = LoggerFactory.getLogger(MediaEntry.class);
+ private final MediaService mediaService;
+ private Map pathToEntry;
+
+ public MediaRegistry(MediaService mediaService) {
+ super("Root", "Registry");
+ pathToEntry = new HashMap();
+ pathToEntry.put("/Root", this);
+
+ this.mediaService = mediaService;
+ }
+
+ public void addEntry(MediaEntry mediaEntry) {
+ pathToEntry.put(mediaEntry.getPath(), mediaEntry);
+ }
+
+ public @Nullable MediaEntry getEntry(String path) {
+ return pathToEntry.get(path);
+ }
+
+ public MediaService getMediaService() {
+ return mediaService;
+ }
+
+ @Override
+ public void print() {
+ logger.debug("Registry:");
+ super.print();
+
+ }
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaSearchResult.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaSearchResult.java
new file mode 100644
index 00000000000..33530284511
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaSearchResult.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaSearchResult extends MediaCollection {
+ public String searchQuery = "";
+
+ public MediaSearchResult() {
+
+ }
+
+ public MediaSearchResult(String key, String albumName) {
+ super(key, albumName);
+
+ }
+
+ public String getSearchQuery() {
+ return searchQuery;
+ }
+
+ public void setSearchQuery(String searchQuery) {
+ this.searchQuery = searchQuery;
+ }
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaSource.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaSource.java
new file mode 100644
index 00000000000..56973fd5b83
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaSource.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaSource extends MediaCollection {
+
+ public MediaSource() {
+
+ }
+
+ public MediaSource(String key, String sourceName) {
+ super(key, sourceName);
+ }
+
+ public MediaSource(String key, String sourceName, String artUri) {
+ super(key, sourceName);
+
+ this.artUri = artUri;
+ }
+
+}
diff --git a/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaTrack.java b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaTrack.java
new file mode 100644
index 00000000000..0eefa6ab979
--- /dev/null
+++ b/bundles/org.openhab.core.media/src/main/java/org/openhab/core/media/model/MediaTrack.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.media.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.media.BaseDto;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaTrack extends MediaEntry {
+ private String artUri = "/static/Artists.png";
+ private String artist = "";
+
+ public MediaTrack() {
+
+ }
+
+ public MediaTrack(String key, String trackName) {
+ super(key, trackName);
+ }
+
+ public void setArtUri(String artUri) {
+ this.artUri = artUri;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getArtUri() {
+ return this.artUri;
+ }
+
+ public String getArtist() {
+ return this.artist;
+ }
+
+ @Override
+ public void initFrom(BaseDto dto) {
+ this.artUri = dto.getArtwork();
+ }
+
+}
diff --git a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java
index 340f3f5f041..2606d031ba2 100644
--- a/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java
+++ b/bundles/org.openhab.core.model.item/src/org/openhab/core/model/item/internal/GenericItemProvider.java
@@ -556,7 +556,11 @@ private Map toItemMap(@Nullable Collection- items) {
};
Item baseItem = createItemOfType(baseItemType, modelItem.getName());
- return applyGroupFunction(baseItem, modelItem, function);
+ if (baseItem != null) {
+ return applyGroupFunction(baseItem, modelItem, function);
+ } else {
+ return null;
+ }
}
/**
diff --git a/bundles/org.openhab.core.thing/schema/thing/thing-description-1.0.0.xsd b/bundles/org.openhab.core.thing/schema/thing/thing-description-1.0.0.xsd
index a6c18b8cae1..4155b08f60a 100644
--- a/bundles/org.openhab.core.thing/schema/thing/thing-description-1.0.0.xsd
+++ b/bundles/org.openhab.core.thing/schema/thing/thing-description-1.0.0.xsd
@@ -200,6 +200,7 @@
+
diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java
index e38270a0c41..ce261764644 100644
--- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java
+++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java
@@ -326,18 +326,26 @@ private Switch createPlayerButtons() {
final Switch playerItemSwitch = SitemapFactory.eINSTANCE.createSwitch();
final List mappings = playerItemSwitch.getMappings();
Mapping commandMapping;
+
mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping());
commandMapping.setCmd(NextPreviousType.PREVIOUS.name());
commandMapping.setLabel("<<");
+
mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping());
commandMapping.setCmd(PlayPauseType.PAUSE.name());
commandMapping.setLabel("||");
+
mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping());
commandMapping.setCmd(PlayPauseType.PLAY.name());
commandMapping.setLabel(">");
+
mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping());
commandMapping.setCmd(NextPreviousType.NEXT.name());
commandMapping.setLabel(">>");
+
+ // mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping());
+ // commandMapping.setCmd(MediaType.CHANGE.name());
+ // commandMapping.setLabel(".");
return playerItemSwitch;
}
diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java
index dd7b88a3a9b..acefeb52550 100644
--- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/CoreItemFactory.java
@@ -61,8 +61,7 @@ public class CoreItemFactory implements ItemFactory {
public static final String SWITCH = "Switch";
public static final Set VALID_ITEM_TYPES = Set.of( //
- CALL, COLOR, CONTACT, DATETIME, DIMMER, IMAGE, LOCATION, NUMBER, PLAYER, ROLLERSHUTTER, STRING, SWITCH //
- );
+ CALL, COLOR, CONTACT, DATETIME, DIMMER, IMAGE, LOCATION, NUMBER, PLAYER, ROLLERSHUTTER, STRING, SWITCH);
private final UnitProvider unitProvider;
diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/PlayerItem.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/PlayerItem.java
index 087bdb47c76..aad77d1fa6b 100644
--- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/PlayerItem.java
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/items/PlayerItem.java
@@ -18,9 +18,12 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GenericItem;
import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.MediaCommandType;
+import org.openhab.core.library.types.MediaStateType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
@@ -36,9 +39,10 @@
public class PlayerItem extends GenericItem {
private static final List> ACCEPTED_DATA_TYPES = List.of(PlayPauseType.class,
- RewindFastforwardType.class, UnDefType.class);
+ RewindFastforwardType.class, MediaStateType.class, StringType.class, UnDefType.class);
private static final List> ACCEPTED_COMMAND_TYPES = List.of(PlayPauseType.class,
- RewindFastforwardType.class, NextPreviousType.class, RefreshType.class);
+ RewindFastforwardType.class, NextPreviousType.class, MediaCommandType.class, StringType.class,
+ RefreshType.class);
public PlayerItem(String name) {
super(CoreItemFactory.PLAYER, name);
@@ -63,7 +67,7 @@ public List> getAcceptedCommandTypes() {
*
* @param command the command to be sent
*/
- public void send(PlayPauseType command) {
+ public void send(MediaCommandType command) {
internalSend(command, null);
}
@@ -129,8 +133,8 @@ public void setState(State state) {
@Override
public void setTimeSeries(TimeSeries timeSeries) {
- if (timeSeries.getStates()
- .allMatch(s -> s.state() instanceof PlayPauseType || s.state() instanceof RewindFastforwardType)) {
+ if (timeSeries.getStates().allMatch(s -> s.state() instanceof PlayPauseType
+ || s.state() instanceof RewindFastforwardType || s.state() instanceof MediaCommandType)) {
applyTimeSeries(timeSeries);
} else {
logSetTypeError(timeSeries);
diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaCommandEnumType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaCommandEnumType.java
new file mode 100644
index 00000000000..1c2cf6b7f4e
--- /dev/null
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaCommandEnumType.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.library.types;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.PrimitiveType;
+
+/**
+ * This type is used by the {@link org.openhab.core.library.items.PlayerItem}.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public enum MediaCommandEnumType implements PrimitiveType, Command {
+ NONE,
+ PLAY,
+ ENQUEUE,
+ DEVICE,
+ PAUSE,
+ NEXT,
+ PREVIOUS,
+ REWIND,
+ FASTFORWARD,
+ SEARCH,
+ VOLUME;
+
+ @Override
+ public String format(String pattern) {
+ return String.format(pattern, this.toString());
+ }
+
+ @Override
+ public String toString() {
+ return toFullString();
+ }
+
+ @Override
+ public String toFullString() {
+ return super.toString();
+ }
+}
diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaCommandType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaCommandType.java
new file mode 100644
index 00000000000..43dcad26bb1
--- /dev/null
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaCommandType.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.library.types;
+
+import java.time.ZonedDateTime;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.events.AbstractEventFactory.ZonedDateTimeAdapter;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.ComplexType;
+import org.openhab.core.types.PrimitiveType;
+import org.openhab.core.types.State;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * This type is used by the {@link org.openhab.core.library.items.PlayerItem}.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaCommandType implements ComplexType, State, Command {
+
+ public static final String KEY_COMMAND = "command";
+ public static final String KEY_PARAM = "param";
+ public static final String KEY_DEVICE = "device";
+ public static final String KEY_BINDING = "binding";
+
+ private final MediaCommandEnumType command;
+ private final String param;
+ private final StringType device;
+ private final StringType binding;
+
+ private static final Gson JSONCONVERTER = new GsonBuilder()
+ .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()).create();
+
+ public MediaCommandType() {
+ this(MediaCommandEnumType.NONE, "", new StringType(""), new StringType(""));
+ }
+
+ public MediaCommandType(MediaCommandEnumType command, @Nullable String param, @Nullable StringType device,
+ @Nullable StringType binding) {
+ this.command = command;
+ this.param = param != null ? param : "";
+ this.device = device != null ? device : new StringType("");
+ this.binding = binding != null ? binding : new StringType("");
+ }
+
+ @Override
+ public String toString() {
+ return toFullString();
+ }
+
+ @Override
+ public String toFullString() {
+ return JSONCONVERTER.toJson(this);
+ // return this.state.toFullString() + "," + this.command.toFullString() + "," + param + "," + device + ","
+ // + binding;
+ }
+
+ public static MediaCommandType valueOf(String value) {
+ try {
+ MediaCommandType res = JSONCONVERTER.fromJson(value, MediaCommandType.class);
+ if (res == null) {
+ return new MediaCommandType();
+ }
+ return res;
+ } catch (Exception ex) {
+ throw ex;
+ }
+ }
+
+ @Override
+ public String format(String pattern) {
+ return String.format(pattern, param);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(command, param, device, binding);
+ }
+
+ @Override
+ public SortedMap getConstituents() {
+ TreeMap map = new TreeMap<>();
+ map.put(KEY_COMMAND, getCommand());
+ map.put(KEY_PARAM, getCommand());
+ map.put(KEY_DEVICE, getDevice());
+ map.put(KEY_BINDING, getBinding());
+ return map;
+ }
+
+ public MediaCommandEnumType getCommand() {
+ return command;
+ }
+
+ public StringType getParam() {
+ return new StringType(param);
+ }
+
+ public StringType getDevice() {
+ return device;
+ }
+
+ public StringType getBinding() {
+ return binding;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof String) {
+ return obj.equals(param);
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ MediaCommandType other = (MediaCommandType) obj;
+ return Objects.equals(this.device, other.device) && Objects.equals(this.param, other.param)
+ && Objects.equals(this.device, other.device) && Objects.equals(this.binding, other.binding);
+ }
+}
diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaStateType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaStateType.java
new file mode 100644
index 00000000000..7c35e3a6d9e
--- /dev/null
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/MediaStateType.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.core.library.types;
+
+import java.time.ZonedDateTime;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.events.AbstractEventFactory.ZonedDateTimeAdapter;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.ComplexType;
+import org.openhab.core.types.PrimitiveType;
+import org.openhab.core.types.State;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * This type is used by the {@link org.openhab.core.library.items.PlayerItem}.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class MediaStateType implements ComplexType, State, Command {
+
+ public static final String KEY_STATE = "state";
+ public static final String KEY_DEVICE = "device";
+ public static final String KEY_BINDING = "binding";
+
+ private final PlayPauseType state;
+ private final StringType device;
+ private final StringType binding;
+ private StringType currentPlayingArtistName;
+ private StringType currentPlayingTrackName;
+ private StringType currentPlayingArtUri;
+ private DecimalType currentPlayingTrackPosition;
+ private DecimalType currentPlayingTrackDuration;
+ private DecimalType currentPlayingVolume;
+
+ private static final Gson JSONCONVERTER = new GsonBuilder()
+ .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()).create();
+
+ public MediaStateType() {
+ this(PlayPauseType.PLAY, new StringType(""), new StringType(""));
+ }
+
+ public MediaStateType(PlayPauseType state, @Nullable StringType device, @Nullable StringType binding) {
+ this.state = state;
+ this.device = device != null ? device : new StringType("");
+ this.binding = binding != null ? binding : new StringType("");
+ this.currentPlayingArtistName = new StringType("");
+ this.currentPlayingTrackName = new StringType("");
+ this.currentPlayingArtUri = new StringType("");
+ this.currentPlayingTrackPosition = new DecimalType("0.0");
+ this.currentPlayingTrackDuration = new DecimalType("0.0");
+ this.currentPlayingVolume = new DecimalType("0.0");
+ }
+
+ @Override
+ public String toString() {
+ return toFullString();
+ }
+
+ @Override
+ public String toFullString() {
+ return JSONCONVERTER.toJson(this);
+ // return this.state.toFullString() + "," + this.command.toFullString() + "," + param + "," + device + ","
+ // + binding;
+ }
+
+ public void setCurrentPlayingPosition(double value) {
+ this.currentPlayingTrackPosition = new DecimalType(value);
+ }
+
+ public void setCurrentPlayingTrackDuration(double value) {
+ this.currentPlayingTrackDuration = new DecimalType(value);
+ }
+
+ public void setCurrentPlayingVolume(double value) {
+ this.currentPlayingVolume = new DecimalType(value);
+ }
+
+ public void setCurrentPlayingArtistName(String artistName) {
+ this.currentPlayingArtistName = new StringType(artistName);
+ }
+
+ public void setCurrentPlayingTrackName(String trackName) {
+ this.currentPlayingTrackName = new StringType(trackName);
+ }
+
+ public void setCurrentPlayingArtUri(String artUri) {
+ this.currentPlayingArtUri = new StringType(artUri);
+ }
+
+ public static MediaStateType valueOf(String value) {
+ try {
+ MediaStateType res = JSONCONVERTER.fromJson(value, MediaStateType.class);
+ if (res == null) {
+ return new MediaStateType();
+ }
+ return res;
+ } catch (Exception ex) {
+ throw ex;
+ }
+ }
+
+ @Override
+ public String format(String pattern) {
+ return String.format(pattern, state, device, binding);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(device, binding);
+ }
+
+ @Override
+ public SortedMap getConstituents() {
+ TreeMap map = new TreeMap<>();
+ map.put(KEY_STATE, getState());
+ map.put(KEY_DEVICE, getDevice());
+ map.put(KEY_BINDING, getBinding());
+ return map;
+ }
+
+ public PlayPauseType getState() {
+ return state;
+ }
+
+ public StringType getDevice() {
+ return device;
+ }
+
+ public StringType getBinding() {
+ return binding;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ MediaStateType other = (MediaStateType) obj;
+ return Objects.equals(this.state, other.state) && Objects.equals(this.device, other.device)
+ && Objects.equals(this.binding, other.binding)
+ && Objects.equals(this.currentPlayingArtistName, other.currentPlayingArtistName)
+ && Objects.equals(this.currentPlayingTrackName, other.currentPlayingTrackName)
+ && Objects.equals(this.currentPlayingArtUri, other.currentPlayingArtUri)
+ && Objects.equals(this.currentPlayingTrackPosition, other.currentPlayingTrackPosition)
+ && Objects.equals(this.currentPlayingTrackDuration, other.currentPlayingTrackDuration)
+ && Objects.equals(this.currentPlayingVolume, other.currentPlayingVolume);
+ }
+}
diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PlayPauseType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PlayPauseType.java
index f3b5c4d046a..a378fb787a4 100644
--- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PlayPauseType.java
+++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/PlayPauseType.java
@@ -25,6 +25,7 @@
*/
@NonNullByDefault
public enum PlayPauseType implements PrimitiveType, State, Command {
+ NONE,
PLAY,
PAUSE;
diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/PlayerItemTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/PlayerItemTest.java
index 64468036f7e..f51510a8221 100644
--- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/PlayerItemTest.java
+++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/PlayerItemTest.java
@@ -16,8 +16,12 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
+import org.openhab.core.library.types.MediaCommandEnumType;
+import org.openhab.core.library.types.MediaCommandType;
+import org.openhab.core.library.types.MediaStateType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
+import org.openhab.core.library.types.StringType;
/**
*
@@ -46,6 +50,32 @@ public void setRewindFastforward() {
assertEquals(RewindFastforwardType.FASTFORWARD, item.getState());
}
+ @Test
+ public void setMediaType() {
+ PlayerItem item = new PlayerItem("test");
+ item.setState(new MediaCommandType(MediaCommandEnumType.NONE, "", new StringType(""), new StringType("")));
+
+ /*
+ * assertEquals(MediaCommandType.class, item.getState().getClass());
+ * MediaCommandType mt = (MediaCommandType) item.getState();
+ * assertEquals(PlayPauseType.NONE, mt.getState());
+ * assertEquals(MediaCommandEnumType.NONE, mt.getCommand());
+ */
+ }
+
+ @Test
+ public void setMediaStateType() {
+ PlayerItem item = new PlayerItem("test");
+ item.setState(new MediaStateType(PlayPauseType.NONE, new StringType(""), new StringType("")));
+
+ /*
+ * assertEquals(MediaCommandType.class, item.getState().getClass());
+ * MediaCommandType mt = (MediaCommandType) item.getState();
+ * assertEquals(PlayPauseType.NONE, mt.getState());
+ * assertEquals(MediaCommandEnumType.NONE, mt.getCommand());
+ */
+ }
+
@Test
public void testUndefType() {
PlayerItem item = new PlayerItem("test");
diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/StateUtil.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/StateUtil.java
index a936323afd3..8fd517ea438 100644
--- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/StateUtil.java
+++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/items/StateUtil.java
@@ -26,6 +26,9 @@
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.MediaCommandEnumType;
+import org.openhab.core.library.types.MediaCommandType;
+import org.openhab.core.library.types.MediaStateType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
@@ -93,6 +96,9 @@ public static List getAllStates() {
states.add(UpDownType.UP);
states.add(UpDownType.DOWN);
+ states.add(new MediaCommandType(MediaCommandEnumType.NONE, "", new StringType(""), new StringType("")));
+ states.add(new MediaStateType(PlayPauseType.NONE, new StringType(""), new StringType("")));
+
QuantityType quantityType = new QuantityType<>("12 °C");
states.add(quantityType);