Update to mostly support subsonic api 1.15.0

Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
master
Andrew DeMaria 8 years ago
parent bcd330e74d
commit c9931f5906
No known key found for this signature in database
GPG Key ID: 0A3F5E91F8364EDF
  1. 34
      libresonic-main/src/main/java/org/libresonic/player/controller/JAXBWriter.java
  2. 151
      libresonic-main/src/main/java/org/libresonic/player/controller/RESTController.java
  3. 2
      libresonic-main/src/main/java/org/libresonic/player/security/GlobalSecurityConfig.java
  4. 70
      libresonic-rest-api/src/main/resources/libresonic-rest-api.xsd

@ -26,12 +26,12 @@ import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.input.SAXBuilder;
import org.libresonic.player.util.StringUtil;
import org.libresonic.restapi.Error;
import org.libresonic.restapi.ObjectFactory;
import org.libresonic.restapi.Response;
import org.libresonic.restapi.ResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subsonic.restapi.Error;
import org.subsonic.restapi.ObjectFactory;
import org.subsonic.restapi.Response;
import org.subsonic.restapi.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -40,6 +40,7 @@ import javax.xml.bind.Marshaller;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Date;
@ -69,20 +70,30 @@ public class JAXBWriter {
}
}
private Marshaller createXmlMarshaller() throws JAXBException {
Marshaller marshaller = jaxbContext.createMarshaller();
private Marshaller createXmlMarshaller() {
Marshaller marshaller = null;
try {
marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, StringUtil.ENCODING_UTF8);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
return marshaller;
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
private Marshaller createJsonMarshaller() throws JAXBException {
Marshaller marshaller = jaxbContext.createMarshaller();
private Marshaller createJsonMarshaller() {
try {
Marshaller marshaller;
marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, StringUtil.ENCODING_UTF8);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, true);
return marshaller;
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
private String getRESTProtocolVersion() throws Exception {
@ -105,11 +116,10 @@ public class JAXBWriter {
Response response = new ObjectFactory().createResponse();
response.setStatus(ok ? ResponseStatus.OK : ResponseStatus.FAILED);
response.setVersion(restProtocolVersion);
response.setType("Libresonic");
return response;
}
public void writeResponse(HttpServletRequest request, HttpServletResponse httpResponse, Response jaxbResponse) throws Exception {
public void writeResponse(HttpServletRequest request, HttpServletResponse httpResponse, Response jaxbResponse) {
String format = getStringParameter(request, "f", "xml");
String jsonpCallback = request.getParameter("callback");
@ -140,9 +150,9 @@ public class JAXBWriter {
writer.append(");");
}
httpResponse.getWriter().append(writer.getBuffer());
} catch (Exception x) {
} catch (JAXBException | IOException x) {
LOG.error("Failed to marshal JAXB", x);
throw x;
throw new RuntimeException(x);
}
}

@ -20,6 +20,7 @@
package org.libresonic.player.controller;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.libresonic.player.ajax.LyricsInfo;
import org.libresonic.player.ajax.LyricsService;
import org.libresonic.player.ajax.PlayQueueService;
@ -41,17 +42,17 @@ import org.libresonic.player.service.*;
import org.libresonic.player.util.Pair;
import org.libresonic.player.util.StringUtil;
import org.libresonic.player.util.Util;
import org.libresonic.restapi.*;
import org.libresonic.restapi.Genres;
import org.libresonic.restapi.PodcastStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.subsonic.restapi.*;
import org.subsonic.restapi.PodcastStatus;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
@ -137,10 +138,15 @@ public class RESTController {
private BookmarkDao bookmarkDao;
@Autowired
private PlayQueueDao playQueueDao;
@Autowired
private MediaScannerService mediaScannerService;
private final Map<BookmarkKey, Bookmark> bookmarkCache = new ConcurrentHashMap<BookmarkKey, Bookmark>();
private final JAXBWriter jaxbWriter = new JAXBWriter();
private static final String NOT_YET_IMPLEMENTED = "Not yet implemented";
private static final String NO_LONGER_SUPPORTED = "No longer supported";
@PostConstruct
public void init() {
refreshBookmarkCache();
@ -192,7 +198,7 @@ public class RESTController {
MusicFolders musicFolders = new MusicFolders();
String username = securityService.getCurrentUsername(request);
for (MusicFolder musicFolder : settingsService.getMusicFoldersForUser(username)) {
org.libresonic.restapi.MusicFolder mf = new org.libresonic.restapi.MusicFolder();
org.subsonic.restapi.MusicFolder mf = new org.subsonic.restapi.MusicFolder();
mf.setId(musicFolder.getId());
mf.setName(musicFolder.getName());
musicFolders.getMusicFolder().add(mf);
@ -246,7 +252,7 @@ public class RESTController {
for (MediaFile mediaFile : artist.getMediaFiles()) {
if (mediaFile.isDirectory()) {
Date starredDate = mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username);
org.libresonic.restapi.Artist a = new org.libresonic.restapi.Artist();
org.subsonic.restapi.Artist a = new org.subsonic.restapi.Artist();
index.getArtist().add(a);
a.setId(String.valueOf(mediaFile.getId()));
a.setName(artist.getName());
@ -275,10 +281,10 @@ public class RESTController {
@RequestMapping(value = "/getGenres", method = {RequestMethod.GET, RequestMethod.POST})
public void getGenres(HttpServletRequest request, HttpServletResponse response) throws Exception {
request = wrapRequest(request);
Genres genres = new Genres();
org.subsonic.restapi.Genres genres = new org.subsonic.restapi.Genres();
for (Genre genre : mediaFileDao.getGenres(false)) {
org.libresonic.restapi.Genre g = new org.libresonic.restapi.Genre();
org.subsonic.restapi.Genre g = new org.subsonic.restapi.Genre();
genres.getGenre().add(g);
g.setContent(genre.getName());
g.setAlbumCount(genre.getAlbumCount());
@ -498,8 +504,8 @@ public class RESTController {
return jaxbArtist;
}
private org.libresonic.restapi.Artist createJaxbArtist(MediaFile artist, String username) {
org.libresonic.restapi.Artist result = new org.libresonic.restapi.Artist();
private org.subsonic.restapi.Artist createJaxbArtist(MediaFile artist, String username) {
org.subsonic.restapi.Artist result = new org.subsonic.restapi.Artist();
result.setId(String.valueOf(artist.getId()));
result.setName(artist.getArtist());
Date starred = mediaFileDao.getMediaFileStarredDate(artist.getId(), username);
@ -552,7 +558,7 @@ public class RESTController {
return jaxbAlbum;
}
private <T extends org.libresonic.restapi.Playlist> T createJaxbPlaylist(T jaxbPlaylist, Playlist playlist) {
private <T extends org.subsonic.restapi.Playlist> T createJaxbPlaylist(T jaxbPlaylist, Playlist playlist) {
jaxbPlaylist.setId(String.valueOf(playlist.getId()));
jaxbPlaylist.setName(playlist.getName());
jaxbPlaylist.setComment(playlist.getComment());
@ -644,6 +650,7 @@ public class RESTController {
}
directory.setName(dir.getName());
directory.setStarred(jaxbWriter.convertDate(mediaFileDao.getMediaFileStarredDate(id, username)));
directory.setPlayCount((long) dir.getPlayCount());
if (dir.isAlbum()) {
directory.setAverageRating(ratingService.getAverageRating(dir));
@ -691,7 +698,7 @@ public class RESTController {
List<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(username);
SearchResult result = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG);
org.libresonic.restapi.SearchResult searchResult = new org.libresonic.restapi.SearchResult();
org.subsonic.restapi.SearchResult searchResult = new org.subsonic.restapi.SearchResult();
searchResult.setOffset(result.getOffset());
searchResult.setTotalHits(result.getTotalHits());
@ -799,7 +806,7 @@ public class RESTController {
Playlists result = new Playlists();
for (Playlist playlist : playlistService.getReadablePlaylistsForUser(requestedUsername)) {
result.getPlaylist().add(createJaxbPlaylist(new org.libresonic.restapi.Playlist(), playlist));
result.getPlaylist().add(createJaxbPlaylist(new org.subsonic.restapi.Playlist(), playlist));
}
Response res = createResponse();
@ -1246,6 +1253,7 @@ public class RESTController {
child.setStarred(jaxbWriter.convertDate(mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username)));
child.setUserRating(ratingService.getRatingForUser(username, mediaFile));
child.setAverageRating(ratingService.getAverageRating(mediaFile));
child.setPlayCount((long) mediaFile.getPlayCount());
if (mediaFile.isFile()) {
child.setDuration(mediaFile.getDurationSeconds());
@ -1544,7 +1552,7 @@ public class RESTController {
for (PodcastChannel channel : podcastService.getAllChannels()) {
if (channelId == null || channelId.equals(channel.getId())) {
org.libresonic.restapi.PodcastChannel c = new org.libresonic.restapi.PodcastChannel();
org.subsonic.restapi.PodcastChannel c = new org.subsonic.restapi.PodcastChannel();
result.getChannel().add(c);
c.setId(String.valueOf(channel.getId()));
@ -1587,13 +1595,13 @@ public class RESTController {
jaxbWriter.writeResponse(request, response, res);
}
private org.libresonic.restapi.PodcastEpisode createJaxbPodcastEpisode(Player player, String username, PodcastEpisode episode) {
org.libresonic.restapi.PodcastEpisode e = new org.libresonic.restapi.PodcastEpisode();
private org.subsonic.restapi.PodcastEpisode createJaxbPodcastEpisode(Player player, String username, PodcastEpisode episode) {
org.subsonic.restapi.PodcastEpisode e = new org.subsonic.restapi.PodcastEpisode();
String path = episode.getPath();
if (path != null) {
MediaFile mediaFile = mediaFileService.getMediaFile(path);
e = createJaxbChild(new org.libresonic.restapi.PodcastEpisode(), player, mediaFile, username);
e = createJaxbChild(new org.subsonic.restapi.PodcastEpisode(), player, mediaFile, username);
e.setStreamId(String.valueOf(mediaFile.getId()));
}
@ -1706,7 +1714,7 @@ public class RESTController {
Bookmarks result = new Bookmarks();
for (Bookmark bookmark : bookmarkDao.getBookmarks(username)) {
org.libresonic.restapi.Bookmark b = new org.libresonic.restapi.Bookmark();
org.subsonic.restapi.Bookmark b = new org.subsonic.restapi.Bookmark();
result.getBookmark().add(b);
b.setPosition(bookmark.getPositionMillis());
b.setUsername(bookmark.getUsername());
@ -1762,7 +1770,7 @@ public class RESTController {
return;
}
org.libresonic.restapi.PlayQueue restPlayQueue = new org.libresonic.restapi.PlayQueue();
org.subsonic.restapi.PlayQueue restPlayQueue = new org.subsonic.restapi.PlayQueue();
restPlayQueue.setUsername(playQueue.getUsername());
restPlayQueue.setCurrent(playQueue.getCurrentMediaFileId());
restPlayQueue.setPosition(playQueue.getPositionMillis());
@ -1811,7 +1819,7 @@ public class RESTController {
Shares result = new Shares();
for (Share share : shareService.getSharesForUser(user)) {
org.libresonic.restapi.Share s = createJaxbShare(request, share);
org.subsonic.restapi.Share s = createJaxbShare(request, share);
result.getShare().add(s);
for (MediaFile mediaFile : shareService.getSharedFiles(share.getId(), musicFolders)) {
@ -1849,7 +1857,7 @@ public class RESTController {
shareService.updateShare(share);
Shares result = new Shares();
org.libresonic.restapi.Share s = createJaxbShare(request, share);
org.subsonic.restapi.Share s = createJaxbShare(request, share);
result.getShare().add(s);
List<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(username);
@ -1909,8 +1917,8 @@ public class RESTController {
writeEmptyResponse(request, response);
}
private org.libresonic.restapi.Share createJaxbShare(HttpServletRequest request, Share share) {
org.libresonic.restapi.Share result = new org.libresonic.restapi.Share();
private org.subsonic.restapi.Share createJaxbShare(HttpServletRequest request, Share share) {
org.subsonic.restapi.Share result = new org.subsonic.restapi.Share();
result.setId(String.valueOf(share.getId()));
result.setUrl(shareService.getShareUrl(request, share));
result.setUsername(share.getUsername());
@ -2035,10 +2043,10 @@ public class RESTController {
jaxbWriter.writeResponse(request, response, res);
}
private org.libresonic.restapi.User createJaxbUser(User user) {
private org.subsonic.restapi.User createJaxbUser(User user) {
UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
org.libresonic.restapi.User result = new org.libresonic.restapi.User();
org.subsonic.restapi.User result = new org.subsonic.restapi.User();
result.setUsername(user.getUsername());
result.setEmail(user.getEmail());
result.setScrobblingEnabled(userSettings.isLastFmEnabled());
@ -2053,6 +2061,10 @@ public class RESTController {
result.setStreamRole(user.isStreamRole());
result.setJukeboxRole(user.isJukeboxRole());
result.setShareRole(user.isShareRole());
// currently this role isn't supported by libresonic
result.setVideoConversionRole(false);
// Useless
result.setAvatarLastChanged(null);
TranscodeScheme transcodeScheme = userSettings.getTranscodeScheme();
if (transcodeScheme != null && transcodeScheme != TranscodeScheme.OFF) {
@ -2180,6 +2192,16 @@ public class RESTController {
writeEmptyResponse(request, response);
}
@RequestMapping(value = "/getChatMessages", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<String> getChatMessages(HttpServletRequest request, HttpServletResponse response) {
return ResponseEntity.status(HttpStatus.SC_GONE).body(NO_LONGER_SUPPORTED);
}
@RequestMapping(value = "/addChatMessage", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<String> addChatMessage(HttpServletRequest request, HttpServletResponse response) {
return ResponseEntity.status(HttpStatus.SC_GONE).body(NO_LONGER_SUPPORTED);
}
@RequestMapping(value = "/getLyrics", method = {RequestMethod.GET, RequestMethod.POST})
public void getLyrics(HttpServletRequest request, HttpServletResponse response) throws Exception {
request = wrapRequest(request);
@ -2218,6 +2240,87 @@ public class RESTController {
writeEmptyResponse(request, response);
}
@RequestMapping(path = "/getAlbumInfo", method = RequestMethod.GET)
public void getAlbumInfo(HttpServletRequest request, HttpServletResponse response) throws Exception {
request = wrapRequest(request);
int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
MediaFile mediaFile = this.mediaFileService.getMediaFile(id);
if (mediaFile == null) {
error(request, response, RESTController.ErrorCode.NOT_FOUND, "Media file not found.");
return;
}
AlbumNotes albumNotes = this.lastFmService.getAlbumNotes(mediaFile);
AlbumInfo result = getAlbumInfoInternal(albumNotes);
Response res = createResponse();
res.setAlbumInfo(result);
this.jaxbWriter.writeResponse(request, response, res);
}
@RequestMapping(path = "/getAlbumInfo2", method = RequestMethod.GET)
public void getAlbumInfo2(HttpServletRequest request, HttpServletResponse response) throws Exception {
request = wrapRequest(request);
int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
Album album = this.albumDao.getAlbum(id);
if (album == null) {
error(request, response, RESTController.ErrorCode.NOT_FOUND, "Album not found.");
return;
}
AlbumNotes albumNotes = this.lastFmService.getAlbumNotes(album);
AlbumInfo result = getAlbumInfoInternal(albumNotes);
Response res = createResponse();
res.setAlbumInfo(result);
this.jaxbWriter.writeResponse(request, response, res);
}
private AlbumInfo getAlbumInfoInternal(AlbumNotes albumNotes) {
AlbumInfo result = new AlbumInfo();
if (albumNotes != null)
{
result.setNotes(albumNotes.getNotes());
result.setMusicBrainzId(albumNotes.getMusicBrainzId());
result.setLastFmUrl(albumNotes.getLastFmUrl());
result.setSmallImageUrl(albumNotes.getSmallImageUrl());
result.setMediumImageUrl(albumNotes.getMediumImageUrl());
result.setLargeImageUrl(albumNotes.getLargeImageUrl());
}
return result;
}
@RequestMapping(value = "/getVideoInfo", method = RequestMethod.GET)
public ResponseEntity<String> getVideoInfo() throws Exception {
return ResponseEntity.status(HttpStatus.SC_NOT_IMPLEMENTED).body(NOT_YET_IMPLEMENTED);
}
@RequestMapping(value = "/getCaptions", method = RequestMethod.GET)
public ResponseEntity<String> getCaptions() {
return ResponseEntity.status(HttpStatus.SC_NOT_IMPLEMENTED).body(NOT_YET_IMPLEMENTED);
}
@RequestMapping(value = "/startScan", method = {RequestMethod.PUT})
public void startScan(HttpServletRequest request, HttpServletResponse response) {
request = wrapRequest(request);
mediaScannerService.scanLibrary();
getScanStatus(request, response);
}
@RequestMapping(value = "/getScanStatus", method = {RequestMethod.GET})
public void getScanStatus(HttpServletRequest request, HttpServletResponse response) {
request = wrapRequest(request);
ScanStatus scanStatus = new ScanStatus();
scanStatus.setScanning(this.mediaScannerService.isScanning());
scanStatus.setCount((long) this.mediaScannerService.getScanCount());
Response res = createResponse();
res.setScanStatus(scanStatus);
this.jaxbWriter.writeResponse(request, response, res);
}
private HttpServletRequest wrapRequest(HttpServletRequest request) {
return wrapRequest(request, false);
}

@ -133,7 +133,7 @@ public class GlobalSecurityConfig extends GlobalAuthenticationConfigurerAdapter
"/playerSettings*", "/shareSettings*", "/passwordSettings*")
.hasRole("SETTINGS")
.antMatchers("/generalSettings*", "/advancedSettings*", "/userSettings*",
"/musicFolderSettings*", "/databaseSettings*")
"/musicFolderSettings*", "/databaseSettings*", "/rest/startScan*")
.hasRole("ADMIN")
.antMatchers("/deletePlaylist*", "/savePlaylist*", "/db*")
.hasRole("PLAYLIST")

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:sub="http://libresonic.org/restapi"
targetNamespace="http://libresonic.org/restapi"
xmlns:sub="http://subsonic.org/restapi"
targetNamespace="http://subsonic.org/restapi"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
version="1.14.0">
version="1.15.0">
<xs:element name="subsonic-response" type="sub:Response"/>
@ -19,6 +19,7 @@
<xs:element name="album" type="sub:AlbumWithSongsID3" minOccurs="1" maxOccurs="1"/>
<xs:element name="song" type="sub:Child" minOccurs="1" maxOccurs="1"/>
<xs:element name="videos" type="sub:Videos" minOccurs="1" maxOccurs="1"/>
<xs:element name="videoInfo" type="sub:VideoInfo" minOccurs="1" maxOccurs="1"/>
<xs:element name="nowPlaying" type="sub:NowPlaying" minOccurs="1" maxOccurs="1"/>
<xs:element name="searchResult" type="sub:SearchResult" minOccurs="1" maxOccurs="1"/>
<xs:element name="searchResult2" type="sub:SearchResult2" minOccurs="1" maxOccurs="1"/>
@ -30,6 +31,7 @@
<xs:element name="license" type="sub:License" minOccurs="1" maxOccurs="1"/>
<xs:element name="users" type="sub:Users" minOccurs="1" maxOccurs="1"/>
<xs:element name="user" type="sub:User" minOccurs="1" maxOccurs="1"/>
<xs:element name="chatMessages" type="sub:ChatMessages" minOccurs="1" maxOccurs="1"/>
<xs:element name="albumList" type="sub:AlbumList" minOccurs="1" maxOccurs="1"/>
<xs:element name="albumList2" type="sub:AlbumList2" minOccurs="1" maxOccurs="1"/>
<xs:element name="randomSongs" type="sub:Songs" minOccurs="1" maxOccurs="1"/>
@ -43,16 +45,17 @@
<xs:element name="shares" type="sub:Shares" minOccurs="1" maxOccurs="1"/>
<xs:element name="starred" type="sub:Starred" minOccurs="1" maxOccurs="1"/>
<xs:element name="starred2" type="sub:Starred2" minOccurs="1" maxOccurs="1"/>
<xs:element name="albumInfo" type="sub:AlbumInfo" minOccurs="1" maxOccurs="1"/>
<xs:element name="artistInfo" type="sub:ArtistInfo" minOccurs="1" maxOccurs="1"/>
<xs:element name="artistInfo2" type="sub:ArtistInfo2" minOccurs="1" maxOccurs="1"/>
<xs:element name="similarSongs" type="sub:SimilarSongs" minOccurs="1" maxOccurs="1"/>
<xs:element name="similarSongs2" type="sub:SimilarSongs2" minOccurs="1" maxOccurs="1"/>
<xs:element name="topSongs" type="sub:TopSongs" minOccurs="1" maxOccurs="1"/>
<xs:element name="scanStatus" type="sub:ScanStatus" minOccurs="1" maxOccurs="1"/>
<xs:element name="error" type="sub:Error" minOccurs="1" maxOccurs="1"/>
</xs:choice>
<xs:attribute name="status" type="sub:ResponseStatus" use="required"/>
<xs:attribute name="version" type="sub:Version" use="required"/>
<xs:attribute name="type" type="xs:string" use="required" />
</xs:complexType>
<xs:simpleType name="ResponseStatus">
@ -155,6 +158,7 @@
<xs:attribute name="coverArt" type="xs:string" use="optional"/>
<xs:attribute name="songCount" type="xs:int" use="required"/>
<xs:attribute name="duration" type="xs:int" use="required"/>
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
<xs:attribute name="created" type="xs:dateTime" use="required"/>
<xs:attribute name="starred" type="xs:dateTime" use="optional"/>
<xs:attribute name="year" type="xs:int" use="optional"/> <!-- Added in 1.10.1 -->
@ -177,6 +181,32 @@
</xs:sequence>
</xs:complexType>
<xs:complexType name="VideoInfo">
<xs:sequence>
<xs:element name="captions" type="sub:Captions" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="audioTrack" type="sub:AudioTrack" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="conversion" type="sub:VideoConversion" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="Captions">
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="name" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="AudioTrack">
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="name" type="xs:string" use="optional"/>
<xs:attribute name="languageCode" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="VideoConversion">
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="bitRate" type="xs:int" use="optional"/> <!-- In Kbps -->
<xs:attribute name="audioTrackId" type="xs:int" use="optional"/>
</xs:complexType>
<xs:complexType name="Directory">
<xs:sequence>
<xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
@ -187,6 +217,7 @@
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.10.1 -->
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.13.0 -->
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.13.0 -->
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
</xs:complexType>
<xs:complexType name="Child">
@ -211,6 +242,7 @@
<xs:attribute name="isVideo" type="xs:boolean" use="optional"/> <!-- Added in 1.4.1 -->
<xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.6.0 -->
<xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.6.0 -->
<xs:attribute name="playCount" type="xs:long" use="optional"/> <!-- Added in 1.14.0 -->
<xs:attribute name="discNumber" type="xs:int" use="optional"/> <!-- Added in 1.8.0 -->
<xs:attribute name="created" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
<xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
@ -336,6 +368,18 @@
</xs:complexContent>
</xs:complexType>
<xs:complexType name="ChatMessages">
<xs:sequence>
<xs:element name="chatMessage" type="sub:ChatMessage" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ChatMessage">
<xs:attribute name="username" type="xs:string" use="required"/>
<xs:attribute name="time" type="xs:long" use="required"/>
<xs:attribute name="message" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="AlbumList">
<xs:sequence>
<xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
@ -477,6 +521,17 @@
</xs:sequence>
</xs:complexType>
<xs:complexType name="AlbumInfo">
<xs:sequence>
<xs:element name="notes" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="musicBrainzId" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="lastFmUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="smallImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="mediumImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="largeImageUrl" type="xs:string" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ArtistInfoBase">
<xs:sequence>
<xs:element name="biography" type="xs:string" minOccurs="0" maxOccurs="1"/>
@ -541,6 +596,11 @@
<xs:attribute name="trialExpires" type="xs:dateTime" use="optional"/>
</xs:complexType>
<xs:complexType name="ScanStatus">
<xs:attribute name="scanning" type="xs:boolean" use="required"/>
<xs:attribute name="count" type="xs:long" use="optional"/>
</xs:complexType>
<xs:complexType name="Users">
<xs:sequence>
<xs:element name="user" type="sub:User" minOccurs="0" maxOccurs="unbounded"/>
@ -566,6 +626,8 @@
<xs:attribute name="streamRole" type="xs:boolean" use="required"/>
<xs:attribute name="jukeboxRole" type="xs:boolean" use="required"/>
<xs:attribute name="shareRole" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
<xs:attribute name="videoConversionRole" type="xs:boolean" use="required"/> <!-- Added in 1.14.0 -->
<xs:attribute name="avatarLastChanged" type="xs:dateTime" use="optional"/> <!-- Added in 1.14.0 -->
</xs:complexType>
<xs:complexType name="Error">

Loading…
Cancel
Save