|
|
|
/*
|
|
|
|
This file is part of Airsonic.
|
|
|
|
|
|
|
|
Airsonic is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Airsonic is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
Copyright 2016 (C) Airsonic Authors
|
|
|
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
|
|
|
*/
|
|
|
|
package org.airsonic.player.service.upnp;
|
|
|
|
|
|
|
|
import org.airsonic.player.domain.CoverArtScheme;
|
|
|
|
import org.airsonic.player.domain.MediaFile;
|
|
|
|
import org.airsonic.player.service.*;
|
|
|
|
import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode;
|
|
|
|
import org.fourthline.cling.support.contentdirectory.ContentDirectoryException;
|
|
|
|
import org.fourthline.cling.support.model.*;
|
|
|
|
import org.fourthline.cling.support.model.item.Item;
|
|
|
|
import org.fourthline.cling.support.model.item.MusicTrack;
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
import org.springframework.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 CustomContentDirectory {
|
|
|
|
|
|
|
|
public static final Logger LOG = LoggerFactory.getLogger(DispatchingContentDirectory.class);
|
|
|
|
|
|
|
|
public static final String CONTAINER_ID_ROOT = "0";
|
|
|
|
public static final String CONTAINER_ID_PLAYLIST_PREFIX = "playlist";
|
|
|
|
public static final String CONTAINER_ID_FOLDER_PREFIX = "folder";
|
|
|
|
public static final String CONTAINER_ID_ALBUM_PREFIX = "album";
|
|
|
|
public static final String CONTAINER_ID_ARTIST_PREFIX = "artist";
|
|
|
|
public static final String CONTAINER_ID_ARTISTALBUM_PREFIX = "artistalbum";
|
|
|
|
public static final String CONTAINER_ID_GENRE_PREFIX = "genre";
|
|
|
|
public static final String CONTAINER_ID_RECENT_PREFIX = "recent";
|
|
|
|
|
|
|
|
protected static final String SEPARATOR = "-";
|
|
|
|
|
|
|
|
@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;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|