From 7994a010290a13fb58f0c434402ad729c83679d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 May 2024 00:03:49 +0000 Subject: [PATCH 1/6] Bump org.springframework.boot:spring-boot-starter-parent Bumps [org.springframework.boot:spring-boot-starter-parent](https://github.com/spring-projects/spring-boot) from 3.2.5 to 3.3.0. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.2.5...v3.3.0) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-parent dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f9f8b15d9..755fc7699 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.5 + 3.3.0 From 06b8eb9ed21dc313b1aadedf23c362c0235cdffc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 23:48:34 +0000 Subject: [PATCH 2/6] Bump com.puppycrawl.tools:checkstyle from 10.16.0 to 10.17.0 Bumps [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) from 10.16.0 to 10.17.0. - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-10.16.0...checkstyle-10.17.0) --- updated-dependencies: - dependency-name: com.puppycrawl.tools:checkstyle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f9f8b15d9..49283d8b6 100644 --- a/pom.xml +++ b/pom.xml @@ -251,7 +251,7 @@ com.puppycrawl.tools checkstyle - 10.16.0 + 10.17.0 From 113c6fdcd689f60a7a1c870bfd6693514b5f65c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 23:48:37 +0000 Subject: [PATCH 3/6] Bump org.apache.maven:maven-artifact from 3.9.6 to 3.9.7 Bumps [org.apache.maven:maven-artifact](https://github.com/apache/maven) from 3.9.6 to 3.9.7. - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-3.9.6...maven-3.9.7) --- updated-dependencies: - dependency-name: org.apache.maven:maven-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- airsonic-main/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index d79e4b500..4ad485bd4 100644 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -464,7 +464,7 @@ org.apache.maven maven-artifact - 3.9.6 + 3.9.7 From d35a1219c4f74040da79fa122ebacd357c507d52 Mon Sep 17 00:00:00 2001 From: "Y.Tory" <5343692+kagemomiji@users.noreply.github.com> Date: Wed, 29 May 2024 15:55:21 +0000 Subject: [PATCH 4/6] kagemomiji/airsonic-advanced#474 fix auth bug at stream endpoint from upnp player --- .../player/controller/StreamController.java | 11 +- .../player/service/SecurityService.java | 5 +- .../airsonic/player/service/UPnPService.java | 151 +++++------ .../service/upnp/AlbumUpnpProcessor.java | 62 +++-- .../upnp/ApacheUpnpServiceConfiguration.java | 50 ++++ .../service/upnp/ArtistUpnpProcessor.java | 44 +++- .../service/upnp/CustomContentDirectory.java | 89 +------ .../upnp/DispatchingContentDirectory.java | 242 ++---------------- .../upnp/FolderBasedContentDirectory.java | 30 +-- .../service/upnp/GenreUpnpProcessor.java | 23 +- .../service/upnp/MediaFileUpnpProcessor.java | 71 +++-- .../service/upnp/PlaylistUpnpProcessor.java | 23 +- .../player/service/upnp/ProcessorType.java | 57 +++++ .../upnp/RecentAlbumUpnpProcessor.java | 11 +- .../service/upnp/RootUpnpProcessor.java | 17 +- .../service/upnp/UpnpContentProcessor.java | 47 +--- .../service/upnp/UpnpProcessorRouter.java | 39 +++ .../service/upnp/UpnpProcessorRouterImpl.java | 107 ++++++++ .../player/service/upnp/UpnpUtil.java | 115 +++++++++ 19 files changed, 656 insertions(+), 538 deletions(-) create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/upnp/ApacheUpnpServiceConfiguration.java create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/upnp/ProcessorType.java create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouter.java create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouterImpl.java create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpUtil.java diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java index efa2adc1e..4d30fcef7 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java @@ -114,12 +114,13 @@ public ResponseEntity handleRequest(Authentication authentication, @RequestParam(required = false, name = "offsetSeconds") Double offsetSeconds, ServletWebRequest swr) throws Exception { - User user = securityService.getCurrentUser(swr.getRequest()); + String username = securityService.getCurrentUsername(swr.getRequest()); + User user = securityService.getUserByName(username); if (!(authentication instanceof JWTAuthenticationToken) && !user.isStreamRole()) { - throw new AccessDeniedException("Streaming is forbidden for user " + user.getUsername()); + throw new AccessDeniedException("Streaming is forbidden for user " + username); } - Player player = playerService.getPlayer(swr.getRequest(), swr.getResponse(), user.getUsername(), false, true); + Player player = playerService.getPlayer(swr.getRequest(), swr.getResponse(), username, false, true); Long expectedSize = null; @@ -154,8 +155,8 @@ public ResponseEntity handleRequest(Authentication authentication, if (isSingleFile) { if (!(authentication instanceof JWTAuthenticationToken) - && !securityService.isFolderAccessAllowed(file, user.getUsername())) { - throw new AccessDeniedException("Access to file " + file.getId() + " is forbidden for user " + user.getUsername()); + && !securityService.isFolderAccessAllowed(file, username)) { + throw new AccessDeniedException("Access to file " + file.getId() + " is forbidden for user " + username); } // Update the index of the currently playing media file. At diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java index 0ee0f0bcf..878463902 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java @@ -52,6 +52,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; @@ -307,7 +308,6 @@ public List getCredentials(String username, App... apps) { return userRepository.findByUsername(username).map(user -> { return userCredentialRepository.findByUserAndAppIn(user, List.of(apps)); }).orElseGet(() -> { - LOG.warn("Can't get credentials for a non-existent user {}", username); return Collections.emptyList(); }); } @@ -420,7 +420,8 @@ public String getCurrentUsername(HttpServletRequest request) { * @param username The username used when logging in. * @return The user, or null if not found. */ - public User getUserByName(String username) { + @Nullable + public User getUserByName(@Nullable String username) { return getUserByName(username, true); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/UPnPService.java b/airsonic-main/src/main/java/org/airsonic/player/service/UPnPService.java index 94ae697b1..a54792dce 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/UPnPService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/UPnPService.java @@ -14,15 +14,15 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service; +import org.airsonic.player.service.upnp.ApacheUpnpServiceConfiguration; import org.airsonic.player.service.upnp.CustomContentDirectory; import org.airsonic.player.service.upnp.MSMediaReceiverRegistrarService; -import org.airsonic.player.util.FileUtil; -import org.fourthline.cling.DefaultUpnpServiceConfiguration; import org.fourthline.cling.UpnpService; import org.fourthline.cling.UpnpServiceImpl; import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder; @@ -36,13 +36,6 @@ import org.fourthline.cling.support.model.ProtocolInfos; import org.fourthline.cling.support.model.dlna.DLNAProfiles; import org.fourthline.cling.support.model.dlna.DLNAProtocolInfo; -import org.fourthline.cling.transport.impl.apache.StreamClientConfigurationImpl; -import org.fourthline.cling.transport.impl.apache.StreamClientImpl; -import org.fourthline.cling.transport.impl.apache.StreamServerConfigurationImpl; -import org.fourthline.cling.transport.impl.apache.StreamServerImpl; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; -import org.fourthline.cling.transport.spi.StreamClient; -import org.fourthline.cling.transport.spi.StreamServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -51,7 +44,9 @@ import jakarta.annotation.PostConstruct; +import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -67,9 +62,14 @@ public class UPnPService { private static final Logger LOG = LoggerFactory.getLogger(UPnPService.class); + private final static int MIN_ADVERTISEMENT_AGE_SECONDS = 60 * 60 * 24; + @Autowired private SettingsService settingsService; + @Autowired + private VersionService versionService; + private UpnpService upnpService; @Autowired @@ -103,20 +103,15 @@ public void ensureServiceStarted() { public void ensureServiceStopped() { running.getAndUpdate(bo -> { - if (bo) { - if (upnpService != null) { - LOG.info("Disabling UPnP/DLNA media server"); - upnpService.getRegistry().removeAllLocalDevices(); - System.err.println("Shutting down UPnP service..."); - upnpService.shutdown(); - System.err.println("Shutting down UPnP service - Done!"); - } - return false; - } else { - return false; + if (upnpService != null && bo) { + LOG.info("Disabling UPnP/DLNA media server"); + upnpService.getRegistry().removeAllLocalDevices(); + LOG.info("Shutting down UPnP service..."); + upnpService.shutdown(); + LOG.info("Shutting down UPnP service - Done!"); } + return false; }); - } private void startService() { @@ -132,7 +127,8 @@ private void startService() { private synchronized void createService() { upnpService = new UpnpServiceImpl(new ApacheUpnpServiceConfiguration(settingsService.getUPnpPort())); - // Asynch search for other devices (most importantly UPnP-enabled routers for port-mapping) + // Asynch search for other devices (most importantly UPnP-enabled routers for + // port-mapping) upnpService.getControlPoint().search(); } @@ -153,26 +149,9 @@ public void setMediaServerEnabled(boolean enabled) { private LocalDevice createMediaServerDevice() throws Exception { - String serverName = settingsService.getDlnaServerName(); - String serverId = settingsService.getDlnaServerId(); - if (serverId == null) { - serverId = UUID.randomUUID().toString(); - settingsService.setDlnaServerId(serverId); - } - DeviceIdentity identity = new DeviceIdentity(UDN.valueOf(serverId)); - DeviceType type = new UDADeviceType("MediaServer", 1); - - // TODO: DLNACaps - - DeviceDetails details = new DeviceDetails(serverName, new ManufacturerDetails(serverName), - new ModelDetails(serverName), - new DLNADoc[]{new DLNADoc("DMS", DLNADoc.Version.V1_5)}, null); - - InputStream in = getClass().getResourceAsStream("logo-512.png"); - Icon icon = new Icon("image/png", 512, 512, 32, "logo-512", in); - FileUtil.closeQuietly(in); - - LocalService contentDirectoryservice = new AnnotationLocalServiceBinder().read(CustomContentDirectory.class); + @SuppressWarnings("unchecked") + LocalService contentDirectoryservice = new AnnotationLocalServiceBinder() + .read(CustomContentDirectory.class); contentDirectoryservice.setManager(new DefaultServiceManager(contentDirectoryservice) { @Override @@ -193,25 +172,61 @@ protected CustomContentDirectory createServiceInstance() { } } - LocalService connetionManagerService = new AnnotationLocalServiceBinder().read(ConnectionManagerService.class); - connetionManagerService.setManager(new DefaultServiceManager(connetionManagerService) { - @Override - protected ConnectionManagerService createServiceInstance() { - return new ConnectionManagerService(protocols, null); - } - }); + @SuppressWarnings("unchecked") + LocalService connetionManagerService = new AnnotationLocalServiceBinder() + .read(ConnectionManagerService.class); + connetionManagerService + .setManager(new DefaultServiceManager(connetionManagerService) { + @Override + protected ConnectionManagerService createServiceInstance() { + return new ConnectionManagerService(protocols, null); + } + }); // For compatibility with Microsoft - LocalService receiverService = new AnnotationLocalServiceBinder().read(MSMediaReceiverRegistrarService.class); + @SuppressWarnings("unchecked") + LocalService receiverService = new AnnotationLocalServiceBinder() + .read(MSMediaReceiverRegistrarService.class); receiverService.setManager(new DefaultServiceManager<>(receiverService, MSMediaReceiverRegistrarService.class)); - return new LocalDevice(identity, type, details, new Icon[]{icon}, new LocalService[]{contentDirectoryservice, connetionManagerService, receiverService}); + Icon icon = null; + try (InputStream in = getClass().getResourceAsStream("logo-512.png")) { + icon = new Icon("image/png", 512, 512, 32, "logo-512", in); + } catch (IOException e) { + throw new RuntimeException(e); + } + + String serverName = settingsService.getDlnaServerName(); + String serverId = settingsService.getDlnaServerId(); + String serialNumber = versionService.getLocalBuildNumber(); + if (serverId == null) { + serverId = UUID.randomUUID().toString(); + settingsService.setDlnaServerId(serverId); + } + + // TODO: DLNACaps + DLNADoc[] dlnaDocs = new DLNADoc[] { new DLNADoc("DMS", DLNADoc.Version.V1_5) }; + URI modelURI = URI.create("https://airsonic.github.io/"); + URI manufacturerURI = URI.create("https://github.com/kagemomiji/airsonic-advanced"); + URI presentaionURI = URI.create(settingsService.getDlnaBaseLANURL()); + ManufacturerDetails manufacturerDetails = new ManufacturerDetails(serverName, modelURI); + ModelDetails modelDetails = new ModelDetails(serverName, null, versionService.getLocalVersion().toString(), + manufacturerURI); + DeviceDetails details = new DeviceDetails(serverName, manufacturerDetails, modelDetails, serialNumber, null, + presentaionURI, dlnaDocs, null); + DeviceIdentity identity = new DeviceIdentity(UDN.uniqueSystemIdentifier(serverName), + MIN_ADVERTISEMENT_AGE_SECONDS); + DeviceType type = new UDADeviceType("MediaServer", 1); + + return new LocalDevice(identity, type, details, new Icon[] { icon }, + new LocalService[] { contentDirectoryservice, connetionManagerService, receiverService }); } public List getSonosControllerHosts() { ensureServiceStarted(); List result = new ArrayList(); - for (Device device : upnpService.getRegistry().getDevices(new DeviceType("schemas-upnp-org", "ZonePlayer"))) { + for (Device device : upnpService.getRegistry() + .getDevices(new DeviceType("schemas-upnp-org", "ZonePlayer"))) { if (device instanceof RemoteDevice) { URL descriptorURL = ((RemoteDevice) device).getIdentity().getDescriptorURL(); if (descriptorURL != null) { @@ -221,36 +236,4 @@ public List getSonosControllerHosts() { } return result; } - - public UpnpService getUpnpService() { - return upnpService; - } - - public void setSettingsService(SettingsService settingsService) { - this.settingsService = settingsService; - } - - public void setCustomContentDirectory(CustomContentDirectory customContentDirectory) { - this.dispatchingContentDirectory = customContentDirectory; - } - - /** - * Note the different packages on similarly named classes from the parent - * - */ - public static class ApacheUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration { - public ApacheUpnpServiceConfiguration(int streamListenPort) { - super(streamListenPort); - } - - @Override - public StreamClient createStreamClient() { - return new StreamClientImpl(new StreamClientConfigurationImpl(getSyncProtocolExecutorService())); - } - - @Override - public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) { - return new StreamServerImpl(new StreamServerConfigurationImpl(networkAddressFactory.getStreamListenPort())); - } - } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/AlbumUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/AlbumUpnpProcessor.java index 749813998..c79cf5739 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/AlbumUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/AlbumUpnpProcessor.java @@ -14,6 +14,7 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2017 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ @@ -21,12 +22,12 @@ import com.google.common.primitives.Ints; import org.airsonic.player.domain.Album; -import org.airsonic.player.domain.CoverArtScheme; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MusicFolder; -import org.airsonic.player.domain.User; +import org.airsonic.player.domain.ParamSearchResult; import org.airsonic.player.service.AlbumService; import org.airsonic.player.service.MediaFileService; +import org.airsonic.player.service.MediaFolderService; import org.airsonic.player.service.SearchService; import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; @@ -36,7 +37,6 @@ import org.fourthline.cling.support.model.container.MusicAlbum; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.util.ArrayList; @@ -61,8 +61,17 @@ public class AlbumUpnpProcessor extends UpnpContentProcessor @Autowired private MediaFileService mediaFileService; + @Autowired + private MediaFolderService mediaFolderService; + + @Autowired + private UpnpProcessorRouter router; + + @Autowired + private UpnpUtil upnpUtil; + public AlbumUpnpProcessor() { - setRootId(DispatchingContentDirectory.CONTAINER_ID_ALBUM_PREFIX); + setRootId(ProcessorType.ALBUM); setRootTitle("Albums"); } @@ -73,7 +82,7 @@ public AlbumUpnpProcessor() { public BrowseResult browseRoot(String filter, long firstResult, long maxResults, SortCriterion[] orderBy) throws Exception { DIDLContent didl = new DIDLContent(); - List allFolders = getDispatchingContentDirectory().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); List selectedItems = albumService.getAlphabeticalAlbums(Ints.saturatedCast(firstResult), Ints.saturatedCast(maxResults), false, true, allFolders); for (Album item : selectedItems) { addItem(didl, item); @@ -89,7 +98,7 @@ public Container createContainer(Album album) { container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + album.getComment()); } else { container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + album.getId()); - container.setAlbumArtURIs(new URI[] { getAlbumArtURI(album.getId()) }); + container.setAlbumArtURIs(new URI[] { upnpUtil.getAlbumArtURI(album.getId()) }); container.setDescription(album.getComment()); } container.setParentID(getRootId()); @@ -103,7 +112,7 @@ public Container createContainer(Album album) { @Override public List getAllItems() { - List allFolders = getDispatchingContentDirectory().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); return albumService.getAlphabeticalAlbums(false, true, allFolders); } @@ -126,10 +135,10 @@ public List getChildren(Album album) { if (album.getId() == -1) { List albumList = null; if (album.getComment().startsWith(ALL_BY_ARTIST)) { - ArtistUpnpProcessor ap = getDispatcher().getArtistProcessor(); + ArtistUpnpProcessor ap = router.getArtistProcessor(); albumList = ap.getChildren(ap.getItemById(album.getComment().replaceAll(ALL_BY_ARTIST + "_", ""))); } else if (album.getComment().equalsIgnoreCase(ALL_RECENT)) { - albumList = getDispatcher().getRecentAlbumProcessor().getAllItems(); + albumList = router.getRecentProcessor().getAllItems(); } else { albumList = new ArrayList<>(); } @@ -146,32 +155,37 @@ public List getChildren(Album album) { @Override public int getAllItemsSize() { - List allFolders = getDispatchingContentDirectory().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); return albumService.getAlbumCount(allFolders); } @Override public void addChild(DIDLContent didl, MediaFile child) { - didl.addItem(getDispatcher().getMediaFileProcessor().createItem(child)); - } - - public URI getAlbumArtURI(int albumId) { - return UriComponentsBuilder - .fromUriString(getDispatcher().getBaseUrl()) - .uriComponents(getDispatcher().getJwtSecurityService() - .addJWTToken( - User.USERNAME_ANONYMOUS, - UriComponentsBuilder.fromUriString("ext/coverArt.view") - .queryParam("id", albumId) - .queryParam("size", CoverArtScheme.LARGE.getSize())) - .build()) - .build().encode().toUri(); + didl.addItem(router.getMediaFileProcessor().createItem(child)); } public PersonWithRole[] getAlbumArtists(String artist) { return new PersonWithRole[] { new PersonWithRole(artist) }; } + public BrowseResult searchByName(String name, + long firstResult, long maxResults, + SortCriterion[] orderBy) { + DIDLContent didl = new DIDLContent(); + try { + List allFolders = mediaFolderService.getAllMusicFolders(); + ParamSearchResult result = searchService.searchByName(name, Ints.saturatedCast(firstResult), Ints.saturatedCast(maxResults), allFolders, Album.class); + List selectedItems = result.getItems(); + for (Album item : selectedItems) { + addItem(didl, item); + } + + return createBrowseResult(didl, didl.getCount(), result.getTotalHits()); + } catch (Exception e) { + return null; + } + } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ApacheUpnpServiceConfiguration.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ApacheUpnpServiceConfiguration.java new file mode 100644 index 000000000..3a263a161 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ApacheUpnpServiceConfiguration.java @@ -0,0 +1,50 @@ +/* + This file is part of Airsonic. + + Airsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Airsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Airsonic. If not, see . + + Copyright 2024 (C) Y.Tory + Copyright 2017 (C) Airsonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.airsonic.player.service.upnp; + +import org.fourthline.cling.DefaultUpnpServiceConfiguration; +import org.fourthline.cling.transport.impl.apache.StreamClientConfigurationImpl; +import org.fourthline.cling.transport.impl.apache.StreamClientImpl; +import org.fourthline.cling.transport.impl.apache.StreamServerConfigurationImpl; +import org.fourthline.cling.transport.impl.apache.StreamServerImpl; +import org.fourthline.cling.transport.spi.NetworkAddressFactory; +import org.fourthline.cling.transport.spi.StreamClient; +import org.fourthline.cling.transport.spi.StreamServer; + +/** + * Note the different packages on similarly named classes from the parent + * + */ +public class ApacheUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration { + public ApacheUpnpServiceConfiguration(int streamListenPort) { + super(streamListenPort); + } + + @Override + public StreamClient createStreamClient() { + return new StreamClientImpl(new StreamClientConfigurationImpl(getSyncProtocolExecutorService())); + } + + @Override + public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) { + return new StreamServerImpl(new StreamServerConfigurationImpl(networkAddressFactory.getStreamListenPort())); + } +} \ No newline at end of file diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ArtistUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ArtistUpnpProcessor.java index fcc8956f8..7f801d26e 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ArtistUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ArtistUpnpProcessor.java @@ -14,17 +14,24 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2017 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service.upnp; +import com.google.common.primitives.Ints; import org.airsonic.player.domain.Album; import org.airsonic.player.domain.Artist; import org.airsonic.player.domain.MusicFolder; +import org.airsonic.player.domain.ParamSearchResult; import org.airsonic.player.repository.AlbumRepository; import org.airsonic.player.repository.ArtistRepository; +import org.airsonic.player.service.MediaFolderService; +import org.airsonic.player.service.SearchService; +import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.MusicArtist; import org.springframework.beans.factory.annotation.Autowired; @@ -50,8 +57,17 @@ public class ArtistUpnpProcessor extends UpnpContentProcessor { @Autowired private AlbumRepository albumRepository; + @Autowired + private MediaFolderService mediaFolderService; + + @Autowired + private UpnpProcessorRouter router; + + @Autowired + private SearchService searchService; + public ArtistUpnpProcessor() { - setRootId(DispatchingContentDirectory.CONTAINER_ID_ARTIST_PREFIX); + setRootId(ProcessorType.ARTIST); setRootTitle("Artists"); } @@ -68,7 +84,7 @@ public Container createContainer(Artist artist) { @Override public List getAllItems() { - List allFolders = getDispatcher().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); if (CollectionUtils.isEmpty(allFolders)) { return Collections.emptyList(); } @@ -90,7 +106,7 @@ public Artist getItemById(String id) { @Override public List getChildren(Artist artist) { - List allFolders = getDispatcher().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); List allAlbums = albumRepository.findByArtistAndFolderInAndPresentTrue(artist.getName(), allFolders); if (allAlbums.size() > 1) { // if the artist has more than one album, add in an option to @@ -106,10 +122,26 @@ public List getChildren(Artist artist) { @Override public void addChild(DIDLContent didl, Album album) { - didl.addContainer(getAlbumProcessor().createContainer(album)); + didl.addContainer(router.getAlbumProcessor().createContainer(album)); } - public AlbumUpnpProcessor getAlbumProcessor() { - return getDispatcher().getAlbumProcessor(); + + public BrowseResult searchByName(String name, + long firstResult, long maxResults, + SortCriterion[] orderBy) { + DIDLContent didl = new DIDLContent(); + try { + List allFolders = mediaFolderService.getAllMusicFolders(); + ParamSearchResult result = searchService.searchByName(name, Ints.saturatedCast(firstResult), Ints.saturatedCast(maxResults), allFolders, Artist.class); + List selectedItems = result.getItems(); + for (Artist item : selectedItems) { + addItem(didl, item); + } + + return createBrowseResult(didl, didl.getCount(), result.getTotalHits()); + } catch (Exception e) { + return null; + } } + } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/CustomContentDirectory.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/CustomContentDirectory.java index d14d2a729..ecaa6cab5 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/CustomContentDirectory.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/CustomContentDirectory.java @@ -14,33 +14,21 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service.upnp; -import com.google.common.collect.Lists; -import org.airsonic.player.domain.MediaFile; -import org.airsonic.player.domain.Player; -import org.airsonic.player.domain.User; -import org.airsonic.player.service.JWTSecurityService; -import org.airsonic.player.service.PlayerService; -import org.airsonic.player.service.SecurityService; -import org.airsonic.player.service.SettingsService; -import org.airsonic.player.service.TranscodingService; -import org.airsonic.player.util.StringUtil; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.StringUtils; import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService; import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; import org.fourthline.cling.support.contentdirectory.DIDLParser; import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; -import org.seamless.util.MimeType; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Arrays; +import java.util.Collections; /** * @author Sindre Mehus @@ -50,60 +38,8 @@ public abstract class CustomContentDirectory extends AbstractContentDirectorySer protected static final String CONTAINER_ID_ROOT = "0"; - @Autowired - protected SettingsService settingsService; - @Autowired - private PlayerService playerService; - @Autowired - private TranscodingService transcodingService; - @Autowired - protected JWTSecurityService jwtSecurityService; - @Autowired - private SecurityService securityService; - public CustomContentDirectory() { - super(Lists.newArrayList("*"), Lists.newArrayList()); - } - - protected Res createResourceForSong(MediaFile song) { - // Create a guest user if necessary - securityService.createGuestUserIfNotExists(); - Player player = playerService.getGuestPlayer(null); - - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("ext/stream") - .queryParam("id", song.getId()) - .queryParam("player", player.getId()); - - if (song.isVideo()) { - builder.queryParam("format", TranscodingService.FORMAT_RAW); - } - - builder = jwtSecurityService.addJWTToken(User.USERNAME_ANONYMOUS, builder); - - String url = getBaseUrl() + builder.toUriString(); - - String suffix = song.isVideo() ? FilenameUtils.getExtension(song.getPath()) : transcodingService.getSuffix(player, song, null); - String mimeTypeString = StringUtil.getMimeType(suffix); - MimeType mimeType = mimeTypeString == null ? null : MimeType.valueOf(mimeTypeString); - - Res res = new Res(mimeType, null, url); - res.setDuration(formatDuration(song.getDuration())); - return res; - } - - private String formatDuration(Double seconds) { - if (seconds == null) { - return null; - } - return StringUtil.formatDuration((long) (seconds * 1000), true); - } - - protected String getBaseUrl() { - String dlnaBaseLANURL = settingsService.getDlnaBaseLANURL(); - if (StringUtils.isBlank(dlnaBaseLANURL)) { - throw new RuntimeException("DLNA Base LAN URL is not set correctly"); - } - return StringUtils.appendIfMissing(dlnaBaseLANURL, "/"); + super(Arrays.asList("*"), Collections.emptyList()); } protected BrowseResult createBrowseResult(DIDLContent didl, int count, int totalMatches) throws Exception { @@ -119,19 +55,4 @@ public BrowseResult search(String containerId, return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy); } - public void setPlayerService(PlayerService playerService) { - this.playerService = playerService; - } - - public void setTranscodingService(TranscodingService transcodingService) { - this.transcodingService = transcodingService; - } - - public void setSettingsService(SettingsService settingsService) { - this.settingsService = settingsService; - } - - public void setJwtSecurityService(JWTSecurityService jwtSecurityService) { - this.jwtSecurityService = jwtSecurityService; - } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/DispatchingContentDirectory.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/DispatchingContentDirectory.java index 6eb2df9ff..2e80c1145 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/DispatchingContentDirectory.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/DispatchingContentDirectory.java @@ -14,29 +14,20 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service.upnp; -import org.airsonic.player.domain.CoverArtScheme; -import org.airsonic.player.domain.MediaFile; -import org.airsonic.player.domain.User; -import org.airsonic.player.service.*; import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; import org.fourthline.cling.support.model.*; -import org.fourthline.cling.support.model.item.Item; -import org.fourthline.cling.support.model.item.MusicTrack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.springframework.web.util.UriComponentsBuilder; -import java.net.URI; -import java.util.Arrays; /** * @author Allen Petersen @@ -48,62 +39,19 @@ public class DispatchingContentDirectory extends CustomContentDirectory { private static final Logger LOG = LoggerFactory.getLogger(DispatchingContentDirectory.class); - public static final String CONTAINER_ID_ROOT = "0"; - public static final String CONTAINER_ID_PLAYLIST_PREFIX = "playlist"; - public static final String CONTAINER_ID_FOLDER_PREFIX = "folder"; - public static final String CONTAINER_ID_ALBUM_PREFIX = "album"; - public static final String CONTAINER_ID_ARTIST_PREFIX = "artist"; - public static final String CONTAINER_ID_ARTISTALBUM_PREFIX = "artistalbum"; - public static final String CONTAINER_ID_GENRE_PREFIX = "genre"; - public static final String CONTAINER_ID_RECENT_PREFIX = "recent"; - protected static final String SEPARATOR = "-"; - @Lazy - @Autowired - private PlaylistUpnpProcessor playlistProcessor; - @Lazy - @Autowired - private MediaFileUpnpProcessor mediaFileProcessor; - //@Autowired can't autowire because of the subclassing :P - @Lazy - @Autowired//first checks type then field name to autowire - private AlbumUpnpProcessor albumUpnpProcessor; - //@Autowired can't autowire because of the subclassing :P - @Lazy - @Autowired//first checks type then field name to autowire - private RecentAlbumUpnpProcessor recentAlbumUpnpProcessor; - @Lazy - @Autowired - private ArtistUpnpProcessor artistProcessor; - @Lazy - @Autowired - private GenreUpnpProcessor genreProcessor; - @Lazy - @Autowired - private RootUpnpProcessor rootProcessor; - - @Autowired - private MediaFileService mediaFileService; - @Autowired - private MediaFolderService mediaFolderService; @Autowired - private PlaylistService playlistService; - - @Autowired - private MusicIndexService musicIndexService; - - @Autowired - private SearchService searchService; - + private UpnpProcessorRouter router; @Override public BrowseResult browse(String objectId, BrowseFlag browseFlag, - String filter, long firstResult, - long maxResults, SortCriterion[] orderBy) - throws ContentDirectoryException { + String filter, long firstResult, + long maxResults, SortCriterion[] orderBy) + throws ContentDirectoryException { - LOG.info("UPnP request - objectId: " + objectId + ", browseFlag: " + browseFlag + ", filter: " + filter + ", firstResult: " + firstResult + ", maxResults: " + maxResults); + LOG.info("UPnP request - objectId: " + objectId + ", browseFlag: " + browseFlag + ", filter: " + filter + + ", firstResult: " + firstResult + ", maxResults: " + maxResults); if (objectId == null) throw new ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, "objectId is null"); @@ -119,18 +67,20 @@ public BrowseResult browse(String objectId, BrowseFlag browseFlag, String browseRoot = splitId[0]; String itemId = splitId.length == 1 ? null : splitId[1]; - UpnpContentProcessor processor = findProcessor(browseRoot); + UpnpContentProcessor processor = router.findProcessor(ProcessorType.toEnum(browseRoot)); if (processor == null) { // if it's null then assume it's a file, and that the id // is all that's there. itemId = browseRoot; - processor = getMediaFileProcessor(); + processor = router.findProcessor(ProcessorType.MEDIAFILE); } if (itemId == null) { - returnValue = browseFlag == BrowseFlag.METADATA ? processor.browseRootMetadata() : processor.browseRoot(filter, firstResult, maxResults, orderBy); + returnValue = browseFlag == BrowseFlag.METADATA ? processor.browseRootMetadata() + : processor.browseRoot(filter, firstResult, maxResults, orderBy); } else { - returnValue = browseFlag == BrowseFlag.METADATA ? processor.browseObjectMetadata(itemId) : processor.browseObject(itemId, filter, firstResult, maxResults, orderBy); + returnValue = browseFlag == BrowseFlag.METADATA ? processor.browseObjectMetadata(itemId) + : processor.browseObject(itemId, filter, firstResult, maxResults, orderBy); } return returnValue; } catch (Throwable x) { @@ -141,174 +91,24 @@ public BrowseResult browse(String objectId, BrowseFlag browseFlag, @Override public BrowseResult search(String containerId, - String searchCriteria, String filter, - long firstResult, long maxResults, - SortCriterion[] orderBy) throws ContentDirectoryException { + String searchCriteria, String filter, + long firstResult, long maxResults, + SortCriterion[] orderBy) throws ContentDirectoryException { // i don't see a parser for upnp search criteria anywhere, so this will // have to do String upnpClass = searchCriteria.replaceAll("^.*upnp:class\\s+[\\S]+\\s+\"([\\S]*)\".*$", "$1"); String titleSearch = searchCriteria.replaceAll("^.*dc:title\\s+[\\S]+\\s+\"([\\S]*)\".*$", "$1"); BrowseResult returnValue = null; if ("object.container.person.musicArtist".equalsIgnoreCase(upnpClass)) { - returnValue = getArtistProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); + returnValue = router.getArtistProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); } else if ("object.item.audioItem".equalsIgnoreCase(upnpClass)) { - returnValue = getMediaFileProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); + returnValue = router.getMediaFileProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); } else if ("object.container.album.musicAlbum".equalsIgnoreCase(upnpClass)) { - returnValue = getAlbumProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); + returnValue = router.getAlbumProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); } - return returnValue != null ? returnValue : super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy); - } - - - private UpnpContentProcessor findProcessor(String type) { - switch (type) { - case CONTAINER_ID_ROOT: - return getRootProcessor(); - case CONTAINER_ID_PLAYLIST_PREFIX: - return getPlaylistProcessor(); - case CONTAINER_ID_FOLDER_PREFIX: - return getMediaFileProcessor(); - case CONTAINER_ID_ALBUM_PREFIX: - return getAlbumProcessor(); - case CONTAINER_ID_RECENT_PREFIX: - return getRecentAlbumProcessor(); - case CONTAINER_ID_ARTIST_PREFIX: - return getArtistProcessor(); - case CONTAINER_ID_GENRE_PREFIX: - return getGenreProcessor(); - } - return null; - } - - public Item createItem(MediaFile song) { - MediaFile parent = mediaFileService.getParentOf(song); - MusicTrack item = new MusicTrack(); - item.setId(String.valueOf(song.getId())); - item.setParentID(String.valueOf(parent.getId())); - item.setTitle(song.getTitle()); - item.setAlbum(song.getAlbumName()); - if (song.getArtist() != null) { - item.setArtists(new PersonWithRole[]{new PersonWithRole(song.getArtist())}); - } - Integer year = song.getYear(); - if (year != null) { - item.setDate(year + "-01-01"); - } - item.setOriginalTrackNumber(song.getTrackNumber()); - if (song.getGenre() != null) { - item.setGenres(new String[]{song.getGenre()}); - } - item.setResources(Arrays.asList(createResourceForSong(song))); - item.setDescription(song.getComment()); - item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(getAlbumArtUrl(parent.getId()))); - - return item; - } - - public URI getAlbumArtUrl(int id) { - return UriComponentsBuilder - .fromUriString(getBaseUrl()) - .uriComponents(jwtSecurityService - .addJWTToken( - User.USERNAME_ANONYMOUS, - UriComponentsBuilder.fromUriString("ext/coverArt.view") - .queryParam("id", id) - .queryParam("size", CoverArtScheme.LARGE.getSize())) - .build()) - .build().encode().toUri(); - } - - public PlaylistUpnpProcessor getPlaylistProcessor() { - return playlistProcessor; - } - public void setPlaylistProcessor(PlaylistUpnpProcessor playlistProcessor) { - this.playlistProcessor = playlistProcessor; - } - - public MediaFileUpnpProcessor getMediaFileProcessor() { - return mediaFileProcessor; - } - public void setMediaFileProcessor(MediaFileUpnpProcessor mediaFileProcessor) { - this.mediaFileProcessor = mediaFileProcessor; - } - - public AlbumUpnpProcessor getAlbumProcessor() { - return albumUpnpProcessor; - } - public void setAlbumProcessor(AlbumUpnpProcessor albumProcessor) { - this.albumUpnpProcessor = albumProcessor; - } - - public RecentAlbumUpnpProcessor getRecentAlbumProcessor() { - return recentAlbumUpnpProcessor; - } - public void setRecentAlbumProcessor(RecentAlbumUpnpProcessor recentAlbumProcessor) { - this.recentAlbumUpnpProcessor = recentAlbumProcessor; - } - - public ArtistUpnpProcessor getArtistProcessor() { - return artistProcessor; - } - public void setArtistProcessor(ArtistUpnpProcessor artistProcessor) { - this.artistProcessor = artistProcessor; - } - - public GenreUpnpProcessor getGenreProcessor() { - return genreProcessor; - } - public void setGenreProcessor(GenreUpnpProcessor genreProcessor) { - this.genreProcessor = genreProcessor; - } - - public RootUpnpProcessor getRootProcessor() { - return rootProcessor; - } - public void setRootProcessor(RootUpnpProcessor rootProcessor) { - this.rootProcessor = rootProcessor; - } - - public MediaFileService getMediaFileService() { - return mediaFileService; - } - public void setMediaFileService(MediaFileService mediaFileService) { - this.mediaFileService = mediaFileService; + return returnValue != null ? returnValue + : super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy); } - public MediaFolderService getMediaFolderService() { - return mediaFolderService; - } - - public void setMediaFolderService(MediaFolderService mediaFolderService) { - this.mediaFolderService = mediaFolderService; - } - - public SettingsService getSettingsService() { - return settingsService; - } - - public PlaylistService getPlaylistService() { - return playlistService; - } - public void setPlaylistService(PlaylistService playlistService) { - this.playlistService = playlistService; - } - - public JWTSecurityService getJwtSecurityService() { - return jwtSecurityService; - } - - public MusicIndexService getMusicIndexService() { - return this.musicIndexService; - } - public void setMusicIndexService(MusicIndexService musicIndexService) { - this.musicIndexService = musicIndexService; - } - - public SearchService getSearchService() { - return this.searchService; - } - public void setSearchService(SearchService searchService) { - this.searchService = searchService; - } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java index 4157441cd..907c4a46b 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java @@ -14,6 +14,7 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ @@ -38,7 +39,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.util.Arrays; @@ -63,6 +63,8 @@ public class FolderBasedContentDirectory extends CustomContentDirectory { private PlaylistService playlistService; @Autowired private IndexManager indexManager; + @Autowired + private UpnpUtil upnpUtil; @Override public BrowseResult browse(String objectId, BrowseFlag browseFlag, String filter, long firstResult, @@ -212,9 +214,9 @@ private Item createItem(MediaFile song) { if (song.getGenre() != null) { item.setGenres(new String[]{song.getGenre()}); } - item.setResources(Arrays.asList(createResourceForSong(song))); + item.setResources(Arrays.asList(upnpUtil.createResourceForSong(song))); item.setDescription(song.getComment()); - item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(getAlbumArtUrl(parent))); + item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(upnpUtil.getAlbumArtURI(parent.getId()))); return item; } @@ -238,7 +240,7 @@ private Container createContainer(MediaFile mediaFile) { private Container createAlbumContainer(MediaFile album) { MusicAlbum container = new MusicAlbum(); - container.setAlbumArtURIs(new URI[]{getAlbumArtUrl(album)}); + container.setAlbumArtURIs(new URI[]{upnpUtil.getAlbumArtURI(album.getId())}); // TODO: correct artist? if (album.getArtist() != null) { @@ -271,24 +273,4 @@ private Container createPlaylistContainer(Playlist playlist) { return container; } - private URI getAlbumArtUrl(MediaFile album) { - return UriComponentsBuilder - .fromUriString(getBaseUrl()) - .uriComponents(jwtSecurityService - .addJWTToken( - User.USERNAME_ANONYMOUS, - UriComponentsBuilder.fromUriString("ext/coverArt.view") - .queryParam("id", album.getId()) - .queryParam("size", CoverArtScheme.LARGE.getSize())) - .build()) - .build().encode().toUri(); - } - - public void setMediaFileService(MediaFileService mediaFileService) { - this.mediaFileService = mediaFileService; - } - - public void setPlaylistService(PlaylistService playlistService) { - this.playlistService = playlistService; - } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/GenreUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/GenreUpnpProcessor.java index e03fd3b3b..6a1fac6e0 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/GenreUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/GenreUpnpProcessor.java @@ -14,6 +14,7 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2017 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ @@ -22,12 +23,15 @@ import org.airsonic.player.domain.Genre; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MusicFolder; +import org.airsonic.player.service.MediaFileService; +import org.airsonic.player.service.MediaFolderService; import org.airsonic.player.util.Util; import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.GenreContainer; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @@ -40,10 +44,19 @@ public class GenreUpnpProcessor extends UpnpContentProcessor { public GenreUpnpProcessor() { - setRootId(DispatchingContentDirectory.CONTAINER_ID_GENRE_PREFIX); + setRootId(ProcessorType.GENRE); setRootTitle("Genres"); } + @Autowired + private UpnpProcessorRouter router; + + @Autowired + private MediaFileService mediaFileService; + + @Autowired + private MediaFolderService mediaFolderService; + /** * Browses the top-level content of a type. */ @@ -84,7 +97,7 @@ public Container createContainer(Genre item, int index) { @Override public List getAllItems() { - return getDispatcher().getMediaFileService().getGenres(false); + return mediaFileService.getGenres(false); } @Override @@ -99,12 +112,12 @@ public Genre getItemById(String id) { @Override public List getChildren(Genre item) { - List allFolders = getDispatcher().getMediaFolderService().getAllMusicFolders(); - return getDispatcher().getMediaFileProcessor().getMediaFileService().getSongsByGenre(0, Integer.MAX_VALUE, item.getName(), allFolders); + List allFolders = mediaFolderService.getAllMusicFolders(); + return mediaFileService.getSongsByGenre(0, Integer.MAX_VALUE, item.getName(), allFolders); } @Override public void addChild(DIDLContent didl, MediaFile child) { - didl.addItem(getDispatcher().getMediaFileProcessor().createItem(child)); + didl.addItem(router.getMediaFileProcessor().createItem(child)); } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/MediaFileUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/MediaFileUpnpProcessor.java index 63903497d..bbdfb7c89 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/MediaFileUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/MediaFileUpnpProcessor.java @@ -20,16 +20,22 @@ */ package org.airsonic.player.service.upnp; +import com.google.common.primitives.Ints; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MusicFolder; +import org.airsonic.player.domain.ParamSearchResult; import org.airsonic.player.service.MediaFileService; +import org.airsonic.player.service.MediaFolderService; +import org.airsonic.player.service.SearchService; import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.MusicAlbum; import org.fourthline.cling.support.model.item.Item; import org.fourthline.cling.support.model.item.MusicTrack; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.net.URI; @@ -46,10 +52,25 @@ public class MediaFileUpnpProcessor extends UpnpContentProcessor { public MediaFileUpnpProcessor() { - setRootId(DispatchingContentDirectory.CONTAINER_ID_FOLDER_PREFIX); + setRootId(ProcessorType.FOLDER); setRootTitle("Folders"); } + @Autowired + private UpnpProcessorRouter router; + + @Autowired + private MediaFileService mediaFileService; + + @Autowired + private MediaFolderService mediaFolderService; + + @Autowired + private UpnpUtil upnpUtil; + + @Autowired + private SearchService searchService; + @Override // overriding for the case of browsing a file public BrowseResult browseObjectMetadata(String id) throws Exception { @@ -63,49 +84,49 @@ public BrowseResult browseObjectMetadata(String id) throws Exception { public Container createContainer(MediaFile item) { MusicAlbum container = new MusicAlbum(); if (item.isAlbum()) { - container.setAlbumArtURIs(new URI[] { getDispatcher().getAlbumProcessor().getAlbumArtURI(item.getId())}); + container.setAlbumArtURIs(new URI[] { upnpUtil.getAlbumArtURI(item.getId())}); if (item.getArtist() != null) { - container.setArtists(getDispatcher().getAlbumProcessor().getAlbumArtists(item.getArtist())); + container.setArtists(router.getAlbumProcessor().getAlbumArtists(item.getArtist())); } container.setDescription(item.getComment()); } - container.setId(DispatchingContentDirectory.CONTAINER_ID_FOLDER_PREFIX + DispatchingContentDirectory.SEPARATOR + item.getId()); + container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + item.getId()); container.setTitle(item.getName()); List children = getChildren(item); container.setChildCount(children.size()); - if (! getMediaFileService().isRoot(item)) { - MediaFile parent = getMediaFileService().getParentOf(item); + if (! mediaFileService.isRoot(item)) { + MediaFile parent = mediaFileService.getParentOf(item); if (parent != null) { container.setParentID(String.valueOf(parent.getId())); } } else { - container.setParentID(DispatchingContentDirectory.CONTAINER_ID_FOLDER_PREFIX); + container.setParentID(getRootId()); } return container; } @Override public List getAllItems() { - List allFolders = getDispatcher().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); if (allFolders.size() == 1) { // if there's only one root folder just return it - return getChildren(getMediaFileService().getMediaFile("", allFolders.get(0))); + return getChildren(mediaFileService.getMediaFile("", allFolders.get(0))); } else { - return allFolders.stream().map(f -> getMediaFileService().getMediaFile("", f)).collect(toList()); + return allFolders.stream().map(f -> mediaFileService.getMediaFile("", f)).collect(toList()); } } @Override public MediaFile getItemById(String id) { - return getMediaFileService().getMediaFile(Integer.parseInt(id)); + return mediaFileService.getMediaFile(Integer.parseInt(id)); } @Override public List getChildren(MediaFile item) { - List children = getMediaFileService().getVisibleChildrenOf(item, true, true); + List children = mediaFileService.getVisibleChildrenOf(item, true, true); children.sort((MediaFile o1, MediaFile o2) -> o1.getPath().replaceAll("\\W", "").compareToIgnoreCase(o2.getPath().replaceAll("\\W", ""))); return children; } @@ -129,14 +150,14 @@ public void addChild(DIDLContent didl, MediaFile child) { } public Item createItem(MediaFile song) { - MediaFile parent = getMediaFileService().getParentOf(song); + MediaFile parent = mediaFileService.getParentOf(song); MusicTrack item = new MusicTrack(); item.setId(String.valueOf(song.getId())); item.setParentID(String.valueOf(parent.getId())); item.setTitle(song.getTitle()); item.setAlbum(song.getAlbumName()); if (song.getArtist() != null) { - item.setArtists(getDispatcher().getAlbumProcessor().getAlbumArtists(song.getArtist())); + item.setArtists(router.getAlbumProcessor().getAlbumArtists(song.getArtist())); } Integer year = song.getYear(); if (year != null) { @@ -146,15 +167,29 @@ public Item createItem(MediaFile song) { if (song.getGenre() != null) { item.setGenres(new String[]{song.getGenre()}); } - item.setResources(Arrays.asList(getDispatcher().createResourceForSong(song))); + item.setResources(Arrays.asList(upnpUtil.createResourceForSong(song))); item.setDescription(song.getComment()); - item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(getDispatcher().getAlbumProcessor().getAlbumArtURI(parent.getId()))); + item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(upnpUtil.getAlbumArtURI(parent.getId()))); return item; } - public MediaFileService getMediaFileService() { - return getDispatchingContentDirectory().getMediaFileService(); + public BrowseResult searchByName(String name, + long firstResult, long maxResults, + SortCriterion[] orderBy) { + DIDLContent didl = new DIDLContent(); + try { + List allFolders = mediaFolderService.getAllMusicFolders(); + ParamSearchResult result = searchService.searchByName(name, Ints.saturatedCast(firstResult), Ints.saturatedCast(maxResults), allFolders, MediaFile.class); + List selectedItems = result.getItems(); + for (MediaFile item : selectedItems) { + addItem(didl, item); + } + + return createBrowseResult(didl, didl.getCount(), result.getTotalHits()); + } catch (Exception e) { + return null; + } } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/PlaylistUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/PlaylistUpnpProcessor.java index 1658fb3fa..d972740fd 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/PlaylistUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/PlaylistUpnpProcessor.java @@ -14,6 +14,7 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2017 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ @@ -39,8 +40,11 @@ public class PlaylistUpnpProcessor extends UpnpContentProcessor getAllItems() { - return getPlaylistService().getAllPlaylists(); + return playlistService.getAllPlaylists(); } public Playlist getItemById(String id) { - return getDispatcher().getPlaylistService().getPlaylist(Integer.parseInt(id)); + return playlistService.getPlaylist(Integer.parseInt(id)); } public List getChildren(Playlist item) { - return getPlaylistService().getFilesInPlaylist(item.getId()); + return playlistService.getFilesInPlaylist(item.getId()); } public void addChild(DIDLContent didl, MediaFile child) { - didl.addItem(getDispatchingContentDirectory().createItem(child)); - } - - public PlaylistService getPlaylistService() { - return this.playlistService; - } - public void setPlaylistService(PlaylistService playlistService) { - this.playlistService = playlistService; + didl.addItem(router.getMediaFileProcessor().createItem(child)); } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ProcessorType.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ProcessorType.java new file mode 100644 index 000000000..e49c5e30d --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/ProcessorType.java @@ -0,0 +1,57 @@ +/* + This file is part of Airsonic. + + Airsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Airsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Airsonic. If not, see . + + Copyright 2024 (C) Y.Tory + Copyright 2017 (C) Airsonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.airsonic.player.service.upnp; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; + +public enum ProcessorType { + + ROOT("0"), + PLAYLIST("playlist"), + FOLDER("folder"), + ALBUM("album"), + ARTIST("artist"), + ARTISTALBUM("artistalbum"), + GENRE("genre"), + RECENT("recent"), + MEDIAFILE("mediafile"), + UNKNOWN("unknown"); + + private String keyType; + + private ProcessorType(String keyType) { + this.keyType = keyType; + } + + public String getKeyType() { + return this.keyType; + } + + @Nonnull + public static ProcessorType toEnum(@Nullable String keyType) { + for (ProcessorType pt: values()) { + if (pt.getKeyType().equalsIgnoreCase(keyType)) return pt; + } + return UNKNOWN; + } + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RecentAlbumUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RecentAlbumUpnpProcessor.java index 1ce7e31dd..41a5cb2fc 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RecentAlbumUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RecentAlbumUpnpProcessor.java @@ -14,6 +14,7 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2017 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ @@ -21,6 +22,7 @@ import org.airsonic.player.domain.Album; import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.service.AlbumService; +import org.airsonic.player.service.MediaFolderService; import org.airsonic.player.util.Util; import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; @@ -41,8 +43,11 @@ public class RecentAlbumUpnpProcessor extends AlbumUpnpProcessor { @Autowired private AlbumService albumService; + @Autowired + private MediaFolderService mediaFolderService; + public RecentAlbumUpnpProcessor() { - setRootId(DispatchingContentDirectory.CONTAINER_ID_RECENT_PREFIX); + setRootId(ProcessorType.RECENT); setRootTitle("RecentAlbums"); } @@ -71,7 +76,7 @@ public BrowseResult browseRoot(String filter, long firstResult, long maxResults, @Override public List getAllItems() { - List allFolders = getDispatchingContentDirectory().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); List recentAlbums = albumService.getRecentlyAddedAlbums(0, RECENT_COUNT, allFolders); if (recentAlbums.size() > 1) { // if there is more than one recent album, add in an option to @@ -87,7 +92,7 @@ public List getAllItems() { @Override public int getAllItemsSize() { - List allFolders = getDispatchingContentDirectory().getMediaFolderService().getAllMusicFolders(); + List allFolders = mediaFolderService.getAllMusicFolders(); int allAlbumCount = albumService.getAlbumCount(allFolders); return Math.min(allAlbumCount, RECENT_COUNT); } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java index 77221c7e7..831e2f3dc 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java @@ -41,9 +41,12 @@ public class RootUpnpProcessor extends UpnpContentProcessor getAllItems() throws Exception { ArrayList allItems = new ArrayList(); - allItems.add(getDispatchingContentDirectory().getAlbumProcessor().createRootContainer()); - allItems.add(getDispatchingContentDirectory().getArtistProcessor().createRootContainer()); - allItems.add(getDispatchingContentDirectory().getMediaFileProcessor().createRootContainer()); - allItems.add(getDispatchingContentDirectory().getGenreProcessor().createRootContainer()); - allItems.add(getDispatchingContentDirectory().getPlaylistProcessor().createRootContainer()); - allItems.add(getDispatchingContentDirectory().getRecentAlbumProcessor().createRootContainer()); + allItems.add(router.getAlbumProcessor().createRootContainer()); + allItems.add(router.getArtistProcessor().createRootContainer()); + allItems.add(router.getMediaFileProcessor().createRootContainer()); + allItems.add(router.getGenreProcessor().createRootContainer()); + allItems.add(router.getPlaylistProcessor().createRootContainer()); + allItems.add(router.getRecentProcessor().createRootContainer()); return allItems; } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpContentProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpContentProcessor.java index cb1e58bf3..b0acddd98 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpContentProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpContentProcessor.java @@ -14,14 +14,12 @@ You should have received a copy of the GNU General Public License along with Airsonic. If not, see . + Copyright 2024 (C) Y.Tory Copyright 2017 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service.upnp; -import com.google.common.primitives.Ints; -import org.airsonic.player.domain.MusicFolder; -import org.airsonic.player.domain.ParamSearchResult; import org.airsonic.player.util.Util; import org.fourthline.cling.support.contentdirectory.DIDLParser; import org.fourthline.cling.support.model.BrowseResult; @@ -29,9 +27,7 @@ import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.StorageFolder; -import org.springframework.beans.factory.annotation.Autowired; -import java.lang.reflect.ParameterizedType; import java.util.List; /** @@ -40,11 +36,8 @@ */ public abstract class UpnpContentProcessor { - @Autowired - private DispatchingContentDirectory dispatchingContentDirectory; - protected String rootTitle; - protected String rootId; + protected ProcessorType rootId; /** * Browses the root metadata for a type. @@ -62,7 +55,7 @@ public Container createRootContainer() throws Exception { int childCount = getAllItemsSize(); container.setChildCount(childCount); - container.setParentID(DispatchingContentDirectory.CONTAINER_ID_ROOT); + container.setParentID(ProcessorType.ROOT.getKeyType()); return container; } @@ -121,36 +114,7 @@ protected BrowseResult createBrowseResult(DIDLContent didl, long count, long tot return new BrowseResult(new DIDLParser().generate(didl), count, totalMatches); } - public BrowseResult searchByName(String name, - long firstResult, long maxResults, - SortCriterion[] orderBy) { - DIDLContent didl = new DIDLContent(); - - Class clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; - try { - List allFolders = getDispatchingContentDirectory().getMediaFolderService().getAllMusicFolders(); - ParamSearchResult result = getDispatcher().getSearchService().searchByName(name, Ints.saturatedCast(firstResult), Ints.saturatedCast(maxResults), allFolders, clazz); - List selectedItems = result.getItems(); - for (T item : selectedItems) { - addItem(didl, item); - } - - return createBrowseResult(didl, didl.getCount(), result.getTotalHits()); - } catch (Exception e) { - return null; - } - } - - public DispatchingContentDirectory getDispatchingContentDirectory() { - return dispatchingContentDirectory; - } - public void setDispatchingContentDirectory(DispatchingContentDirectory dispatchingContentDirectory) { - this.dispatchingContentDirectory = dispatchingContentDirectory; - } - public DispatchingContentDirectory getDispatcher() { - return getDispatchingContentDirectory(); - } public void addItem(DIDLContent didl, T item) { didl.addContainer(createContainer(item)); @@ -178,10 +142,9 @@ public void setRootTitle(String rootTitle) { this.rootTitle = rootTitle; } public String getRootId() { - return rootId; + return rootId.getKeyType(); } - public void setRootId(String rootId) { + public void setRootId(ProcessorType rootId) { this.rootId = rootId; } } - diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouter.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouter.java new file mode 100644 index 000000000..36335db4c --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouter.java @@ -0,0 +1,39 @@ +/* + This file is part of Airsonic. + + Airsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Airsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Airsonic. If not, see . + + Copyright 2024 (C) Y.Tory + Copyright 2017 (C) Airsonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.airsonic.player.service.upnp; + +public interface UpnpProcessorRouter { + + public UpnpContentProcessor findProcessor(ProcessorType type); + + public AlbumUpnpProcessor getAlbumProcessor(); + + public ArtistUpnpProcessor getArtistProcessor(); + + public MediaFileUpnpProcessor getMediaFileProcessor(); + + public RecentAlbumUpnpProcessor getRecentProcessor(); + + public PlaylistUpnpProcessor getPlaylistProcessor(); + + public GenreUpnpProcessor getGenreProcessor(); + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouterImpl.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouterImpl.java new file mode 100644 index 000000000..42f1f52fc --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpProcessorRouterImpl.java @@ -0,0 +1,107 @@ +/* + This file is part of Airsonic. + + Airsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Airsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Airsonic. If not, see . + + Copyright 2024 (C) Y.Tory + Copyright 2017 (C) Airsonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.airsonic.player.service.upnp; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Component +public class UpnpProcessorRouterImpl implements UpnpProcessorRouter { + + @Autowired + @Lazy + private RootUpnpProcessor rootUpnpProcessor; + @Lazy + @Autowired + private AlbumUpnpProcessor albumUpnpProcessor; + @Lazy + @Autowired + private ArtistUpnpProcessor artistUpnpProcessor; + @Lazy + @Autowired + private GenreUpnpProcessor genreUpnpProcessor; + @Lazy + @Autowired + private MediaFileUpnpProcessor mediaFileUpnpProcessor; + @Lazy + @Autowired + private PlaylistUpnpProcessor playlistUpnpProcessor; + @Lazy + @Autowired + private RecentAlbumUpnpProcessor recentAlbumUpnpProcessor; + + @Override + public UpnpContentProcessor findProcessor(ProcessorType type) { + switch (type) { + case ROOT: + return rootUpnpProcessor; + case PLAYLIST: + return playlistUpnpProcessor; + case FOLDER: + case MEDIAFILE: + return mediaFileUpnpProcessor; + case ALBUM: + return albumUpnpProcessor; + case RECENT: + return recentAlbumUpnpProcessor; + case ARTIST: + return artistUpnpProcessor; + case GENRE: + return genreUpnpProcessor; + case ARTISTALBUM: + case UNKNOWN: + return null; + } + return null; + } + + @Override + public MediaFileUpnpProcessor getMediaFileProcessor() { + return mediaFileUpnpProcessor; + } + + @Override + public ArtistUpnpProcessor getArtistProcessor() { + return artistUpnpProcessor; + } + + @Override + public AlbumUpnpProcessor getAlbumProcessor() { + return albumUpnpProcessor; + } + + @Override + public RecentAlbumUpnpProcessor getRecentProcessor() { + return recentAlbumUpnpProcessor; + } + + @Override + public GenreUpnpProcessor getGenreProcessor() { + return genreUpnpProcessor; + } + + @Override + public PlaylistUpnpProcessor getPlaylistProcessor() { + return playlistUpnpProcessor; + } + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpUtil.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpUtil.java new file mode 100644 index 000000000..66dad5ea5 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/UpnpUtil.java @@ -0,0 +1,115 @@ +/* + This file is part of Airsonic. + + Airsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Airsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Airsonic. If not, see . + + Copyright 2024 (C) Y.Tory + Copyright 2017 (C) Airsonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.airsonic.player.service.upnp; + +import org.airsonic.player.domain.CoverArtScheme; +import org.airsonic.player.domain.MediaFile; +import org.airsonic.player.domain.Player; +import org.airsonic.player.domain.User; +import org.airsonic.player.service.JWTSecurityService; +import org.airsonic.player.service.PlayerService; +import org.airsonic.player.service.SecurityService; +import org.airsonic.player.service.SettingsService; +import org.airsonic.player.service.TranscodingService; +import org.airsonic.player.util.StringUtil; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.fourthline.cling.support.model.Res; +import org.seamless.util.MimeType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +@Component +public class UpnpUtil { + + @Autowired + private SecurityService securityService; + + @Autowired + private PlayerService playerService; + + @Autowired + private JWTSecurityService jwtSecurityService; + + @Autowired + private SettingsService settingsService; + + @Autowired + private TranscodingService transcodingService; + + public Res createResourceForSong(MediaFile song) { + // Create a guest user if necessary + securityService.createGuestUserIfNotExists(); + Player player = playerService.getGuestPlayer(null); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("ext/stream") + .queryParam("id", song.getId()) + .queryParam("player", player.getId()); + + if (song.isVideo()) { + builder.queryParam("format", TranscodingService.FORMAT_RAW); + } + + builder = jwtSecurityService.addJWTToken(User.USERNAME_ANONYMOUS, builder); + + String url = getBaseUrl() + builder.toUriString(); + + String suffix = song.isVideo() ? FilenameUtils.getExtension(song.getPath()) : transcodingService.getSuffix(player, song, null); + String mimeTypeString = StringUtil.getMimeType(suffix); + MimeType mimeType = mimeTypeString == null ? null : MimeType.valueOf(mimeTypeString); + + Res res = new Res(mimeType, null, url); + res.setDuration(formatDuration(song.getDuration())); + return res; + } + + private String formatDuration(Double seconds) { + if (seconds == null) { + return null; + } + return StringUtil.formatDuration((long) (seconds * 1000), true); + } + + public String getBaseUrl() { + String dlnaBaseLANURL = settingsService.getDlnaBaseLANURL(); + if (StringUtils.isBlank(dlnaBaseLANURL)) { + throw new RuntimeException("DLNA Base LAN URL is not set correctly"); + } + return StringUtils.appendIfMissing(dlnaBaseLANURL, "/"); + } + + public URI getAlbumArtURI(int albumId) { + return UriComponentsBuilder + .fromUriString(getBaseUrl()) + .uriComponents(jwtSecurityService + .addJWTToken( + User.USERNAME_ANONYMOUS, + UriComponentsBuilder.fromUriString("ext/coverArt.view") + .queryParam("id", albumId) + .queryParam("size", CoverArtScheme.LARGE.getSize())) + .build()) + .build().encode().toUri(); + } + +} From e68f263df14af0aff1ab7800f4b573d8dfe76b2b Mon Sep 17 00:00:00 2001 From: "Y.Tory" <5343692+kagemomiji@users.noreply.github.com> Date: Wed, 29 May 2024 16:06:34 +0000 Subject: [PATCH 5/6] Update metrics dependencies to version 4.2.25 --- airsonic-main/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/airsonic-main/pom.xml b/airsonic-main/pom.xml index d79e4b500..bcd5c1d7f 100644 --- a/airsonic-main/pom.xml +++ b/airsonic-main/pom.xml @@ -49,10 +49,12 @@ io.dropwizard.metrics metrics-core + 4.2.25 io.dropwizard.metrics metrics-jmx + 4.2.25 From a4d1808f218489d1607ef24b0f9136d5d2eccea8 Mon Sep 17 00:00:00 2001 From: "Y.Tory" <5343692+kagemomiji@users.noreply.github.com> Date: Thu, 30 May 2024 00:05:31 +0000 Subject: [PATCH 6/6] Revert "Merge remote-tracking branch 'origin/dependabot/maven/org.springframework.boot-spring-boot-starter-parent-3.3.0' into issue474-fix-upnp-not-playable" This reverts commit c5266790f2af0468b60c8673b949357e5bfa909d, reversing changes made to d35a1219c4f74040da79fa122ebacd357c507d52. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f2f8aa63a..49283d8b6 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.0 + 3.2.5