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);