package org.dyndns.jkiddo.service.daap.server;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.StringTokenizer;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.dyndns.jkiddo.NotImplementedException;
import org.dyndns.jkiddo.dmap.chunks.audio.AlbumSearchContainer;
import org.dyndns.jkiddo.dmap.chunks.audio.ArtistSearchContainer;
import org.dyndns.jkiddo.dmap.chunks.audio.AudioProtocolVersion;
import org.dyndns.jkiddo.dmap.chunks.audio.DatabaseItems;
import org.dyndns.jkiddo.dmap.chunks.audio.SupportsExtraData;
import org.dyndns.jkiddo.dmap.chunks.audio.SupportsGroups;
import org.dyndns.jkiddo.dmap.chunks.audio.extension.MusicSharingVersion;
import org.dyndns.jkiddo.dmap.chunks.audio.extension.UnknownMQ;
import org.dyndns.jkiddo.dmap.chunks.audio.extension.UnknownSL;
import org.dyndns.jkiddo.dmap.chunks.audio.extension.UnknownSR;
import org.dyndns.jkiddo.dmap.chunks.audio.extension.UnknownTr;
import org.dyndns.jkiddo.dmp.chunks.VersionChunk;
import org.dyndns.jkiddo.dmp.chunks.media.AuthenticationMethod;
import org.dyndns.jkiddo.dmp.chunks.media.AuthenticationMethod.PasswordMethod;
import org.dyndns.jkiddo.dmp.chunks.media.DatabaseCount;
import org.dyndns.jkiddo.dmp.chunks.media.ItemName;
import org.dyndns.jkiddo.dmp.chunks.media.Listing;
import org.dyndns.jkiddo.dmp.chunks.media.LoginRequired;
import org.dyndns.jkiddo.dmp.chunks.media.MediaProtocolVersion;
import org.dyndns.jkiddo.dmp.chunks.media.ReturnedCount;
import org.dyndns.jkiddo.dmp.chunks.media.ServerInfoResponse;
import org.dyndns.jkiddo.dmp.chunks.media.SpecifiedTotalCount;
import org.dyndns.jkiddo.dmp.chunks.media.Status;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsAutoLogout;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsBrowse;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsExtensions;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsIndex;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsPersistentIds;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsPlaylistEdit;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsQuery;
import org.dyndns.jkiddo.dmp.chunks.media.SupportsUpdate;
import org.dyndns.jkiddo.dmp.chunks.media.TimeoutInterval;
import org.dyndns.jkiddo.dmp.chunks.media.UpdateType;
import org.dyndns.jkiddo.dmp.util.DmapUtil;
import org.dyndns.jkiddo.dpap.chunks.picture.PictureProtocolVersion;
import org.dyndns.jkiddo.service.dmap.DMAPResource;
import org.dyndns.jkiddo.service.dmap.IItemManager;
import org.dyndns.jkiddo.service.dmap.Util;
import org.dyndns.jkiddo.zeroconf.IZeroconfManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
@Consumes(MediaType.WILDCARD)
// @Produces(MediaType.WILDCARD)
public class DAAPResource extends DMAPResource<IItemManager> implements IMusicLibrary {
private static final Logger LOGGER = LoggerFactory.getLogger(DAAPResource.class);
public static final String DAAP_PORT_NAME = "DAAP_PORT_NAME";
public static final String DAAP_RESOURCE = "DAAP_IMPLEMENTATION";
private static final String MEDIA_KINDS_SHARED_KEY = "Media Kinds Shared";
private static final String PASSWORD_KEY = "Password";
private static final VersionChunk pictureProtocolVersion = new PictureProtocolVersion(DmapUtil.PPRO_VERSION_201);
private static final VersionChunk audioProtocolVersion = new AudioProtocolVersion(DmapUtil.APRO_VERSION_3012);
private static final VersionChunk mediaProtocolVersion = new MediaProtocolVersion(DmapUtil.MPRO_VERSION_2010);
private static final MusicSharingVersion musicSharingVersion = new MusicSharingVersion(
DmapUtil.MUSIC_SHARING_VERSION_3012);
protected final String serviceGuid;
@Inject
public DAAPResource(final IZeroconfManager mDNS, @Named(DAAP_PORT_NAME) final Integer port,
@Named(Util.APPLICATION_NAME) final String applicationName,
@Named(DAAPResource.DAAP_RESOURCE) final IItemManager itemManager) throws IOException {
super(mDNS, port, itemManager);
this.name = applicationName;
this.serviceGuid = Util.toServiceGuid(applicationName);
this.register();
}
@Override
protected IZeroconfManager.ServiceInfo getServiceInfoToRegister() {
final HashMap<String, String> records = new HashMap<>();
records.put(MACHINE_NAME_KEY, name);
records.put("OSsi", "0x15F");
records.put(MEDIA_KINDS_SHARED_KEY, "1");
records.put(TXT_VERSION_KEY, TXT_VERSION);
records.put(MACHINE_ID_KEY, MACHINE_ID);
records.put(VERSION_KEY, audioProtocolVersion.getValue() + "");
records.put(ITSH_VERSION_KEY, musicSharingVersion.getValue() + "");
records.put("MID", MID_0X);
records.put("dmv", mediaProtocolVersion.getValue() + "");
records.put(DATABASE_ID_KEY, DATABASE_ID);
if (PasswordMethod.NO_PASSWORD == itemManager.getAuthenticationMethod()) {
records.put(PASSWORD_KEY, "0");
return new IZeroconfManager.ServiceInfo(DAAP_SERVICE_TYPE, name, port, records);
}
records.put(PASSWORD_KEY, "1");
return new IZeroconfManager.ServiceInfo(DAAP_SERVICE_TYPE, name + "_PW", port, records);
}
@Override
@Path("server-info")
@GET
public Response serverInfo(@QueryParam("hsgid") final String hsgid) throws IOException, SQLException {
final ServerInfoResponse serverInfoResponse = new ServerInfoResponse();
serverInfoResponse.add(new Status(200));
serverInfoResponse.add(mediaProtocolVersion);
serverInfoResponse.add(new ItemName(name));
serverInfoResponse.add(audioProtocolVersion);
serverInfoResponse.add(musicSharingVersion);
serverInfoResponse.add(new SupportsExtraData(7));
serverInfoResponse.add(new SupportsGroups(SupportsGroups.NO_GROUPS));
serverInfoResponse.add(new UnknownMQ(true));
serverInfoResponse.add(new UnknownTr(true));
serverInfoResponse.add(new UnknownSL(true));
serverInfoResponse.add(new UnknownSR(true));
serverInfoResponse.add(new SupportsPlaylistEdit(false));
serverInfoResponse.add(new LoginRequired(true));
serverInfoResponse.add(new TimeoutInterval(1800));
serverInfoResponse.add(new SupportsAutoLogout(true));
final PasswordMethod authenticationMethod = itemManager.getAuthenticationMethod();
if (authenticationMethod == PasswordMethod.NO_PASSWORD) {
serverInfoResponse.add(new AuthenticationMethod(AuthenticationMethod.PasswordMethod.NO_PASSWORD));
} else {
if (authenticationMethod == PasswordMethod.PASSWORD) {
serverInfoResponse.add(new AuthenticationMethod(PasswordMethod.PASSWORD));
} else {
serverInfoResponse.add(new AuthenticationMethod(PasswordMethod.USERNAME_AND_PASSWORD));
}
// serverInfoResponse.add(new
// AuthenticationSchemes(AuthenticationSchemes.BASIC_SCHEME |
// AuthenticationSchemes.DIGEST_SCHEME));
}
serverInfoResponse.add(new SupportsUpdate(true));
serverInfoResponse.add(new SupportsPersistentIds(true));
serverInfoResponse.add(new SupportsExtensions(true));
serverInfoResponse.add(new SupportsBrowse(true));
serverInfoResponse.add(new SupportsQuery(true));
serverInfoResponse.add(new SupportsIndex(true));
// serverInfoResponse.add(new UnknownSE(0x78040061));
// serverInfoResponse.add(new UnknownCU(0x3F));
// serverInfoResponse.add(new UnknownFR(0x64));
// serverInfoResponse.add(new
// SupportsFairPlay(SupportsFairPlay.UNKNOWN_VALUE));//iTunes 12.3.0.44
// serverInfoResponse.add(new UnknownSX(0x26f));
// serverInfoResponse.add(pictureProtocolVersion);
// final SpeakerMachineList ml = new SpeakerMachineList();
// ml.add(new MachineAddress(new byte[] { (byte) 0x86, (byte) 0x83,
// 0x3a, 0x56, (byte) 0xe8, (byte) 0xb8 }));
// ml.add(new MachineAddress(new byte[] { 0x40, (byte) 0xf9, (byte)
// 0xe3, 0x00, 0x00, 0x72 }));
// ml.add(new MachineAddress(new byte[] { 0x41, (byte) 0xf9, (byte)
// 0xe3, 0x00, 0x00, 0x72 }));
// serverInfoResponse.add(ml);
//
// Util.getHardwareAddress();
//
// serverInfoResponse.add(new SupportsResolve(true));
serverInfoResponse.add(new DatabaseCount(itemManager.getDatabases().size()));
// serverInfoResponse.add(new
// UTCTime(Calendar.getInstance().getTimeInMillis() / 1000));
// serverInfoResponse.add(new UTCTimeOffset(7200));
return Util.buildResponse(serverInfoResponse, getDMAPKey(), name);
}
@Override
@Path("/databases/{databaseId}/items/{itemId}.{format}")
@GET
public Response item(@PathParam("databaseId") final long databaseId, @PathParam("itemId") final long itemId,
@PathParam("format") final String format, @HeaderParam("Range") final String rangeHeader)
throws IOException {
final byte[] array = itemManager.getItemAsByteArray(databaseId, itemId);
final long[] range = getRange(rangeHeader, 0, array.length);
final int pos = (int) range[0];
final int end = (int) range[1];
return Util.buildAudioResponse(Arrays.copyOfRange(array, pos, end), pos, getDMAPKey(), name);
}
static private long[] getRange(final String rangeHeader, long position, long end) {
if (!Strings.isNullOrEmpty(rangeHeader)) {
final StringTokenizer token = new StringTokenizer(rangeHeader, "=");
final String key = token.nextToken().trim();
if (("bytes").equals(key) == false) {
throw new NullPointerException("Format of range header is unknown");
}
final StringTokenizer rangesToken = new StringTokenizer(token.nextToken(), "-");
position = Long.parseLong(rangesToken.nextToken().trim());
if (rangesToken.hasMoreTokens())
end = Long.parseLong(rangesToken.nextToken().trim());
}
return new long[] { position, end };
}
@Override
@Path("databases/{databaseId}/items")
@GET
public Response items(@PathParam("databaseId") final long databaseId,
@QueryParam("session-id") final long sessionId, @QueryParam("revision-number") final long revisionNumber,
@QueryParam("delta") final long delta, @QueryParam("type") final String type,
@QueryParam("meta") final String meta, @QueryParam("query") final String query,
@QueryParam("hsgid") final String hsgid) throws IOException, SQLException {
// dpap: limited by query
// http://192.168.1.2dpap://192.168.1.2:8770/databases/1/items?session-id=1101478641&meta=dpap.thumb,dmap.itemid,dpap.filedata&query=('dmap.itemid:2810','dmap.itemid:2811','dmap.itemid:2812','dmap.itemid:2813','dmap.itemid:2814','dmap.itemid:2815','dmap.itemid:2816','dmap.itemid:2817','dmap.itemid:2818','dmap.itemid:2819','dmap.itemid:2820','dmap.itemid:2821','dmap.itemid:2822','dmap.itemid:2823','dmap.itemid:2824','dmap.itemid:2825','dmap.itemid:2826','dmap.itemid:2827','dmap.itemid:2851','dmap.itemid:2852')
// GET
// dpap://192.168.1.2:8770/databases/1/items?session-id=1101478641&meta=dpap.hires,dmap.itemid,dpap.filedata&query=('dmap.itemid:2742')
// HTTP/1.1
// .getDatabases(), new Predicate<Database>() {
// @Override
// public boolean apply(Database database)
// {
// return database.getItemId() == databaseId;
// }
// }).getItems();
final Iterable<String> parameters = DmapUtil.parseMeta(meta);
final DatabaseItems databaseSongs = new DatabaseItems();
databaseSongs.add(new Status(200));
databaseSongs.add(new UpdateType(0));
final Listing listing = itemManager.getMediaItems(databaseId, parameters);
databaseSongs.add(new SpecifiedTotalCount(Iterables.size(listing.getListingItems())));
databaseSongs.add(new ReturnedCount(Iterables.size(listing.getListingItems())));
/*
* for(MediaItem item : items) { ListingItem listingItem = new
* ListingItem(); if("all".equals(meta)) {
* listingItem.add(item.getChunk("dmap.itemkind")); for(Chunk chunk :
* item.getChunks()) { if(chunk.getName().equals("dmap.itemkind"))
* continue; listingItem.add(chunk); } } else { for(String key :
* parameters) { Chunk chunk = item.getChunk(key); if(chunk != null) {
* listingItem.add(chunk); } else { logger.info("Unknown chunk type: " +
* key); } } } listing.add(listingItem); }
*/
databaseSongs.add(listing);
// if(request.isUpdateType() && deletedSongs != null)
// {
// DeletedIdListing deletedListing = new DeletedIdListing();
//
// for(Song song : deletedSongs)
// {
// deletedListing.add(song.getChunk("dmap.itemid"));
// }
//
// databaseSongs.add(deletedListing);
// }
return Util.buildResponse(databaseSongs, getDMAPKey(), name);
}
@Override
// @Path("databases/{databaseId}/groups/{groupdId}/extra_data/artwork")
@Path("databases/{databaseId}/items/{groupdId}/extra_data/artwork")
@GET
public Response artwork(@PathParam("databaseId") final long databaseId, @PathParam("groupId") final long groupId,
@QueryParam("session-id") final long sessionId, @QueryParam("mw") final String mw,
@QueryParam("mh") final String mh, @QueryParam("group-type") final String group_type,
@QueryParam("daapSecInfo") final String daapSecInfo) throws IOException {
throw new NotImplementedException();
}
@Override
@Path("databases/{databaseId}/groups")
@GET
public Response groups(@PathParam("databaseId") final long databaseId, @QueryParam("meta") final String meta,
@QueryParam("type") final String type, @QueryParam("group-type") final String groupType,
@QueryParam("sort") final String sort, @QueryParam("include-sort-headers") final long includeSortHeaders,
@QueryParam("query") final String query, @QueryParam("session-id") final long sessionId,
@QueryParam("hsgid") final String hsgid) throws IOException, SQLException {
final Iterable<String> parameters = DmapUtil.parseMeta(meta);
if ("artists".equalsIgnoreCase(groupType)) {
final ArtistSearchContainer response = new ArtistSearchContainer();
response.add(new Status(200));
response.add(new UpdateType(0));
// response.add(new SpecifiedTotalCount(0));//
// response.add(new ReturnedCount(0));//
// final Listing listing = new Listing();
final Listing listing = itemManager.getMediaItems(databaseId, parameters);
response.add(new SpecifiedTotalCount(Iterables.size(listing.getListingItems())));
response.add(new ReturnedCount(Iterables.size(listing.getListingItems())));
// listing.add(new SortingHeaderListing());//
response.add(listing);
return Util.buildResponse(response, getDMAPKey(), name);
} else if ("albums".equalsIgnoreCase(groupType)) {
final AlbumSearchContainer response = new AlbumSearchContainer();
response.add(new Status(200));
response.add(new UpdateType(0));
final Listing listing = itemManager.getMediaItems(databaseId, parameters);
response.add(new SpecifiedTotalCount(Iterables.size(listing.getListingItems())));
response.add(new ReturnedCount(Iterables.size(listing.getListingItems())));
// final Listing listing = new Listing();
response.add(listing);
return Util.buildResponse(response, getDMAPKey(), name);
} else
throw new NotImplementedException();
}
@Override
public String getDMAPKey() {
return "DAAP-Server";
}
}