From b3ae8faac6e603758b54bf4b1ab2adcd181574a4 Mon Sep 17 00:00:00 2001 From: Allen Petersen Date: Fri, 9 Jun 2017 20:48:18 -0400 Subject: [PATCH] Fixes Libresonic/libresonic#451 This replaces the existing FolderBasedContentDirectory with a new DispatchingContentDirectory. The new system then dispatches requests to subclasses of UpnpContentProcessor, which then handle the requests as appropriate. This version also implements a new searchByName method in SearchService, which is used to provide artist, album, and track search functionality for the upnp service. --- .../player/domain/ParamSearchResult.java | 59 ++++ .../player/service/SearchService.java | 65 ++++ .../player/service/UPnPService.java | 16 +- .../service/upnp/AlbumUpnpProcessor.java | 136 ++++++++ .../service/upnp/ArtistUpnpProcessor.java | 99 ++++++ .../upnp/DispatchingContentDirectory.java | 292 ++++++++++++++++++ .../service/upnp/GenreUpnpProcessor.java | 99 ++++++ .../service/upnp/MediaFileUpnpProcessor.java | 165 ++++++++++ .../service/upnp/PlaylistUpnpProcessor.java | 81 +++++ .../upnp/RecentAlbumUpnpProcessor.java | 50 +++ .../service/upnp/RootUpnpProcessor.java | 83 +++++ .../service/upnp/UpnpContentProcessor.java | 188 +++++++++++ .../resources/applicationContext-service.xml | 21 +- 13 files changed, 1345 insertions(+), 9 deletions(-) create mode 100644 libresonic-main/src/main/java/org/libresonic/player/domain/ParamSearchResult.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/AlbumUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/ArtistUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/DispatchingContentDirectory.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/GenreUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/MediaFileUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/PlaylistUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/RecentAlbumUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/RootUpnpProcessor.java create mode 100644 libresonic-main/src/main/java/org/libresonic/player/service/upnp/UpnpContentProcessor.java diff --git a/libresonic-main/src/main/java/org/libresonic/player/domain/ParamSearchResult.java b/libresonic-main/src/main/java/org/libresonic/player/domain/ParamSearchResult.java new file mode 100644 index 00000000..91c44ecb --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/domain/ParamSearchResult.java @@ -0,0 +1,59 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2016 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.domain; + +import org.libresonic.player.service.SearchService; + +import java.util.ArrayList; +import java.util.List; + +/** + * The outcome of a search. + * + * @author Sindre Mehus + * @see SearchService#search + */ +public class ParamSearchResult { + + private final List items = new ArrayList(); + + private int offset; + private int totalHits; + + public List getItems() { + return items; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getTotalHits() { + return totalHits; + } + + public void setTotalHits(int totalHits) { + this.totalHits = totalHits; + } +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/SearchService.java b/libresonic-main/src/main/java/org/libresonic/player/service/SearchService.java index 004a16c0..ce6cd90b 100644 --- a/libresonic-main/src/main/java/org/libresonic/player/service/SearchService.java +++ b/libresonic-main/src/main/java/org/libresonic/player/service/SearchService.java @@ -32,6 +32,7 @@ import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.queryParser.MultiFieldQueryParser; +import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.*; import org.apache.lucene.search.spans.SpanOrQuery; import org.apache.lucene.search.spans.SpanQuery; @@ -372,6 +373,70 @@ public class SearchService { return result; } + public ParamSearchResult searchByName(String name, int offset, int count, List folderList, Class clazz) { + IndexType indexType = null; + String field = null; + if (clazz.isAssignableFrom(Album.class)) { + indexType = IndexType.ALBUM_ID3; + field = FIELD_ALBUM; + } else if (clazz.isAssignableFrom(Artist.class)) { + indexType = IndexType.ARTIST_ID3; + field = FIELD_ARTIST; + } else if (clazz.isAssignableFrom(MediaFile.class)) { + indexType = IndexType.SONG; + field = FIELD_TITLE; + } + ParamSearchResult result = new ParamSearchResult(); + // we only support album, artist, and song for now + if (indexType == null || field == null) { + return result; + } + + result.setOffset(offset); + + IndexReader reader = null; + + try { + reader = createIndexReader(indexType); + Searcher searcher = new IndexSearcher(reader); + Analyzer analyzer = new LibresonicAnalyzer(); + QueryParser queryParser = new QueryParser(LUCENE_VERSION, field, analyzer); + + Query q = queryParser.parse(name + "*"); + + Sort sort = new Sort(new SortField(field, SortField.STRING)); + TopDocs topDocs = searcher.search(q, null, offset + count, sort); + result.setTotalHits(topDocs.totalHits); + + int start = Math.min(offset, topDocs.totalHits); + int end = Math.min(start + count, topDocs.totalHits); + for (int i = start; i < end; i++) { + Document doc = searcher.doc(topDocs.scoreDocs[i].doc); + switch (indexType) { + case SONG: + MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID))); + addIfNotNull(clazz.cast(mediaFile), result.getItems()); + break; + case ARTIST_ID3: + Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID))); + addIfNotNull(clazz.cast(artist), result.getItems()); + break; + case ALBUM_ID3: + Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID))); + addIfNotNull(clazz.cast(album), result.getItems()); + break; + default: + break; + } + } + } catch (Throwable x) { + LOG.error("Failed to execute Lucene search.", x); + } finally { + FileUtil.closeQuietly(reader); + } + return result; + } + private void addIfNotNull(T value, List list) { if (value != null) { list.add(value); diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/UPnPService.java b/libresonic-main/src/main/java/org/libresonic/player/service/UPnPService.java index 4b291e32..c6e06c4b 100644 --- a/libresonic-main/src/main/java/org/libresonic/player/service/UPnPService.java +++ b/libresonic-main/src/main/java/org/libresonic/player/service/UPnPService.java @@ -33,7 +33,7 @@ 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.libresonic.player.service.upnp.ApacheUpnpServiceConfiguration; -import org.libresonic.player.service.upnp.FolderBasedContentDirectory; +import org.libresonic.player.service.upnp.LibresonicContentDirectory; import org.libresonic.player.service.upnp.MSMediaReceiverRegistrarService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,7 +53,7 @@ public class UPnPService { private SettingsService settingsService; private UpnpService upnpService; - private FolderBasedContentDirectory folderBasedContentDirectory; + private LibresonicContentDirectory libresonicContentDirectory; private AtomicReference running = new AtomicReference<>(false); public void init() { @@ -147,12 +147,12 @@ public class UPnPService { Icon icon = new Icon("image/png", 512, 512, 32, "logo-512", getClass().getResourceAsStream("logo-512.png")); - LocalService contentDirectoryservice = new AnnotationLocalServiceBinder().read(FolderBasedContentDirectory.class); - contentDirectoryservice.setManager(new DefaultServiceManager(contentDirectoryservice) { + LocalService contentDirectoryservice = new AnnotationLocalServiceBinder().read(LibresonicContentDirectory.class); + contentDirectoryservice.setManager(new DefaultServiceManager(contentDirectoryservice) { @Override - protected FolderBasedContentDirectory createServiceInstance() throws Exception { - return folderBasedContentDirectory; + protected LibresonicContentDirectory createServiceInstance() throws Exception { + return libresonicContentDirectory; } }); @@ -205,7 +205,7 @@ public class UPnPService { this.settingsService = settingsService; } - public void setFolderBasedContentDirectory(FolderBasedContentDirectory folderBasedContentDirectory) { - this.folderBasedContentDirectory = folderBasedContentDirectory; + public void setLibresonicContentDirectory(LibresonicContentDirectory libresonicContentDirectory) { + this.libresonicContentDirectory = libresonicContentDirectory; } } diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/AlbumUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/AlbumUpnpProcessor.java new file mode 100644 index 00000000..1a893e7c --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/AlbumUpnpProcessor.java @@ -0,0 +1,136 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.PersonWithRole; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.MusicAlbum; +import org.libresonic.player.dao.AlbumDao; +import org.libresonic.player.dao.MediaFileDao; +import org.libresonic.player.domain.*; +import org.libresonic.player.service.SearchService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +public class AlbumUpnpProcessor extends UpnpContentProcessor { + + public static final String ALL_BY_ARTIST = "allByArtist"; + public static final String ALL_RECENT = "allRecent"; + + @Autowired + AlbumDao albumDao; + + @Autowired + SearchService searchService; + + public AlbumUpnpProcessor() { + setRootId(DispatchingContentDirectory.CONTAINER_ID_ALBUM_PREFIX); + setRootTitle("Albums"); + } + + public Container createContainer(Album album) throws Exception { + MusicAlbum container = new MusicAlbum(); + + if (album.getId() == -1) { + container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + album.getComment()); + } else { + container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + album.getId()); + container.setAlbumArtURIs(new URI[] { getAlbumArtURI(album.getId()) }); + container.setDescription(album.getComment()); + } + container.setParentID(getRootId()); + container.setTitle(album.getName()); + // TODO: correct artist? + if (album.getArtist() != null) { + container.setArtists(getAlbumArtists(album.getArtist())); + } + return container; + } + + public List getAllItems() { + List allFolders = getDispatchingContentDirectory().getSettingsService().getAllMusicFolders(); + return getAlbumDao().getAlphabetialAlbums(0, 0, false, allFolders); + } + + public Album getItemById(String id) throws Exception { + Album returnValue = null; + if (id.startsWith(ALL_BY_ARTIST) || id.equalsIgnoreCase(ALL_RECENT)) { + returnValue = new Album(); + returnValue.setId(-1); + returnValue.setComment(id); + } else { + returnValue = getAlbumDao().getAlbum(Integer.parseInt(id)); + } + return returnValue; + } + + public List getChildren(Album album) throws Exception { + List allFiles = getMediaFileDao().getSongsForAlbum(album.getArtist(), album.getName()); + if (album.getId() == -1) { + List albumList = null; + if (album.getComment().startsWith(ALL_BY_ARTIST)) { + ArtistUpnpProcessor ap = getDispatcher().getArtistProcessor(); + albumList = ap.getChildren(ap.getItemById(album.getComment().replaceAll(ALL_BY_ARTIST + "_", ""))); + } else if (album.getComment().equalsIgnoreCase(ALL_RECENT)) { + albumList = getDispatcher().getRecentAlbumProcessor().getAllItems(); + } + for (Album a: albumList) { + if (a.getId() != -1) { + allFiles.addAll(getMediaFileDao().getSongsForAlbum(a.getArtist(), a.getName())); + } + } + } else { + allFiles = getMediaFileDao().getSongsForAlbum(album.getArtist(), album.getName()); + } + return allFiles; + } + + public void addChild(DIDLContent didl, MediaFile child) throws Exception { + didl.addItem(getDispatcher().getMediaFileProcessor().createItem(child)); + } + + public URI getAlbumArtURI(int albumId) throws URISyntaxException { + return getDispatcher().getJwtSecurityService().addJWTToken(UriComponentsBuilder.fromUriString(getDispatcher().getBaseUrl() + "/ext/coverArt.view").queryParam("id", albumId).queryParam("size", CoverArtScheme.LARGE.getSize())).build().encode().toUri(); + } + + public PersonWithRole[] getAlbumArtists(String artist) { + return new PersonWithRole[] { new PersonWithRole(artist) }; + } + + public AlbumDao getAlbumDao() { + return albumDao; + } + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public MediaFileDao getMediaFileDao() { + return getDispatcher().getMediaFileProcessor().getMediaFileDao(); + } + +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/ArtistUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/ArtistUpnpProcessor.java new file mode 100644 index 00000000..c8145173 --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/ArtistUpnpProcessor.java @@ -0,0 +1,99 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; + +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.MusicArtist; +import org.libresonic.player.dao.ArtistDao; +import org.libresonic.player.domain.*; +import org.libresonic.player.service.SearchService; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +public class ArtistUpnpProcessor extends UpnpContentProcessor { + + @Autowired + private ArtistDao artistDao; + + @Autowired + SearchService searchService; + + public ArtistUpnpProcessor() { + setRootId(DispatchingContentDirectory.CONTAINER_ID_ARTIST_PREFIX); + setRootTitle("Artists"); + } + + public Container createContainer(Artist artist) { + MusicArtist container = new MusicArtist(); + container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + artist.getId()); + container.setParentID(getRootId()); + container.setTitle(artist.getName()); + container.setChildCount(artist.getAlbumCount()); + + return container; + } + + public List getAllItems() { + List allFolders = getDispatcher().getSettingsService().getAllMusicFolders(); + List allArtists = getArtistDao().getAlphabetialArtists(0, Integer.MAX_VALUE, allFolders); + // alpha artists doesn't quite work :P + allArtists.sort((Artist o1, Artist o2)->o1.getName().replaceAll("\\W", "").compareToIgnoreCase(o2.getName().replaceAll("\\W", ""))); + + return allArtists; + } + + public Artist getItemById(String id) throws Exception { + return getArtistDao().getArtist(Integer.parseInt(id)); + } + + public List getChildren(Artist artist) { + List allFolders = getDispatcher().getSettingsService().getAllMusicFolders(); + List allAlbums = getAlbumProcessor().getAlbumDao().getAlbumsForArtist(artist.getName(), allFolders); + if (allAlbums.size() > 1) { + Album viewAll = new Album(); + viewAll.setName("- All Albums -"); + viewAll.setId(-1); + viewAll.setComment(AlbumUpnpProcessor.ALL_BY_ARTIST + "_" + artist.getId()); + allAlbums.add(0, viewAll); + } + return allAlbums; + } + + public void addChild(DIDLContent didl, Album album) throws Exception { + didl.addContainer(getAlbumProcessor().createContainer(album)); + } + + public ArtistDao getArtistDao() { + return artistDao; + } + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public AlbumUpnpProcessor getAlbumProcessor() { + return getDispatcher().getAlbumProcessor(); + } +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/DispatchingContentDirectory.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/DispatchingContentDirectory.java new file mode 100644 index 00000000..93056c00 --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/DispatchingContentDirectory.java @@ -0,0 +1,292 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2016 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; + +import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; +import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; +import org.fourthline.cling.support.model.BrowseFlag; +import org.fourthline.cling.support.model.BrowseResult; +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.PersonWithRole; +import org.fourthline.cling.support.model.SortCriterion; +import org.fourthline.cling.support.model.item.Item; +import org.fourthline.cling.support.model.item.MusicTrack; +import org.libresonic.player.domain.*; +import org.libresonic.player.service.JWTSecurityService; +import org.libresonic.player.service.MediaFileService; +import org.libresonic.player.service.MusicIndexService; +import org.libresonic.player.service.PlaylistService; +import org.libresonic.player.service.SearchService; +import org.libresonic.player.service.SettingsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; + +/** + * @author Allen Petersen + * @author Sindre Mehus + * @version $Id$ + */ +public class DispatchingContentDirectory extends LibresonicContentDirectory { + + public static final Logger LOG = LoggerFactory.getLogger(FolderBasedContentDirectory.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 = "-"; + + @Autowired + private PlaylistUpnpProcessor playlistProcessor; + @Autowired + private MediaFileUpnpProcessor mediaFileProcessor; + //@Autowired can't autowire because of the subclassing :P + private AlbumUpnpProcessor albumProcessor; + //@Autowired can't autowire because of the subclassing :P + private RecentAlbumUpnpProcessor recentAlbumProcessor; + @Autowired + private ArtistUpnpProcessor artistProcessor; + @Autowired + private GenreUpnpProcessor genreProcessor; + @Autowired + private RootUpnpProcessor rootProcessor; + + @Autowired + private MediaFileService mediaFileService; + + private PlaylistService playlistService; + + @Autowired + private MusicIndexService musicIndexService; + + @Autowired + private SearchService searchService; + + + @Override + public BrowseResult browse(String objectId, BrowseFlag browseFlag, + String filter, long firstResult, + long maxResults, SortCriterion[] orderBy) + throws ContentDirectoryException { + + 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"); + + // maxResult == 0 means all. + if (maxResults == 0) { + maxResults = Long.MAX_VALUE; + } + + BrowseResult returnValue = null; + try { + String[] splitId = objectId.split(SEPARATOR); + String browseRoot = splitId[0]; + String itemId = splitId.length == 1 ? null : splitId[1]; + + UpnpContentProcessor processor = findProcessor(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; + browseRoot = "folder"; + processor = getMediaFileProcessor(); + } + + if (itemId == null) { + 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); + } + return returnValue; + } catch (Throwable x) { + LOG.error("UPnP error: " + x, x); + throw new ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, x.toString()); + } + } + + @Override + public BrowseResult search(String containerId, + 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); + } else if ("object.item.audioItem".equalsIgnoreCase(upnpClass)) { + returnValue = getMediaFileProcessor().searchByName(titleSearch, firstResult, maxResults, orderBy); + } else if ("object.container.album.musicAlbum".equalsIgnoreCase(upnpClass)) { + returnValue = 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) throws Exception { + 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) throws URISyntaxException { + return jwtSecurityService.addJWTToken(UriComponentsBuilder.fromUriString(getBaseUrl() + "/ext/coverArt.view").queryParam("id", id).queryParam("size", CoverArtScheme.LARGE.getSize())).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 albumProcessor; + } + public void setAlbumProcessor(AlbumUpnpProcessor albumProcessor) { + this.albumProcessor = albumProcessor; + } + + public RecentAlbumUpnpProcessor getRecentAlbumProcessor() { + return recentAlbumProcessor; + } + public void setRecentAlbumProcessor(RecentAlbumUpnpProcessor recentAlbumProcessor) { + this.recentAlbumProcessor = 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; + } + + 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/libresonic-main/src/main/java/org/libresonic/player/service/upnp/GenreUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/GenreUpnpProcessor.java new file mode 100644 index 00000000..e85bf5a9 --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/GenreUpnpProcessor.java @@ -0,0 +1,99 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; +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.libresonic.player.domain.*; +import org.libresonic.player.util.Util; + +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +public class GenreUpnpProcessor extends UpnpContentProcessor { + + public GenreUpnpProcessor() { + setRootId(DispatchingContentDirectory.CONTAINER_ID_GENRE_PREFIX); + setRootTitle("Genres"); + } + + /** + * Browses the top-level content of a type. + */ + public BrowseResult browseRoot(String filter, long firstResult, long maxResults, SortCriterion[] orderBy) throws Exception { + // we have to override this to do an index-based id. + DIDLContent didl = new DIDLContent(); + List allItems = getAllItems(); + if (filter != null) { + // filter items + } + if (orderBy != null) { + // sort items + } + List selectedItems = Util.subList(allItems, firstResult, maxResults); + for (int i=0; i < selectedItems.size(); i++) { + Genre item = selectedItems.get(i); + didl.addContainer(createContainer(item, (int) (i+firstResult))); + } + return createBrowseResult(didl, (int) didl.getCount(), allItems.size()); + } + + public Container createContainer(Genre item) { + // genre uses index because we don't have a proper id + return null; + } + + public Container createContainer(Genre item, int index) { + GenreContainer container = new GenreContainer(); + container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + index); + container.setParentID(getRootId()); + container.setTitle(item.getName()); + container.setChildCount(item.getAlbumCount()); + + return container; + } + + public List getAllItems() { + return getDispatcher().getMediaFileService().getGenres(false); + } + + public Genre getItemById(String id) { + int index = Integer.parseInt(id); + List allGenres = getAllItems(); + if (allGenres.size() > index) { + return allGenres.get(index); + } + return null; + } + + public List getChildren(Genre item) { + List allFolders = getDispatcher().getSettingsService().getAllMusicFolders(); + return getDispatcher().getMediaFileProcessor().getMediaFileDao().getSongsByGenre(item.getName(), 0, Integer.MAX_VALUE, allFolders); + } + + public void addChild(DIDLContent didl, MediaFile child) throws Exception { + didl.addItem(getDispatcher().getMediaFileProcessor().createItem(child)); + } +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/MediaFileUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/MediaFileUpnpProcessor.java new file mode 100644 index 00000000..ce3d1ae3 --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/MediaFileUpnpProcessor.java @@ -0,0 +1,165 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; + +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.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.libresonic.player.dao.MediaFileDao; +import org.libresonic.player.domain.*; +import org.libresonic.player.service.MediaFileService; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +public class MediaFileUpnpProcessor extends UpnpContentProcessor { + + @Autowired + MediaFileDao mediaFileDao; + + public MediaFileUpnpProcessor() { + setRootId(DispatchingContentDirectory.CONTAINER_ID_FOLDER_PREFIX); + setRootTitle("Folders"); + } + + @Override + // overriding for the case of browsing a file + public BrowseResult browseObjectMetadata(String id) throws Exception { + MediaFile item = getItemById(id); + DIDLContent didl = new DIDLContent(); + addChild(didl, item); + return createBrowseResult(didl, 1, 1); + } + + public Container createContainer(MediaFile item) throws Exception { + MusicAlbum container = new MusicAlbum(); + if (item.isAlbum()) { + container.setAlbumArtURIs(new URI[] { getDispatcher().getAlbumProcessor().getAlbumArtURI(item.getId())}); + + if (item.getArtist() != null) { + container.setArtists(getDispatcher().getAlbumProcessor().getAlbumArtists(item.getArtist())); + } + container.setDescription(item.getComment()); + } + container.setId(DispatchingContentDirectory.CONTAINER_ID_FOLDER_PREFIX + 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 (parent != null) { + container.setParentID(String.valueOf(parent.getId())); + } + } else { + container.setParentID(DispatchingContentDirectory.CONTAINER_ID_FOLDER_PREFIX); + } + return container; + } + + public List getAllItems() throws Exception { + List allFolders = getDispatcher().getSettingsService().getAllMusicFolders(); + List returnValue = new ArrayList(); + if (allFolders.size() == 1) { + // if there's only one root folder just return it + return getChildren(getMediaFileService().getMediaFile(allFolders.get(0).getPath())); + } else { + for (MusicFolder folder : allFolders) { + returnValue.add(getMediaFileService().getMediaFile(folder.getPath())); + } + } + return returnValue; + } + + public MediaFile getItemById(String id) throws Exception { + return getMediaFileService().getMediaFile(Integer.parseInt(id)); + } + + public List getChildren(MediaFile item) { + List children = getMediaFileService().getChildrenOf(item, true, true, true); + children.sort((MediaFile o1, MediaFile o2)->o1.getPath().replaceAll("\\W", "").compareToIgnoreCase(o2.getPath().replaceAll("\\W", ""))); + return children; + } + + public void addItem(DIDLContent didl, MediaFile item) throws Exception { + if (item.isFile()) { + didl.addItem(createItem(item)); + } else { + didl.addContainer(createContainer(item)); + } + } + + public void addChild(DIDLContent didl, MediaFile child) throws Exception { + if (child.isFile()) { + didl.addItem(createItem(child)); + } else { + didl.addContainer(createContainer(child)); + } + } + + public Item createItem(MediaFile song) throws Exception { + MediaFile parent = getMediaFileService().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())); + } + 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(getDispatcher().createResourceForSong(song))); + item.setDescription(song.getComment()); + item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(getDispatcher().getAlbumProcessor().getAlbumArtURI(parent.getId()))); + + return item; + } + + public MediaFileService getMediaFileService() { + return getDispatchingContentDirectory().getMediaFileService(); + } + + public MediaFileDao getMediaFileDao() { + return mediaFileDao; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/PlaylistUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/PlaylistUpnpProcessor.java new file mode 100644 index 00000000..88c79e2a --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/PlaylistUpnpProcessor.java @@ -0,0 +1,81 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; + +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.PlaylistContainer; +import org.libresonic.player.domain.*; +import org.libresonic.player.service.PlaylistService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +@Component +public class PlaylistUpnpProcessor extends UpnpContentProcessor { + @Autowired + private PlaylistService playlistService; + + public PlaylistUpnpProcessor() { + setRootId(DispatchingContentDirectory.CONTAINER_ID_PLAYLIST_PREFIX); + setRootTitle("Playlists"); + } + + public Container createContainer(Playlist item) { + PlaylistContainer container = new PlaylistContainer(); + container.setId(getRootId() + DispatchingContentDirectory.SEPARATOR + item.getId()); + container.setParentID(getRootId()); + container.setTitle(item.getName()); + container.setDescription(item.getComment()); + container.setChildCount(getPlaylistService().getFilesInPlaylist(item.getId()).size()); + + return container; + } + + public List getAllItems() { + List playlists = getPlaylistService().getAllPlaylists(); + return playlists; + } + + public Playlist getItemById(String id) throws Exception { + return getDispatcher().getPlaylistService().getPlaylist(Integer.parseInt(id)); + } + + public List getChildren(Playlist item) { + return getPlaylistService().getFilesInPlaylist(item.getId()); + } + + public void addChild(DIDLContent didl, MediaFile child) throws Exception { + didl.addItem(getDispatchingContentDirectory().createItem(child)); + } + + public PlaylistService getPlaylistService() { + return this.playlistService; + } + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/RecentAlbumUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/RecentAlbumUpnpProcessor.java new file mode 100644 index 00000000..c85784ff --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/RecentAlbumUpnpProcessor.java @@ -0,0 +1,50 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; +import org.libresonic.player.domain.Album; +import org.libresonic.player.domain.MusicFolder; + +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +public class RecentAlbumUpnpProcessor extends AlbumUpnpProcessor { + private final static int RECENT_COUNT = 50; + + public RecentAlbumUpnpProcessor() { + setRootId(DispatchingContentDirectory.CONTAINER_ID_RECENT_PREFIX); + setRootTitle("RecentAlbums"); + } + + public List getAllItems() { + List allFolders = getDispatchingContentDirectory().getSettingsService().getAllMusicFolders(); + List recentAlbums = getAlbumDao().getNewestAlbums(0, RECENT_COUNT, allFolders); + if (recentAlbums.size() > 1) { + Album viewAll = new Album(); + viewAll.setName("- All Albums -"); + viewAll.setId(-1); + viewAll.setComment(AlbumUpnpProcessor.ALL_RECENT); + recentAlbums.add(0, viewAll); + } + return recentAlbums; + } +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/RootUpnpProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/RootUpnpProcessor.java new file mode 100644 index 00000000..6fc9daca --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/RootUpnpProcessor.java @@ -0,0 +1,83 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.WriteStatus; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.StorageFolder; +import org.libresonic.player.domain.MediaLibraryStatistics; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +@Component +public class RootUpnpProcessor extends UpnpContentProcessor { + public Container createRootContainer() { + StorageFolder root = new StorageFolder(); + root.setId(DispatchingContentDirectory.CONTAINER_ID_ROOT); + root.setParentID("-1"); + + MediaLibraryStatistics statistics = getDispatchingContentDirectory().getSettingsService().getMediaLibraryStatistics(); + // returning large storageUsed values doesn't play nicely with + // some upnp clients + //root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes()); + root.setStorageUsed(-1L); + root.setTitle("Libresonic Media"); + root.setRestricted(true); + root.setSearchable(true); + root.setWriteStatus(WriteStatus.NOT_WRITABLE); + + root.setChildCount(6); + return root; + } + + public Container createContainer(Container item) { + // the items are the containers in this case. + return item; + } + + public List 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()); + return allItems; + } + + public Container getItemById(String id) { + return createRootContainer(); + } + + public List getChildren(Container item) throws Exception { + return getAllItems(); + } + + public void addChild(DIDLContent didl, Container child) { + // special case; root doesn't have object instances + } +} diff --git a/libresonic-main/src/main/java/org/libresonic/player/service/upnp/UpnpContentProcessor.java b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/UpnpContentProcessor.java new file mode 100644 index 00000000..25db011a --- /dev/null +++ b/libresonic-main/src/main/java/org/libresonic/player/service/upnp/UpnpContentProcessor.java @@ -0,0 +1,188 @@ +/* + This file is part of Libresonic. + + Libresonic 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. + + Libresonic 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 Libresonic. If not, see . + + Copyright 2017 (C) Libresonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus +*/ +package org.libresonic.player.service.upnp; + +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.SortCriterion; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.StorageFolder; +import org.libresonic.player.domain.MusicFolder; +import org.libresonic.player.domain.ParamSearchResult; +import org.libresonic.player.util.Util; +import org.springframework.beans.factory.annotation.Autowired; + +import java.lang.reflect.ParameterizedType; +import java.util.List; + +/** + * @author Allen Petersen + * @version $Id$ + */ +public abstract class UpnpContentProcessor { + + @Autowired + private DispatchingContentDirectory dispatchingContentDirectory; + + protected String rootTitle; + protected String rootId; + + /** + * Browses the root metadata for a type. + */ + public BrowseResult browseRootMetadata() throws Exception { + DIDLContent didl = new DIDLContent(); + didl.addContainer(createRootContainer()); + return createBrowseResult(didl, 1, 1); + } + + public Container createRootContainer() throws Exception { + Container container = new StorageFolder(); + container.setId(getRootId()); + container.setTitle(getRootTitle()); + + int childCount = getAllItemsSize(); + container.setChildCount(childCount); + container.setParentID(DispatchingContentDirectory.CONTAINER_ID_ROOT); + return container; + } + + /** + * Browses the top-level content of a type. + */ + public BrowseResult browseRoot(String filter, long firstResult, long maxResults, SortCriterion[] orderBy) throws Exception { + DIDLContent didl = new DIDLContent(); + List allItems = getAllItems(); + if (filter != null) { + // filter items (not implemented yet) + } + if (orderBy != null) { + // sort items (not implemented yet) + } + List selectedItems = Util.subList(allItems, firstResult, maxResults); + for (T item : selectedItems) { + addItem(didl, item); + } + + return createBrowseResult(didl, (int) didl.getCount(), allItems.size()); + } + + /** + * Browses metadata for a child. + */ + public BrowseResult browseObjectMetadata(String id) throws Exception { + T item = getItemById(id); + DIDLContent didl = new DIDLContent(); + addItem(didl, item); + return createBrowseResult(didl, 1, 1); + } + + /** + * Browses a child of the container. + */ + public BrowseResult browseObject(String id, String filter, long firstResult, long maxResults, SortCriterion[] orderBy) throws Exception { + T item = getItemById(id); + List allChildren = getChildren(item); + if (filter != null) { + // filter items (not implemented yet) + } + if (orderBy != null) { + // sort items (not implemented yet) + } + List selectedChildren = Util.subList(allChildren, firstResult, maxResults); + + DIDLContent didl = new DIDLContent(); + for (U child : selectedChildren) { + addChild(didl, child); + } + return createBrowseResult(didl, selectedChildren.size(), allChildren.size()); + } + + protected BrowseResult createBrowseResult(DIDLContent didl, int count, int totalMatches) throws Exception { + return new BrowseResult(new DIDLParser().generate(didl), count, totalMatches); + } + + public BrowseResult searchByName(String name, + long firstResult, long maxResults, + SortCriterion[] orderBy) + throws ContentDirectoryException { + DIDLContent didl = new DIDLContent(); + + Class clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + + try { + List allFolders = getDispatchingContentDirectory().getSettingsService().getAllMusicFolders(); + ParamSearchResult result = getDispatcher().getSearchService().searchByName(name, (int) firstResult, (int) maxResults, allFolders, clazz); + List selectedItems = result.getItems(); + for (T item : selectedItems) { + addItem(didl, item); + } + + return createBrowseResult(didl, (int) 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) throws Exception { + didl.addContainer(createContainer(item)); + } + + // this can probably be optimized in some cases + public int getAllItemsSize() throws Exception { + return getAllItems().size(); + } + + public abstract Container createContainer(T item) throws Exception; + + public abstract List getAllItems() throws Exception; + + public abstract T getItemById(String id) throws Exception; + + public abstract List getChildren(T item) throws Exception; + + public abstract void addChild(DIDLContent didl, U child) throws Exception; + + public String getRootTitle() { + return rootTitle; + } + public void setRootTitle(String rootTitle) { + this.rootTitle = rootTitle; + } + public String getRootId() { + return rootId; + } + public void setRootId(String rootId) { + this.rootId = rootId; + } +} + diff --git a/libresonic-main/src/main/resources/applicationContext-service.xml b/libresonic-main/src/main/resources/applicationContext-service.xml index 49f95547..f301b806 100644 --- a/libresonic-main/src/main/resources/applicationContext-service.xml +++ b/libresonic-main/src/main/resources/applicationContext-service.xml @@ -194,9 +194,28 @@ + + + + + + + + + + + + + + + + + + + - +