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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+