mirror of
https://git.code.sf.net/p/minidlna/git
synced 2025-03-30 04:08:05 +00:00
2321 lines
69 KiB
C
2321 lines
69 KiB
C
/* MiniDLNA project
|
|
*
|
|
* http://sourceforge.net/projects/minidlna/
|
|
*
|
|
* MiniDLNA media server
|
|
* Copyright (C) 2008-2017 Justin Maggard
|
|
*
|
|
* This file is part of MiniDLNA.
|
|
*
|
|
* MiniDLNA is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* MiniDLNA 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 MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Portions of the code from the MiniUPnP project:
|
|
*
|
|
* Copyright (c) 2006-2007, Thomas Bernard
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * The name of the author may not be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <netdb.h>
|
|
#include <ctype.h>
|
|
|
|
#include "event.h"
|
|
#include "upnpglobalvars.h"
|
|
#include "utils.h"
|
|
#include "upnphttp.h"
|
|
#include "upnpsoap.h"
|
|
#include "containers.h"
|
|
#include "upnpreplyparse.h"
|
|
#include "getifaddr.h"
|
|
#include "scanner.h"
|
|
#include "sql.h"
|
|
#include "log.h"
|
|
|
|
#ifdef __sparc__ /* Sorting takes too long on slow processors with very large containers */
|
|
# define __SORT_LIMIT if( totalMatches < 10000 )
|
|
#else
|
|
# define __SORT_LIMIT
|
|
#endif
|
|
#define NON_ZERO(x) (x && atoi(x))
|
|
#define IS_ZERO(x) (!x || !atoi(x))
|
|
|
|
/* Standard Errors:
|
|
*
|
|
* errorCode errorDescription Description
|
|
* -------- ---------------- -----------
|
|
* 401 Invalid Action No action by that name at this service.
|
|
* 402 Invalid Args Could be any of the following: not enough in args,
|
|
* too many in args, no in arg by that name,
|
|
* one or more in args are of the wrong data type.
|
|
* 403 Out of Sync Out of synchronization.
|
|
* 501 Action Failed May be returned in current state of service
|
|
* prevents invoking that action.
|
|
* 600-699 TBD Common action errors. Defined by UPnP Forum
|
|
* Technical Committee.
|
|
* 700-799 TBD Action-specific errors for standard actions.
|
|
* Defined by UPnP Forum working committee.
|
|
* 800-899 TBD Action-specific errors for non-standard actions.
|
|
* Defined by UPnP vendor.
|
|
*/
|
|
#define SoapError(x,y,z) _SoapError(x,y,z,__func__)
|
|
static void
|
|
_SoapError(struct upnphttp * h, int errCode, const char * errDesc, const char *func)
|
|
{
|
|
static const char resp[] =
|
|
"<s:Envelope "
|
|
"xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
|
|
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
|
"<s:Body>"
|
|
"<s:Fault>"
|
|
"<faultcode>s:Client</faultcode>"
|
|
"<faultstring>UPnPError</faultstring>"
|
|
"<detail>"
|
|
"<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\">"
|
|
"<errorCode>%d</errorCode>"
|
|
"<errorDescription>%s</errorDescription>"
|
|
"</UPnPError>"
|
|
"</detail>"
|
|
"</s:Fault>"
|
|
"</s:Body>"
|
|
"</s:Envelope>";
|
|
|
|
char body[2048];
|
|
int bodylen;
|
|
|
|
DPRINTF(E_WARN, L_HTTP, "%s Returning UPnPError %d: %s\n", func, errCode, errDesc);
|
|
bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc);
|
|
BuildResp2_upnphttp(h, 500, "Internal Server Error", body, bodylen);
|
|
SendResp_upnphttp(h);
|
|
CloseSocket_upnphttp(h);
|
|
}
|
|
|
|
static void
|
|
BuildSendAndCloseSoapResp(struct upnphttp * h,
|
|
const char * body, int bodylen)
|
|
{
|
|
static const char beforebody[] =
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
|
|
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
|
|
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
|
"<s:Body>";
|
|
|
|
static const char afterbody[] =
|
|
"</s:Body>"
|
|
"</s:Envelope>\r\n";
|
|
|
|
if (!body || bodylen < 0)
|
|
{
|
|
Send500(h);
|
|
return;
|
|
}
|
|
|
|
BuildHeader_upnphttp(h, 200, "OK", sizeof(beforebody) - 1
|
|
+ sizeof(afterbody) - 1 + bodylen );
|
|
|
|
memcpy(h->res_buf + h->res_buflen, beforebody, sizeof(beforebody) - 1);
|
|
h->res_buflen += sizeof(beforebody) - 1;
|
|
|
|
memcpy(h->res_buf + h->res_buflen, body, bodylen);
|
|
h->res_buflen += bodylen;
|
|
|
|
memcpy(h->res_buf + h->res_buflen, afterbody, sizeof(afterbody) - 1);
|
|
h->res_buflen += sizeof(afterbody) - 1;
|
|
|
|
SendResp_upnphttp(h);
|
|
CloseSocket_upnphttp(h);
|
|
}
|
|
|
|
static void
|
|
GetSystemUpdateID(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<Id>%d</Id>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
int bodylen;
|
|
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:schemas-upnp-org:service:ContentDirectory:1",
|
|
updateID, action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
|
|
static void
|
|
IsAuthorizedValidated(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<Result>%d</Result>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
struct NameValueParserData data;
|
|
const char * id;
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, XML_STORE_EMPTY_FL);
|
|
id = GetValueFromNameValueList(&data, "DeviceID");
|
|
if(id)
|
|
{
|
|
int bodylen;
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
|
|
1, action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
else
|
|
SoapError(h, 402, "Invalid Args");
|
|
|
|
ClearNameValueList(&data);
|
|
}
|
|
|
|
static void
|
|
RegisterDevice(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<RegistrationRespMsg>%s</RegistrationRespMsg>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
int bodylen;
|
|
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
|
|
uuidvalue, action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
|
|
static void
|
|
GetProtocolInfo(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<Source>"
|
|
RESOURCE_PROTOCOL_INFO_VALUES
|
|
"</Source>"
|
|
"<Sink></Sink>"
|
|
"</u:%sResponse>";
|
|
|
|
char * body;
|
|
int bodylen;
|
|
|
|
bodylen = asprintf(&body, resp,
|
|
action, "urn:schemas-upnp-org:service:ConnectionManager:1",
|
|
action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
free(body);
|
|
}
|
|
|
|
static void
|
|
GetSortCapabilities(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<SortCaps>"
|
|
"dc:title,"
|
|
"dc:date,"
|
|
"upnp:class,"
|
|
"upnp:album,"
|
|
"upnp:episodeNumber,"
|
|
"upnp:originalTrackNumber"
|
|
"</SortCaps>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
int bodylen;
|
|
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:schemas-upnp-org:service:ContentDirectory:1",
|
|
action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
|
|
static void
|
|
GetSearchCapabilities(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse xmlns:u=\"%s\">"
|
|
"<SearchCaps>"
|
|
"dc:creator,"
|
|
"dc:date,"
|
|
"dc:title,"
|
|
"upnp:album,"
|
|
"upnp:actor,"
|
|
"upnp:artist,"
|
|
"upnp:class,"
|
|
"upnp:genre,"
|
|
"@id,"
|
|
"@parentID,"
|
|
"@refID"
|
|
"</SearchCaps>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
int bodylen;
|
|
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:schemas-upnp-org:service:ContentDirectory:1",
|
|
action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
|
|
static void
|
|
GetCurrentConnectionIDs(struct upnphttp * h, const char * action)
|
|
{
|
|
/* TODO: Use real data. - JM */
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<ConnectionIDs>0</ConnectionIDs>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
int bodylen;
|
|
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:schemas-upnp-org:service:ConnectionManager:1",
|
|
action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
|
|
static void
|
|
GetCurrentConnectionInfo(struct upnphttp * h, const char * action)
|
|
{
|
|
/* TODO: Use real data. - JM */
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<RcsID>-1</RcsID>"
|
|
"<AVTransportID>-1</AVTransportID>"
|
|
"<ProtocolInfo></ProtocolInfo>"
|
|
"<PeerConnectionManager></PeerConnectionManager>"
|
|
"<PeerConnectionID>-1</PeerConnectionID>"
|
|
"<Direction>Output</Direction>"
|
|
"<Status>Unknown</Status>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[sizeof(resp)+128];
|
|
struct NameValueParserData data;
|
|
const char *id_str;
|
|
int id;
|
|
char *endptr = NULL;
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, XML_STORE_EMPTY_FL);
|
|
id_str = GetValueFromNameValueList(&data, "ConnectionID");
|
|
DPRINTF(E_INFO, L_HTTP, "GetCurrentConnectionInfo(%s)\n", id_str);
|
|
if(id_str)
|
|
id = strtol(id_str, &endptr, 10);
|
|
if (!id_str || endptr == id_str)
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
}
|
|
else if(id != 0)
|
|
{
|
|
SoapError(h, 701, "No such object error");
|
|
}
|
|
else
|
|
{
|
|
int bodylen;
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:schemas-upnp-org:service:ConnectionManager:1",
|
|
action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
ClearNameValueList(&data);
|
|
}
|
|
|
|
/* Standard DLNA/UPnP filter flags */
|
|
#define FILTER_CHILDCOUNT 0x00000001
|
|
#define FILTER_DC_CREATOR 0x00000002
|
|
#define FILTER_DC_DATE 0x00000004
|
|
#define FILTER_DC_DESCRIPTION 0x00000008
|
|
#define FILTER_DLNA_NAMESPACE 0x00000010
|
|
#define FILTER_REFID 0x00000020
|
|
#define FILTER_RES 0x00000040
|
|
#define FILTER_RES_BITRATE 0x00000080
|
|
#define FILTER_RES_DURATION 0x00000100
|
|
#define FILTER_RES_NRAUDIOCHANNELS 0x00000200
|
|
#define FILTER_RES_RESOLUTION 0x00000400
|
|
#define FILTER_RES_SAMPLEFREQUENCY 0x00000800
|
|
#define FILTER_RES_SIZE 0x00001000
|
|
#define FILTER_SEARCHABLE 0x00002000
|
|
#define FILTER_UPNP_ACTOR 0x00004000
|
|
#define FILTER_UPNP_ALBUM 0x00008000
|
|
#define FILTER_UPNP_ALBUMARTURI 0x00010000
|
|
#define FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID 0x00020000
|
|
#define FILTER_UPNP_ARTIST 0x00040000
|
|
#define FILTER_UPNP_EPISODENUMBER 0x00080000
|
|
#define FILTER_UPNP_EPISODESEASON 0x00100000
|
|
#define FILTER_UPNP_GENRE 0x00200000
|
|
#define FILTER_UPNP_ORIGINALTRACKNUMBER 0x00400000
|
|
#define FILTER_UPNP_SEARCHCLASS 0x00800000
|
|
#define FILTER_UPNP_STORAGEUSED 0x01000000
|
|
/* Not normally used, so leave out of the default filter */
|
|
#define FILTER_UPNP_PLAYBACKCOUNT 0x02000000
|
|
#define FILTER_UPNP_LASTPLAYBACKPOSITION 0x04000000
|
|
/* Vendor-specific filter flags */
|
|
#define FILTER_SEC_CAPTION_INFO_EX 0x08000000
|
|
#define FILTER_SEC_DCM_INFO 0x10000000
|
|
#define FILTER_SEC 0x18000000
|
|
#define FILTER_PV_SUBTITLE_FILE_TYPE 0x20000000
|
|
#define FILTER_PV_SUBTITLE_FILE_URI 0x40000000
|
|
#define FILTER_PV_SUBTITLE 0x60000000
|
|
#define FILTER_AV_MEDIA_CLASS 0x80000000
|
|
/* Masks */
|
|
#define STANDARD_FILTER_MASK 0x01FFFFFF
|
|
#define FILTER_BOOKMARK_MASK (FILTER_UPNP_PLAYBACKCOUNT | \
|
|
FILTER_UPNP_LASTPLAYBACKPOSITION | \
|
|
FILTER_SEC_DCM_INFO)
|
|
|
|
static uint32_t
|
|
set_filter_flags(char *filter, struct upnphttp *h)
|
|
{
|
|
char *item, *saveptr = NULL;
|
|
uint32_t flags = 0;
|
|
int samsung = h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG);
|
|
|
|
if( !filter || (strlen(filter) <= 1) ) {
|
|
/* Not the full 32 bits. Skip vendor-specific stuff by default. */
|
|
flags = STANDARD_FILTER_MASK;
|
|
if (samsung)
|
|
flags |= FILTER_SEC_CAPTION_INFO_EX | FILTER_SEC_DCM_INFO;
|
|
}
|
|
if (flags)
|
|
return flags;
|
|
|
|
if( samsung )
|
|
flags |= FILTER_DLNA_NAMESPACE;
|
|
item = strtok_r(filter, ",", &saveptr);
|
|
while( item != NULL )
|
|
{
|
|
if( saveptr )
|
|
*(item-1) = ',';
|
|
while( isspace(*item) )
|
|
item++;
|
|
if( strcmp(item, "@childCount") == 0 )
|
|
{
|
|
flags |= FILTER_CHILDCOUNT;
|
|
}
|
|
else if( strcmp(item, "@searchable") == 0 )
|
|
{
|
|
flags |= FILTER_SEARCHABLE;
|
|
}
|
|
else if( strcmp(item, "dc:creator") == 0 )
|
|
{
|
|
flags |= FILTER_DC_CREATOR;
|
|
}
|
|
else if( strcmp(item, "dc:date") == 0 )
|
|
{
|
|
flags |= FILTER_DC_DATE;
|
|
}
|
|
else if( strcmp(item, "dc:description") == 0 )
|
|
{
|
|
flags |= FILTER_DC_DESCRIPTION;
|
|
}
|
|
else if( strcmp(item, "dlna") == 0 )
|
|
{
|
|
flags |= FILTER_DLNA_NAMESPACE;
|
|
}
|
|
else if( strcmp(item, "@refID") == 0 )
|
|
{
|
|
flags |= FILTER_REFID;
|
|
}
|
|
else if( strcmp(item, "upnp:album") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_ALBUM;
|
|
}
|
|
else if( strcmp(item, "upnp:albumArtURI") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_ALBUMARTURI;
|
|
if( samsung )
|
|
flags |= FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID;
|
|
}
|
|
else if( strcmp(item, "upnp:albumArtURI@dlna:profileID") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_ALBUMARTURI;
|
|
flags |= FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID;
|
|
}
|
|
else if( strcmp(item, "upnp:artist") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_ARTIST;
|
|
}
|
|
else if( strcmp(item, "upnp:actor") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_ACTOR;
|
|
}
|
|
else if( strcmp(item, "upnp:genre") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_GENRE;
|
|
}
|
|
else if( strcmp(item, "upnp:originalTrackNumber") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_ORIGINALTRACKNUMBER;
|
|
}
|
|
else if( strcmp(item, "upnp:searchClass") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_SEARCHCLASS;
|
|
}
|
|
else if( strcmp(item, "upnp:storageUsed") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_STORAGEUSED;
|
|
}
|
|
else if( strcmp(item, "res") == 0 )
|
|
{
|
|
flags |= FILTER_RES;
|
|
}
|
|
else if( (strcmp(item, "res@bitrate") == 0) ||
|
|
(strcmp(item, "@bitrate") == 0) ||
|
|
((strcmp(item, "bitrate") == 0) && (flags & FILTER_RES)) )
|
|
{
|
|
flags |= FILTER_RES;
|
|
flags |= FILTER_RES_BITRATE;
|
|
}
|
|
else if( (strcmp(item, "res@duration") == 0) ||
|
|
(strcmp(item, "@duration") == 0) ||
|
|
((strcmp(item, "duration") == 0) && (flags & FILTER_RES)) )
|
|
{
|
|
flags |= FILTER_RES;
|
|
flags |= FILTER_RES_DURATION;
|
|
}
|
|
else if( (strcmp(item, "res@nrAudioChannels") == 0) ||
|
|
(strcmp(item, "@nrAudioChannels") == 0) ||
|
|
((strcmp(item, "nrAudioChannels") == 0) && (flags & FILTER_RES)) )
|
|
{
|
|
flags |= FILTER_RES;
|
|
flags |= FILTER_RES_NRAUDIOCHANNELS;
|
|
}
|
|
else if( (strcmp(item, "res@resolution") == 0) ||
|
|
(strcmp(item, "@resolution") == 0) ||
|
|
((strcmp(item, "resolution") == 0) && (flags & FILTER_RES)) )
|
|
{
|
|
flags |= FILTER_RES;
|
|
flags |= FILTER_RES_RESOLUTION;
|
|
}
|
|
else if( (strcmp(item, "res@sampleFrequency") == 0) ||
|
|
(strcmp(item, "@sampleFrequency") == 0) ||
|
|
((strcmp(item, "sampleFrequency") == 0) && (flags & FILTER_RES)) )
|
|
{
|
|
flags |= FILTER_RES;
|
|
flags |= FILTER_RES_SAMPLEFREQUENCY;
|
|
}
|
|
else if( (strcmp(item, "res@size") == 0) ||
|
|
(strcmp(item, "@size") == 0) ||
|
|
(strcmp(item, "size") == 0) )
|
|
{
|
|
flags |= FILTER_RES;
|
|
flags |= FILTER_RES_SIZE;
|
|
}
|
|
else if( strcmp(item, "upnp:playbackCount") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_PLAYBACKCOUNT;
|
|
}
|
|
else if( strcmp(item, "upnp:lastPlaybackPosition") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_LASTPLAYBACKPOSITION;
|
|
}
|
|
else if( strcmp(item, "sec:CaptionInfoEx") == 0 )
|
|
{
|
|
flags |= FILTER_SEC_CAPTION_INFO_EX;
|
|
}
|
|
else if( strcmp(item, "sec:dcmInfo") == 0 )
|
|
{
|
|
flags |= FILTER_SEC_DCM_INFO;
|
|
}
|
|
else if( strcmp(item, "res@pv:subtitleFileType") == 0 )
|
|
{
|
|
flags |= FILTER_PV_SUBTITLE_FILE_TYPE;
|
|
}
|
|
else if( strcmp(item, "res@pv:subtitleFileUri") == 0 )
|
|
{
|
|
flags |= FILTER_PV_SUBTITLE_FILE_URI;
|
|
}
|
|
else if( strcmp(item, "av:mediaClass") == 0 )
|
|
{
|
|
flags |= FILTER_AV_MEDIA_CLASS;
|
|
}
|
|
else if( strcmp(item, "upnp:episodeNumber") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_EPISODENUMBER;
|
|
}
|
|
else if( strcmp(item, "upnp:episodeSeason") == 0 )
|
|
{
|
|
flags |= FILTER_UPNP_EPISODESEASON;
|
|
}
|
|
item = strtok_r(NULL, ",", &saveptr);
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
static char *
|
|
parse_sort_criteria(char *sortCriteria, int *error)
|
|
{
|
|
char *order = NULL;
|
|
char *item, *saveptr;
|
|
int i, ret, reverse, title_sorted = 0;
|
|
struct string_s str;
|
|
*error = 0;
|
|
|
|
if( force_sort_criteria )
|
|
sortCriteria = strdup(force_sort_criteria);
|
|
if( !sortCriteria )
|
|
return NULL;
|
|
|
|
if( (item = strtok_r(sortCriteria, ",", &saveptr)) )
|
|
{
|
|
order = malloc(4096);
|
|
str.data = order;
|
|
str.size = 4096;
|
|
str.off = 0;
|
|
strcatf(&str, "order by ");
|
|
}
|
|
for( i = 0; item != NULL; i++ )
|
|
{
|
|
reverse=0;
|
|
if( i )
|
|
strcatf(&str, ", ");
|
|
if( *item == '+' )
|
|
{
|
|
item++;
|
|
}
|
|
else if( *item == '-' )
|
|
{
|
|
reverse = 1;
|
|
item++;
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_ERROR, L_HTTP, "No order specified [%s]\n", item);
|
|
goto bad_direction;
|
|
}
|
|
if( strcasecmp(item, "upnp:class") == 0 )
|
|
{
|
|
strcatf(&str, "o.CLASS");
|
|
}
|
|
else if( strcasecmp(item, "dc:title") == 0 )
|
|
{
|
|
strcatf(&str, "d.TITLE");
|
|
title_sorted = 1;
|
|
}
|
|
else if( strcasecmp(item, "dc:date") == 0 )
|
|
{
|
|
strcatf(&str, "d.DATE");
|
|
}
|
|
else if( strcasecmp(item, "upnp:originalTrackNumber") == 0 ||
|
|
strcasecmp(item, "upnp:episodeNumber") == 0 )
|
|
{
|
|
strcatf(&str, "d.DISC%s, d.TRACK", reverse ? " DESC" : "");
|
|
}
|
|
else if( strcasecmp(item, "upnp:album") == 0 )
|
|
{
|
|
strcatf(&str, "d.ALBUM");
|
|
}
|
|
else if( strcasecmp(item, "path") == 0 )
|
|
{
|
|
strcatf(&str, "d.PATH");
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_ERROR, L_HTTP, "Unhandled SortCriteria [%s]\n", item);
|
|
bad_direction:
|
|
*error = -1;
|
|
if( i )
|
|
{
|
|
ret = strlen(order);
|
|
order[ret-2] = '\0';
|
|
}
|
|
i--;
|
|
goto unhandled_order;
|
|
}
|
|
|
|
if( reverse )
|
|
strcatf(&str, " DESC");
|
|
unhandled_order:
|
|
item = strtok_r(NULL, ",", &saveptr);
|
|
}
|
|
if( i <= 0 )
|
|
{
|
|
free(order);
|
|
if( force_sort_criteria )
|
|
free(sortCriteria);
|
|
return NULL;
|
|
}
|
|
/* Add a "tiebreaker" sort order */
|
|
if( !title_sorted )
|
|
strcatf(&str, ", TITLE ASC");
|
|
|
|
if( force_sort_criteria )
|
|
free(sortCriteria);
|
|
|
|
return order;
|
|
}
|
|
|
|
static void
|
|
_alphasort_alt_title(char **title, char **alt_title, int requested, int returned, const char *disc, const char *track)
|
|
{
|
|
char *old_title = *alt_title ?: NULL;
|
|
char buf[8];
|
|
int pad;
|
|
int ret;
|
|
|
|
snprintf(buf, sizeof(buf), "%d", requested);
|
|
pad = strlen(buf);
|
|
|
|
if (NON_ZERO(track) && !strstr(*title, track)) {
|
|
if (NON_ZERO(disc))
|
|
ret = asprintf(alt_title, "%0*d %s.%s %s",
|
|
pad, returned, disc, track, *title);
|
|
else
|
|
ret = asprintf(alt_title, "%0*d %s %s",
|
|
pad, returned, track, *title);
|
|
}
|
|
else
|
|
ret = asprintf(alt_title, "%0*d %s", pad, returned, *title);
|
|
|
|
if (ret > 0)
|
|
*title = *alt_title;
|
|
else
|
|
*alt_title = NULL;
|
|
free(old_title);
|
|
}
|
|
|
|
inline static void
|
|
add_resized_res(int srcw, int srch, int reqw, int reqh, char *dlna_pn,
|
|
char *detailID, struct Response *args)
|
|
{
|
|
int dstw = reqw;
|
|
int dsth = reqh;
|
|
|
|
if( (args->flags & FLAG_NO_RESIZE) && reqw > 160 && reqh > 160 )
|
|
return;
|
|
|
|
strcatf(args->str, "<res ");
|
|
if( args->filter & FILTER_RES_RESOLUTION )
|
|
{
|
|
dstw = reqw;
|
|
dsth = ((((reqw<<10)/srcw)*srch)>>10);
|
|
if( dsth > reqh ) {
|
|
dsth = reqh;
|
|
dstw = (((reqh<<10)/srch) * srcw>>10);
|
|
}
|
|
strcatf(args->str, "resolution=\"%dx%d\" ", dstw, dsth);
|
|
}
|
|
strcatf(args->str, "protocolInfo=\"http-get:*:image/jpeg:"
|
|
"DLNA.ORG_PN=%s;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=%08X%024X\">"
|
|
"http://%s:%d/Resized/%s.jpg?width=%d,height=%d"
|
|
"</res>",
|
|
dlna_pn, DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_HTTP_STALLING|DLNA_FLAG_TM_B|DLNA_FLAG_TM_I, 0,
|
|
lan_addr[args->iface].str, runtime_vars.port,
|
|
detailID, dstw, dsth);
|
|
}
|
|
|
|
inline static void
|
|
add_res(char *size, char *duration, char *bitrate, char *sampleFrequency,
|
|
char *nrAudioChannels, char *resolution, char *dlna_pn, char *mime,
|
|
char *detailID, const char *ext, struct Response *args)
|
|
{
|
|
strcatf(args->str, "<res ");
|
|
if( size && (args->filter & FILTER_RES_SIZE) ) {
|
|
strcatf(args->str, "size=\"%s\" ", size);
|
|
}
|
|
if( duration && (args->filter & FILTER_RES_DURATION) ) {
|
|
strcatf(args->str, "duration=\"%s\" ", duration);
|
|
}
|
|
if( bitrate && (args->filter & FILTER_RES_BITRATE) ) {
|
|
int br = atoi(bitrate);
|
|
if(args->flags & FLAG_MS_PFS)
|
|
br /= 8;
|
|
strcatf(args->str, "bitrate=\"%d\" ", br);
|
|
}
|
|
if( sampleFrequency && (args->filter & FILTER_RES_SAMPLEFREQUENCY) ) {
|
|
strcatf(args->str, "sampleFrequency=\"%s\" ", sampleFrequency);
|
|
}
|
|
if( nrAudioChannels && (args->filter & FILTER_RES_NRAUDIOCHANNELS) ) {
|
|
strcatf(args->str, "nrAudioChannels=\"%s\" ", nrAudioChannels);
|
|
}
|
|
if( resolution && (args->filter & FILTER_RES_RESOLUTION) ) {
|
|
strcatf(args->str, "resolution=\"%s\" ", resolution);
|
|
}
|
|
if( args->filter & FILTER_PV_SUBTITLE )
|
|
{
|
|
if( args->flags & FLAG_HAS_CAPTIONS )
|
|
{
|
|
if( args->filter & FILTER_PV_SUBTITLE_FILE_TYPE )
|
|
strcatf(args->str, "pv:subtitleFileType=\"SRT\" ");
|
|
if( args->filter & FILTER_PV_SUBTITLE_FILE_URI )
|
|
strcatf(args->str, "pv:subtitleFileUri=\"http://%s:%d/Captions/%s.srt\" ",
|
|
lan_addr[args->iface].str, runtime_vars.port, detailID);
|
|
}
|
|
}
|
|
strcatf(args->str, "protocolInfo=\"http-get:*:%s:%s\">"
|
|
"http://%s:%d/MediaItems/%s.%s"
|
|
"</res>",
|
|
mime, dlna_pn, lan_addr[args->iface].str,
|
|
runtime_vars.port, detailID, ext);
|
|
}
|
|
|
|
static int
|
|
get_child_count(const char *object, struct magic_container_s *magic)
|
|
{
|
|
int ret;
|
|
|
|
if (magic && magic->child_count)
|
|
ret = sql_get_int_field(db, "SELECT count(*) from %s", magic->child_count);
|
|
else if (magic && magic->objectid && *(magic->objectid))
|
|
ret = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s';", *(magic->objectid));
|
|
else
|
|
ret = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s';", object);
|
|
|
|
return (ret > 0) ? ret : 0;
|
|
}
|
|
|
|
static int
|
|
object_exists(const char *object)
|
|
{
|
|
int ret;
|
|
ret = sql_get_int_field(db, "SELECT count(*) from OBJECTS where OBJECT_ID = '%q'",
|
|
strcmp(object, "*") == 0 ? "0" : object);
|
|
return (ret > 0);
|
|
}
|
|
|
|
#define COLUMNS "o.DETAIL_ID, o.CLASS," \
|
|
" d.SIZE, d.TITLE, d.DURATION, d.BITRATE, d.SAMPLERATE, d.ARTIST," \
|
|
" d.ALBUM, d.GENRE, d.COMMENT, d.CHANNELS, d.TRACK, d.DATE, d.RESOLUTION," \
|
|
" d.THUMBNAIL, d.CREATOR, d.DLNA_PN, d.MIME, d.ALBUM_ART, d.ROTATION, d.DISC "
|
|
#define SELECT_COLUMNS "SELECT o.OBJECT_ID, o.PARENT_ID, o.REF_ID, " COLUMNS
|
|
|
|
static int
|
|
callback(void *args, int argc, char **argv, char **azColName)
|
|
{
|
|
struct Response *passed_args = (struct Response *)args;
|
|
char *id = argv[0], *parent = argv[1], *refID = argv[2], *detailID = argv[3], *class = argv[4], *size = argv[5], *title = argv[6],
|
|
*duration = argv[7], *bitrate = argv[8], *sampleFrequency = argv[9], *artist = argv[10], *album = argv[11],
|
|
*genre = argv[12], *comment = argv[13], *nrAudioChannels = argv[14], *track = argv[15], *date = argv[16], *resolution = argv[17],
|
|
*tn = argv[18], *creator = argv[19], *dlna_pn = argv[20], *mime = argv[21], *album_art = argv[22], *rotate = argv[23], *disc = argv[24];
|
|
char dlna_buf[128];
|
|
const char *ext;
|
|
struct string_s *str = passed_args->str;
|
|
int ret = 0;
|
|
|
|
/* Make sure we have at least 8KB left of allocated memory to finish the response. */
|
|
if( str->off > (str->size - 8192) )
|
|
{
|
|
#if MAX_RESPONSE_SIZE > 0
|
|
if( (str->size+DEFAULT_RESP_SIZE) <= MAX_RESPONSE_SIZE )
|
|
{
|
|
#endif
|
|
str->data = realloc(str->data, (str->size+DEFAULT_RESP_SIZE));
|
|
if( str->data )
|
|
{
|
|
str->size += DEFAULT_RESP_SIZE;
|
|
DPRINTF(E_DEBUG, L_HTTP, "UPnP SOAP response enlarged to %lu. [%d results so far]\n",
|
|
(unsigned long)str->size, passed_args->returned);
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response truncated, realloc failed\n");
|
|
passed_args->flags |= RESPONSE_TRUNCATED;
|
|
return 1;
|
|
}
|
|
#if MAX_RESPONSE_SIZE > 0
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response would exceed the max response size [%lld], truncating\n", (long long int)MAX_RESPONSE_SIZE);
|
|
passed_args->flags |= RESPONSE_TRUNCATED;
|
|
return 1;
|
|
}
|
|
#endif
|
|
}
|
|
passed_args->returned++;
|
|
passed_args->flags &= ~RESPONSE_FLAGS;
|
|
|
|
if( strncmp(class, "item", 4) == 0 )
|
|
{
|
|
uint32_t dlna_flags = DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_HTTP_STALLING|DLNA_FLAG_TM_B;
|
|
char *alt_title = NULL;
|
|
/* We may need special handling for certain MIME types */
|
|
if( *mime == 'v' )
|
|
{
|
|
dlna_flags |= DLNA_FLAG_TM_S;
|
|
if (GETFLAG(SUBTITLES_MASK) &&
|
|
(passed_args->client >= EStandardDLNA150 || !passed_args->client))
|
|
passed_args->flags |= FLAG_CAPTION_RES;
|
|
|
|
if( passed_args->flags & FLAG_MIME_AVI_DIVX )
|
|
{
|
|
if( strcmp(mime, "video/x-msvideo") == 0 )
|
|
{
|
|
if( creator )
|
|
strcpy(mime+6, "divx");
|
|
else
|
|
strcpy(mime+6, "avi");
|
|
}
|
|
}
|
|
else if( passed_args->flags & FLAG_MIME_AVI_AVI )
|
|
{
|
|
if( strcmp(mime, "video/x-msvideo") == 0 )
|
|
{
|
|
strcpy(mime+6, "avi");
|
|
}
|
|
}
|
|
else if( passed_args->client == EFreeBox && dlna_pn )
|
|
{
|
|
if( strncmp(dlna_pn, "AVC_TS", 6) == 0 ||
|
|
strncmp(dlna_pn, "MPEG_TS", 7) == 0 )
|
|
{
|
|
strcpy(mime+6, "mp2t");
|
|
}
|
|
}
|
|
if( !(passed_args->flags & FLAG_DLNA) )
|
|
{
|
|
if( strcmp(mime+6, "vnd.dlna.mpeg-tts") == 0 )
|
|
{
|
|
strcpy(mime+6, "mpeg");
|
|
}
|
|
}
|
|
if( (passed_args->flags & FLAG_CAPTION_RES) ||
|
|
(passed_args->filter & (FILTER_SEC_CAPTION_INFO_EX|FILTER_PV_SUBTITLE)) )
|
|
{
|
|
if( sql_get_int_field(db, "SELECT ID from CAPTIONS where ID = '%s'", detailID) > 0 )
|
|
passed_args->flags |= FLAG_HAS_CAPTIONS;
|
|
}
|
|
/* From what I read, Samsung TV's expect a [wrong] MIME type of x-mkv. */
|
|
if( passed_args->flags & FLAG_SAMSUNG )
|
|
{
|
|
if( strcmp(mime+6, "x-matroska") == 0 )
|
|
{
|
|
strcpy(mime+8, "mkv");
|
|
}
|
|
}
|
|
/* LG hack: subtitles won't get used unless dc:title contains a dot. */
|
|
else if( passed_args->client == ELGDevice && (passed_args->flags & FLAG_HAS_CAPTIONS) )
|
|
{
|
|
ret = asprintf(&alt_title, "%s.", title);
|
|
if( ret > 0 )
|
|
title = alt_title;
|
|
else
|
|
alt_title = NULL;
|
|
}
|
|
/* Asus OPlay reboots with titles longer than 23 characters with some file types. */
|
|
else if( passed_args->client == EAsusOPlay && (passed_args->flags & FLAG_HAS_CAPTIONS) )
|
|
{
|
|
if( strlen(title) > 23 )
|
|
title[23] = '\0';
|
|
}
|
|
/* Hyundai hack: Only titles with a media extension get recognized. */
|
|
else if( passed_args->client == EHyundaiTV )
|
|
{
|
|
ext = mime_to_ext(mime);
|
|
ret = asprintf(&alt_title, "%s.%s", title, ext);
|
|
if( ret > 0 )
|
|
title = alt_title;
|
|
else
|
|
alt_title = NULL;
|
|
}
|
|
}
|
|
else if( *mime == 'a' )
|
|
{
|
|
dlna_flags |= DLNA_FLAG_TM_S;
|
|
if( strcmp(mime+6, "x-flac") == 0 )
|
|
{
|
|
if( passed_args->flags & FLAG_MIME_FLAC_FLAC )
|
|
{
|
|
strcpy(mime+6, "flac");
|
|
}
|
|
}
|
|
else if( strcmp(mime+6, "x-wav") == 0 )
|
|
{
|
|
if( passed_args->flags & FLAG_MIME_WAV_WAV )
|
|
{
|
|
strcpy(mime+6, "wav");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
dlna_flags |= DLNA_FLAG_TM_I;
|
|
/* Force an alphabetical sort, for clients that like to do their own sorting */
|
|
if( GETFLAG(FORCE_ALPHASORT_MASK) )
|
|
_alphasort_alt_title(&title, &alt_title, passed_args->requested, passed_args->returned, disc, track);
|
|
|
|
if( passed_args->flags & FLAG_SKIP_DLNA_PN )
|
|
dlna_pn = NULL;
|
|
|
|
if( dlna_pn )
|
|
snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_PN=%s;"
|
|
"DLNA.ORG_OP=01;"
|
|
"DLNA.ORG_CI=0;"
|
|
"DLNA.ORG_FLAGS=%08X%024X",
|
|
dlna_pn, dlna_flags, 0);
|
|
else if( passed_args->flags & FLAG_DLNA )
|
|
snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_OP=01;"
|
|
"DLNA.ORG_CI=0;"
|
|
"DLNA.ORG_FLAGS=%08X%024X",
|
|
dlna_flags, 0);
|
|
else
|
|
strcpy(dlna_buf, "*");
|
|
|
|
ret = strcatf(str, "<item id=\"%s\" parentID=\"%s\" restricted=\"1\"", id, parent);
|
|
if( refID && (passed_args->filter & FILTER_REFID) ) {
|
|
ret = strcatf(str, " refID=\"%s\"", refID);
|
|
}
|
|
ret = strcatf(str, ">"
|
|
"<dc:title>%s</dc:title>"
|
|
"<upnp:class>object.%s</upnp:class>",
|
|
title, class);
|
|
if( comment && (passed_args->filter & FILTER_DC_DESCRIPTION) ) {
|
|
ret = strcatf(str, "<dc:description>%.384s</dc:description>", comment);
|
|
}
|
|
if( creator && (passed_args->filter & FILTER_DC_CREATOR) ) {
|
|
ret = strcatf(str, "<dc:creator>%s</dc:creator>", creator);
|
|
}
|
|
if( date && (passed_args->filter & FILTER_DC_DATE) ) {
|
|
ret = strcatf(str, "<dc:date>%s</dc:date>", date);
|
|
}
|
|
if( (passed_args->filter & FILTER_BOOKMARK_MASK) ) {
|
|
/* Get bookmark */
|
|
int sec = sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID);
|
|
if( sec > 0 ) {
|
|
/* This format is wrong according to the UPnP/AV spec. It should be in duration format,
|
|
** so HH:MM:SS. But Kodi seems to be the only user of this tag, and it only works with a
|
|
** raw seconds value.
|
|
** If Kodi gets fixed, we can use duration_str(sec * 1000) here */
|
|
if( passed_args->flags & FLAG_CONVERT_MS ) {
|
|
sec *= 1000;
|
|
}
|
|
if( passed_args->filter & FILTER_UPNP_LASTPLAYBACKPOSITION )
|
|
ret = strcatf(str, "<upnp:lastPlaybackPosition>%d</upnp:lastPlaybackPosition>",
|
|
sec);
|
|
if( passed_args->filter & FILTER_SEC_DCM_INFO )
|
|
ret = strcatf(str, "<sec:dcmInfo>CREATIONDATE=0,FOLDER=%s,BM=%d</sec:dcmInfo>",
|
|
title, sec);
|
|
}
|
|
if( passed_args->filter & FILTER_UPNP_PLAYBACKCOUNT ) {
|
|
ret = strcatf(str, "<upnp:playbackCount>%d</upnp:playbackCount>",
|
|
sql_get_int_field(db, "SELECT WATCH_COUNT from BOOKMARKS where ID = '%s'", detailID));
|
|
}
|
|
}
|
|
free(alt_title);
|
|
if( artist ) {
|
|
if( (*mime == 'v') && (passed_args->filter & FILTER_UPNP_ACTOR) ) {
|
|
ret = strcatf(str, "<upnp:actor>%s</upnp:actor>", artist);
|
|
}
|
|
if( passed_args->filter & FILTER_UPNP_ARTIST ) {
|
|
ret = strcatf(str, "<upnp:artist>%s</upnp:artist>", artist);
|
|
}
|
|
}
|
|
if( album && (passed_args->filter & FILTER_UPNP_ALBUM) ) {
|
|
ret = strcatf(str, "<upnp:album>%s</upnp:album>", album);
|
|
}
|
|
if( genre && (passed_args->filter & FILTER_UPNP_GENRE) ) {
|
|
ret = strcatf(str, "<upnp:genre>%s</upnp:genre>", genre);
|
|
}
|
|
if( strncmp(id, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 ) {
|
|
track = strrchr(id, '$')+1;
|
|
}
|
|
if( NON_ZERO(track) ) {
|
|
if( *mime == 'a' && (passed_args->filter & FILTER_UPNP_ORIGINALTRACKNUMBER) ) {
|
|
ret = strcatf(str, "<upnp:originalTrackNumber>%s</upnp:originalTrackNumber>", track);
|
|
} else if( *mime == 'v' ) {
|
|
if( NON_ZERO(disc) && (passed_args->filter & FILTER_UPNP_EPISODESEASON) )
|
|
ret = strcatf(str, "<upnp:episodeSeason>%s</upnp:episodeSeason>", disc);
|
|
if( passed_args->filter & FILTER_UPNP_EPISODENUMBER )
|
|
ret = strcatf(str, "<upnp:episodeNumber>%s</upnp:episodeNumber>", track);
|
|
}
|
|
}
|
|
if( passed_args->filter & FILTER_RES ) {
|
|
ext = mime_to_ext(mime);
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
if( *mime == 'i' ) {
|
|
int srcw, srch;
|
|
if( resolution && (sscanf(resolution, "%6dx%6d", &srcw, &srch) == 2) )
|
|
{
|
|
if( srcw > 4096 || srch > 4096 )
|
|
add_resized_res(srcw, srch, 4096, 4096, "JPEG_LRG", detailID, passed_args);
|
|
if( srcw > 1024 || srch > 768 )
|
|
add_resized_res(srcw, srch, 1024, 768, "JPEG_MED", detailID, passed_args);
|
|
if( srcw > 640 || srch > 480 )
|
|
add_resized_res(srcw, srch, 640, 480, "JPEG_SM", detailID, passed_args);
|
|
}
|
|
if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) {
|
|
ret = strcatf(str, "<res protocolInfo=\"http-get:*:%s:%s\">"
|
|
"http://%s:%d/Thumbnails/%s.jpg"
|
|
"</res>",
|
|
mime, "DLNA.ORG_PN=JPEG_TN;DLNA.ORG_CI=1", lan_addr[passed_args->iface].str,
|
|
runtime_vars.port, detailID);
|
|
}
|
|
else
|
|
add_resized_res(srcw, srch, 160, 160, "JPEG_TN", detailID, passed_args);
|
|
}
|
|
else if( *mime == 'v' ) {
|
|
switch( passed_args->client ) {
|
|
case EToshibaTV:
|
|
if( dlna_pn &&
|
|
(strncmp(dlna_pn, "MPEG_TS_HD_NA", 13) == 0 ||
|
|
strncmp(dlna_pn, "MPEG_TS_SD_NA", 13) == 0 ||
|
|
strncmp(dlna_pn, "AVC_TS_MP_HD_AC3", 16) == 0 ||
|
|
strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0))
|
|
{
|
|
sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_NTSC");
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
}
|
|
break;
|
|
case ESonyBDP:
|
|
if( dlna_pn &&
|
|
(strncmp(dlna_pn, "AVC_TS", 6) == 0 ||
|
|
strncmp(dlna_pn, "MPEG_TS", 7) == 0) )
|
|
{
|
|
if( strncmp(dlna_pn, "MPEG_TS_SD_NA", 13) != 0 )
|
|
{
|
|
sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_TS_SD_NA");
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
}
|
|
if( strncmp(dlna_pn, "MPEG_TS_SD_EU", 13) != 0 )
|
|
{
|
|
sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_TS_SD_EU");
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
}
|
|
}
|
|
else if( (dlna_pn &&
|
|
(strncmp(dlna_pn, "AVC_MP4", 7) == 0 ||
|
|
strncmp(dlna_pn, "MPEG4_P2_MP4", 12) == 0)) ||
|
|
strcmp(mime+6, "x-matroska") == 0 ||
|
|
strcmp(mime+6, "x-msvideo") == 0 ||
|
|
strcmp(mime+6, "mpeg") == 0 )
|
|
{
|
|
strcpy(mime+6, "avi");
|
|
if( !dlna_pn || strncmp(dlna_pn, "MPEG_PS_NTSC", 12) != 0 )
|
|
{
|
|
sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_NTSC");
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
}
|
|
if( !dlna_pn || strncmp(dlna_pn, "MPEG_PS_PAL", 11) != 0 )
|
|
{
|
|
sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_PAL");
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
}
|
|
}
|
|
break;
|
|
case ESonyBravia:
|
|
/* BRAVIA KDL-##*X### series TVs do natively support AVC/AC3 in TS, but
|
|
require profile to be renamed (applies to _T and _ISO variants also) */
|
|
if( dlna_pn &&
|
|
(strncmp(dlna_pn, "AVC_TS_MP_SD_AC3", 16) == 0 ||
|
|
strncmp(dlna_pn, "AVC_TS_MP_HD_AC3", 16) == 0 ||
|
|
strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0))
|
|
{
|
|
sprintf(dlna_buf, "DLNA.ORG_PN=AVC_TS_HD_50_AC3%s", dlna_pn + 16);
|
|
add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
|
|
resolution, dlna_buf, mime, detailID, ext, passed_args);
|
|
}
|
|
break;
|
|
case ESamsungSeriesCDE:
|
|
case ELGDevice:
|
|
case ELGNetCastDevice:
|
|
case EAsusOPlay:
|
|
default:
|
|
if( passed_args->flags & FLAG_HAS_CAPTIONS )
|
|
{
|
|
if( passed_args->flags & FLAG_CAPTION_RES )
|
|
ret = strcatf(str, "<res protocolInfo=\"http-get:*:text/srt:*\">"
|
|
"http://%s:%d/Captions/%s.srt"
|
|
"</res>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
|
|
if( passed_args->filter & FILTER_SEC_CAPTION_INFO_EX )
|
|
ret = strcatf(str, "<sec:CaptionInfoEx sec:type=\"srt\">"
|
|
"http://%s:%d/Captions/%s.srt"
|
|
"</sec:CaptionInfoEx>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if( NON_ZERO(album_art) )
|
|
{
|
|
/* Video and audio album art is handled differently */
|
|
if( *mime == 'v' && (passed_args->filter & FILTER_RES) && !(passed_args->flags & FLAG_MS_PFS) ) {
|
|
ret = strcatf(str, "<res protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN\">"
|
|
"http://%s:%d/AlbumArt/%s-%s.jpg"
|
|
"</res>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
|
|
if (passed_args->client == ESamsungSeriesCDE ) {
|
|
ret = strcatf(str, "<res dlna:profileID=\"JPEG_SM\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\""
|
|
" protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;"
|
|
"DLNA.ORG_OP=01;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=%08X%024X\" resolution=\"320x320\">"
|
|
"http://%s:%d/AlbumArt/%s-%s.jpg"
|
|
"</res>",
|
|
DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_TM_B|DLNA_FLAG_TM_I, 0,
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
|
|
}
|
|
} else if( passed_args->filter & FILTER_UPNP_ALBUMARTURI ) {
|
|
ret = strcatf(str, "<upnp:albumArtURI");
|
|
if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) {
|
|
ret = strcatf(str, " dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"");
|
|
}
|
|
ret = strcatf(str, ">http://%s:%d/AlbumArt/%s-%s.jpg</upnp:albumArtURI>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
|
|
}
|
|
}
|
|
if( (passed_args->flags & FLAG_MS_PFS) && *mime == 'i' ) {
|
|
if( passed_args->client == EMediaRoom && !album )
|
|
ret = strcatf(str, "<upnp:album>%s</upnp:album>", "[No Keywords]");
|
|
|
|
/* EVA2000 doesn't seem to handle embedded thumbnails */
|
|
if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) {
|
|
ret = strcatf(str, "<upnp:albumArtURI>"
|
|
"http://%s:%d/Thumbnails/%s.jpg"
|
|
"</upnp:albumArtURI>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
|
|
} else {
|
|
ret = strcatf(str, "<upnp:albumArtURI>"
|
|
"http://%s:%d/Resized/%s.jpg?width=160,height=160"
|
|
"</upnp:albumArtURI>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
|
|
}
|
|
}
|
|
ret = strcatf(str, "</item>");
|
|
}
|
|
else if( strncmp(class, "container", 9) == 0 )
|
|
{
|
|
ret = strcatf(str, "<container id=\"%s\" parentID=\"%s\" restricted=\"1\" ", id, parent);
|
|
if( passed_args->filter & FILTER_SEARCHABLE ) {
|
|
ret = strcatf(str, "searchable=\"%d\" ", check_magic_container(id, passed_args->flags) ? 0 : 1);
|
|
}
|
|
if( passed_args->filter & FILTER_CHILDCOUNT ) {
|
|
ret = strcatf(str, "childCount=\"%d\"", get_child_count(id, check_magic_container(id, passed_args->flags)));
|
|
}
|
|
/* If the client calls for BrowseMetadata on root, we have to include our "upnp:searchClass"'s, unless they're filtered out */
|
|
if( passed_args->requested == 1 && strcmp(id, "0") == 0 && (passed_args->filter & FILTER_UPNP_SEARCHCLASS) ) {
|
|
ret = strcatf(str, ">"
|
|
"<upnp:searchClass includeDerived=\"1\">object.item.audioItem</upnp:searchClass>"
|
|
"<upnp:searchClass includeDerived=\"1\">object.item.imageItem</upnp:searchClass>"
|
|
"<upnp:searchClass includeDerived=\"1\">object.item.videoItem</upnp:searchClass");
|
|
}
|
|
ret = strcatf(str, ">"
|
|
"<dc:title>%s</dc:title>"
|
|
"<upnp:class>object.%s</upnp:class>",
|
|
title, class);
|
|
if( (passed_args->filter & FILTER_UPNP_STORAGEUSED) || strcmp(class+10, "storageFolder") == 0 ) {
|
|
/* TODO: Implement real folder size tracking */
|
|
ret = strcatf(str, "<upnp:storageUsed>%s</upnp:storageUsed>", (size ? size : "-1"));
|
|
}
|
|
if( creator && (passed_args->filter & FILTER_DC_CREATOR) ) {
|
|
ret = strcatf(str, "<dc:creator>%s</dc:creator>", creator);
|
|
}
|
|
if( genre && (passed_args->filter & FILTER_UPNP_GENRE) ) {
|
|
ret = strcatf(str, "<upnp:genre>%s</upnp:genre>", genre);
|
|
}
|
|
if( artist && (passed_args->filter & FILTER_UPNP_ARTIST) ) {
|
|
ret = strcatf(str, "<upnp:artist>%s</upnp:artist>", artist);
|
|
}
|
|
if( NON_ZERO(album_art) && (passed_args->filter & FILTER_UPNP_ALBUMARTURI) ) {
|
|
ret = strcatf(str, "<upnp:albumArtURI ");
|
|
if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) {
|
|
ret = strcatf(str, "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"");
|
|
}
|
|
ret = strcatf(str, ">http://%s:%d/AlbumArt/%s-%s.jpg</upnp:albumArtURI>",
|
|
lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
|
|
}
|
|
if( passed_args->filter & FILTER_AV_MEDIA_CLASS ) {
|
|
char class;
|
|
if( strncmp(id, MUSIC_ID, sizeof(MUSIC_ID)) == 0 )
|
|
class = 'M';
|
|
else if( strncmp(id, VIDEO_ID, sizeof(VIDEO_ID)) == 0 )
|
|
class = 'V';
|
|
else if( strncmp(id, IMAGE_ID, sizeof(IMAGE_ID)) == 0 )
|
|
class = 'P';
|
|
else
|
|
class = 0;
|
|
if( class )
|
|
ret = strcatf(str, "<av:mediaClass xmlns:av=\"urn:schemas-sony-com:av\">"
|
|
"%c</av:mediaClass>", class);
|
|
}
|
|
ret = strcatf(str, "</container>");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
BrowseContentDirectory(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp0[] =
|
|
"<u:BrowseResponse "
|
|
"xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
|
|
"<Result>"
|
|
"<DIDL-Lite"
|
|
CONTENT_DIRECTORY_SCHEMAS;
|
|
struct magic_container_s *magic;
|
|
char *zErrMsg = NULL;
|
|
char *sql, *ptr;
|
|
struct Response args;
|
|
struct string_s str;
|
|
int totalMatches = 0;
|
|
int ret;
|
|
const char *ObjectID, *BrowseFlag;
|
|
char *Filter, *SortCriteria;
|
|
const char *objectid_sql = "o.OBJECT_ID";
|
|
const char *parentid_sql = "o.PARENT_ID";
|
|
const char *refid_sql = "o.REF_ID";
|
|
char where[256] = "";
|
|
char *orderBy = NULL;
|
|
struct NameValueParserData data;
|
|
int RequestedCount = 0;
|
|
int StartingIndex = 0;
|
|
|
|
memset(&args, 0, sizeof(args));
|
|
memset(&str, 0, sizeof(str));
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
|
|
|
|
ObjectID = GetValueFromNameValueList(&data, "ObjectID");
|
|
Filter = GetValueFromNameValueList(&data, "Filter");
|
|
BrowseFlag = GetValueFromNameValueList(&data, "BrowseFlag");
|
|
SortCriteria = GetValueFromNameValueList(&data, "SortCriteria");
|
|
|
|
if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) )
|
|
RequestedCount = atoi(ptr);
|
|
if( RequestedCount < 0 )
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
goto browse_error;
|
|
}
|
|
if( !RequestedCount )
|
|
RequestedCount = -1;
|
|
if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) )
|
|
StartingIndex = atoi(ptr);
|
|
if( StartingIndex < 0 )
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
goto browse_error;
|
|
}
|
|
if( !BrowseFlag || (strcmp(BrowseFlag, "BrowseDirectChildren") && strcmp(BrowseFlag, "BrowseMetadata")) )
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
goto browse_error;
|
|
}
|
|
if( !ObjectID && !(ObjectID = GetValueFromNameValueList(&data, "ContainerID")) )
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
goto browse_error;
|
|
}
|
|
|
|
str.data = malloc(DEFAULT_RESP_SIZE);
|
|
str.size = DEFAULT_RESP_SIZE;
|
|
str.off = sprintf(str.data, "%s", resp0);
|
|
/* See if we need to include DLNA namespace reference */
|
|
args.iface = h->iface;
|
|
args.filter = set_filter_flags(Filter, h);
|
|
if( args.filter & FILTER_DLNA_NAMESPACE )
|
|
ret = strcatf(&str, DLNA_NAMESPACE);
|
|
if( args.filter & FILTER_PV_SUBTITLE )
|
|
ret = strcatf(&str, PV_NAMESPACE);
|
|
if( args.filter & FILTER_SEC )
|
|
ret = strcatf(&str, SEC_NAMESPACE);
|
|
strcatf(&str, ">\n");
|
|
|
|
args.returned = 0;
|
|
args.requested = RequestedCount;
|
|
args.client = h->req_client ? h->req_client->type->type : 0;
|
|
args.flags = h->req_client ? h->req_client->type->flags : 0;
|
|
args.str = &str;
|
|
DPRINTF(E_DEBUG, L_HTTP, "Browsing ContentDirectory:\n"
|
|
" * ObjectID: %s\n"
|
|
" * Count: %d\n"
|
|
" * StartingIndex: %d\n"
|
|
" * BrowseFlag: %s\n"
|
|
" * Filter: %s\n"
|
|
" * SortCriteria: %s\n",
|
|
ObjectID, RequestedCount, StartingIndex,
|
|
BrowseFlag, Filter, SortCriteria);
|
|
|
|
if( strcmp(BrowseFlag+6, "Metadata") == 0 )
|
|
{
|
|
const char *id = ObjectID;
|
|
args.requested = 1;
|
|
magic = in_magic_container(ObjectID, args.flags, &id);
|
|
if (magic)
|
|
{
|
|
if (magic->objectid_sql && strcmp(id, ObjectID) != 0)
|
|
objectid_sql = magic->objectid_sql;
|
|
if (magic->parentid_sql && strcmp(id, ObjectID) != 0)
|
|
parentid_sql = magic->parentid_sql;
|
|
if (magic->refid_sql)
|
|
refid_sql = magic->refid_sql;
|
|
}
|
|
sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS
|
|
"from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
|
|
" where OBJECT_ID = '%q';",
|
|
objectid_sql, parentid_sql, refid_sql, id);
|
|
ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
|
|
totalMatches = args.returned;
|
|
}
|
|
else
|
|
{
|
|
magic = check_magic_container(ObjectID, args.flags);
|
|
if (magic)
|
|
{
|
|
if (magic->objectid && *(magic->objectid))
|
|
ObjectID = *(magic->objectid);
|
|
if (magic->objectid_sql)
|
|
objectid_sql = magic->objectid_sql;
|
|
if (magic->parentid_sql)
|
|
parentid_sql = magic->parentid_sql;
|
|
if (magic->refid_sql)
|
|
refid_sql = magic->refid_sql;
|
|
if (magic->where)
|
|
strncpyt(where, magic->where, sizeof(where));
|
|
if (magic->orderby && !GETFLAG(DLNA_STRICT_MASK))
|
|
orderBy = strdup(magic->orderby);
|
|
if (magic->max_count > 0)
|
|
{
|
|
int limit = MAX(magic->max_count - StartingIndex, 0);
|
|
ret = get_child_count(ObjectID, magic);
|
|
totalMatches = MIN(ret, limit);
|
|
if (RequestedCount > limit || RequestedCount < 0)
|
|
RequestedCount = limit;
|
|
}
|
|
}
|
|
if (!where[0])
|
|
sqlite3_snprintf(sizeof(where), where, "PARENT_ID = '%q'", ObjectID);
|
|
|
|
if (!totalMatches)
|
|
totalMatches = get_child_count(ObjectID, magic);
|
|
ret = 0;
|
|
if (SortCriteria && !orderBy)
|
|
{
|
|
__SORT_LIMIT
|
|
orderBy = parse_sort_criteria(SortCriteria, &ret);
|
|
}
|
|
else if (!orderBy)
|
|
{
|
|
if( strncmp(ObjectID, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 )
|
|
{
|
|
if( strcmp(ObjectID, MUSIC_PLIST_ID) == 0 )
|
|
ret = xasprintf(&orderBy, "order by d.TITLE");
|
|
else
|
|
ret = xasprintf(&orderBy, "order by length(OBJECT_ID), OBJECT_ID");
|
|
}
|
|
else if( args.flags & FLAG_FORCE_SORT )
|
|
{
|
|
__SORT_LIMIT
|
|
ret = xasprintf(&orderBy, "order by o.CLASS, d.DISC, d.TRACK, d.TITLE");
|
|
}
|
|
/* LG TV ordering bug */
|
|
else if( args.client == ELGDevice )
|
|
ret = xasprintf(&orderBy, "order by o.CLASS, d.TITLE");
|
|
else
|
|
orderBy = parse_sort_criteria(SortCriteria, &ret);
|
|
if( ret == -1 )
|
|
{
|
|
free(orderBy);
|
|
orderBy = NULL;
|
|
ret = 0;
|
|
}
|
|
}
|
|
/* If it's a DLNA client, return an error for bad sort criteria */
|
|
if( ret < 0 && ((args.flags & FLAG_DLNA) || GETFLAG(DLNA_STRICT_MASK)) )
|
|
{
|
|
SoapError(h, 709, "Unsupported or invalid sort criteria");
|
|
goto browse_error;
|
|
}
|
|
|
|
sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS
|
|
"from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
|
|
" where %s %s limit %d, %d;",
|
|
objectid_sql, parentid_sql, refid_sql,
|
|
where, THISORNUL(orderBy), StartingIndex, RequestedCount);
|
|
DPRINTF(E_DEBUG, L_HTTP, "Browse SQL: %s\n", sql);
|
|
ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
|
|
}
|
|
if( ret != SQLITE_OK )
|
|
{
|
|
if( args.flags & RESPONSE_TRUNCATED )
|
|
{
|
|
sqlite3_free(zErrMsg);
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql);
|
|
sqlite3_free(zErrMsg);
|
|
SoapError(h, 709, "Unsupported or invalid sort criteria");
|
|
goto browse_error;
|
|
}
|
|
}
|
|
sqlite3_free(sql);
|
|
/* Does the object even exist? */
|
|
if( !totalMatches )
|
|
{
|
|
if( !object_exists(ObjectID) )
|
|
{
|
|
SoapError(h, 701, "No such object error");
|
|
goto browse_error;
|
|
}
|
|
}
|
|
ret = strcatf(&str, "</DIDL-Lite></Result>\n"
|
|
"<NumberReturned>%u</NumberReturned>\n"
|
|
"<TotalMatches>%u</TotalMatches>\n"
|
|
"<UpdateID>%u</UpdateID>"
|
|
"</u:BrowseResponse>",
|
|
args.returned, totalMatches, updateID);
|
|
BuildSendAndCloseSoapResp(h, str.data, str.off);
|
|
browse_error:
|
|
ClearNameValueList(&data);
|
|
free(orderBy);
|
|
free(str.data);
|
|
}
|
|
|
|
static inline void
|
|
charcat(struct string_s *str, char c)
|
|
{
|
|
if (str->size <= str->off)
|
|
{
|
|
str->data[str->size-1] = '\0';
|
|
return;
|
|
}
|
|
str->data[str->off] = c;
|
|
str->off += 1;
|
|
}
|
|
|
|
static inline char *
|
|
parse_search_criteria(const char *str, char *sep)
|
|
{
|
|
struct string_s criteria;
|
|
int len;
|
|
int literal = 0, like = 0, class = 0;
|
|
const char *s;
|
|
|
|
if (!str)
|
|
return strdup("1 = 1");
|
|
|
|
len = strlen(str) + 32;
|
|
criteria.data = malloc(len);
|
|
criteria.size = len;
|
|
criteria.off = 0;
|
|
|
|
s = str;
|
|
|
|
while (isspace(*s))
|
|
s++;
|
|
|
|
while (*s)
|
|
{
|
|
if (literal)
|
|
{
|
|
switch (*s) {
|
|
case '&':
|
|
if (strncmp(s, """, 6) == 0)
|
|
s += 5;
|
|
else if (strncmp(s, "'", 6) == 0)
|
|
{
|
|
strcatf(&criteria, "'");
|
|
s += 6;
|
|
continue;
|
|
}
|
|
else
|
|
break;
|
|
case '"':
|
|
literal = 0;
|
|
if (like)
|
|
{
|
|
charcat(&criteria, '%');
|
|
like--;
|
|
}
|
|
charcat(&criteria, '"');
|
|
break;
|
|
case '\\':
|
|
if (strncmp(s, "\\"", 7) == 0)
|
|
{
|
|
strcatf(&criteria, "&quot;");
|
|
s += 7;
|
|
continue;
|
|
}
|
|
break;
|
|
case 'o':
|
|
if (class)
|
|
{
|
|
class = 0;
|
|
if (strncmp(s, "object.", 7) == 0)
|
|
s += 7;
|
|
else if (strncmp(s, "object\"", 7) == 0 ||
|
|
strncmp(s, "object"", 12) == 0)
|
|
{
|
|
s += 6;
|
|
continue;
|
|
}
|
|
}
|
|
default:
|
|
charcat(&criteria, *s);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (*s) {
|
|
case '\\':
|
|
if (strncmp(s, "\\"", 7) == 0)
|
|
{
|
|
strcatf(&criteria, "&quot;");
|
|
s += 7;
|
|
continue;
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case '"':
|
|
literal = 1;
|
|
charcat(&criteria, *s);
|
|
if (like == 2)
|
|
{
|
|
charcat(&criteria, '%');
|
|
like--;
|
|
}
|
|
break;
|
|
case '&':
|
|
if (strncmp(s, """, 6) == 0)
|
|
{
|
|
literal = 1;
|
|
strcatf(&criteria, "\"");
|
|
if (like == 2)
|
|
{
|
|
charcat(&criteria, '%');
|
|
like--;
|
|
}
|
|
s += 5;
|
|
}
|
|
else if (strncmp(s, "'", 6) == 0)
|
|
{
|
|
strcatf(&criteria, "'");
|
|
s += 5;
|
|
}
|
|
else if (strncmp(s, "<", 4) == 0)
|
|
{
|
|
strcatf(&criteria, "<");
|
|
s += 3;
|
|
}
|
|
else if (strncmp(s, ">", 4) == 0)
|
|
{
|
|
strcatf(&criteria, ">");
|
|
s += 3;
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case '@':
|
|
if (strncmp(s, "@refID", 6) == 0)
|
|
{
|
|
strcatf(&criteria, "REF_ID");
|
|
s += 6;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "@id", 3) == 0)
|
|
{
|
|
strcatf(&criteria, "OBJECT_ID");
|
|
s += 3;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "@parentID", 9) == 0)
|
|
{
|
|
strcatf(&criteria, "PARENT_ID");
|
|
s += 9;
|
|
strcpy(sep, "*");
|
|
continue;
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case 'c':
|
|
if (strncmp(s, "contains", 8) == 0)
|
|
{
|
|
strcatf(&criteria, "like");
|
|
s += 8;
|
|
like = 2;
|
|
continue;
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case 'd':
|
|
if (strncmp(s, "derivedfrom", 11) == 0)
|
|
{
|
|
strcatf(&criteria, "like");
|
|
s += 11;
|
|
like = 1;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "dc:date", 7) == 0)
|
|
{
|
|
strcatf(&criteria, "d.DATE");
|
|
s += 7;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "dc:title", 8) == 0)
|
|
{
|
|
strcatf(&criteria, "d.TITLE");
|
|
s += 8;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "dc:creator", 10) == 0)
|
|
{
|
|
strcatf(&criteria, "d.CREATOR");
|
|
s += 10;
|
|
continue;
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case 'e':
|
|
if (strncmp(s, "exists", 6) == 0)
|
|
{
|
|
s += 6;
|
|
while (isspace(*s))
|
|
s++;
|
|
if (strncmp(s, "true", 4) == 0)
|
|
{
|
|
strcatf(&criteria, "is not NULL");
|
|
s += 3;
|
|
}
|
|
else if (strncmp(s, "false", 5) == 0)
|
|
{
|
|
strcatf(&criteria, "is NULL");
|
|
s += 4;
|
|
}
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case 'o':
|
|
if (class)
|
|
{
|
|
if (strncmp(s, "object.", 7) == 0)
|
|
{
|
|
s += 7;
|
|
charcat(&criteria, '"');
|
|
while (*s && !isspace(*s))
|
|
{
|
|
charcat(&criteria, *s);
|
|
s++;
|
|
}
|
|
charcat(&criteria, '"');
|
|
}
|
|
class = 0;
|
|
continue;
|
|
}
|
|
case 'u':
|
|
if (strncmp(s, "upnp:class", 10) == 0)
|
|
{
|
|
strcatf(&criteria, "o.CLASS");
|
|
s += 10;
|
|
class = 1;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "upnp:actor", 10) == 0)
|
|
{
|
|
strcatf(&criteria, "d.ARTIST");
|
|
s += 10;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "upnp:artist", 11) == 0)
|
|
{
|
|
strcatf(&criteria, "d.ARTIST");
|
|
s += 11;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "upnp:album", 10) == 0)
|
|
{
|
|
strcatf(&criteria, "d.ALBUM");
|
|
s += 10;
|
|
continue;
|
|
}
|
|
else if (strncmp(s, "upnp:genre", 10) == 0)
|
|
{
|
|
strcatf(&criteria, "d.GENRE");
|
|
s += 10;
|
|
continue;
|
|
}
|
|
else
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case '(':
|
|
if (s > str && !isspace(s[-1]))
|
|
charcat(&criteria, ' ');
|
|
charcat(&criteria, *s);
|
|
break;
|
|
case ')':
|
|
charcat(&criteria, *s);
|
|
if (!isspace(s[1]))
|
|
charcat(&criteria, ' ');
|
|
break;
|
|
default:
|
|
charcat(&criteria, *s);
|
|
break;
|
|
}
|
|
}
|
|
s++;
|
|
}
|
|
charcat(&criteria, '\0');
|
|
|
|
return criteria.data;
|
|
}
|
|
|
|
static void
|
|
SearchContentDirectory(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp0[] =
|
|
"<u:SearchResponse "
|
|
"xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
|
|
"<Result>"
|
|
"<DIDL-Lite"
|
|
CONTENT_DIRECTORY_SCHEMAS;
|
|
struct magic_container_s *magic;
|
|
char *zErrMsg = NULL;
|
|
char *sql, *ptr;
|
|
struct Response args;
|
|
struct string_s str;
|
|
int totalMatches;
|
|
int ret;
|
|
const char *ContainerID;
|
|
char *Filter, *SearchCriteria, *SortCriteria;
|
|
char *orderBy = NULL, *where = NULL, sep[] = "$*";
|
|
char groupBy[] = "group by DETAIL_ID";
|
|
struct NameValueParserData data;
|
|
int RequestedCount = 0;
|
|
int StartingIndex = 0;
|
|
|
|
memset(&args, 0, sizeof(args));
|
|
memset(&str, 0, sizeof(str));
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
|
|
|
|
ContainerID = GetValueFromNameValueList(&data, "ContainerID");
|
|
Filter = GetValueFromNameValueList(&data, "Filter");
|
|
SearchCriteria = GetValueFromNameValueList(&data, "SearchCriteria");
|
|
SortCriteria = GetValueFromNameValueList(&data, "SortCriteria");
|
|
|
|
if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) )
|
|
RequestedCount = atoi(ptr);
|
|
if( !RequestedCount )
|
|
RequestedCount = -1;
|
|
if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) )
|
|
StartingIndex = atoi(ptr);
|
|
if( !ContainerID )
|
|
{
|
|
if( !(ContainerID = GetValueFromNameValueList(&data, "ObjectID")) )
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
goto search_error;
|
|
}
|
|
}
|
|
|
|
str.data = malloc(DEFAULT_RESP_SIZE);
|
|
str.size = DEFAULT_RESP_SIZE;
|
|
str.off = sprintf(str.data, "%s", resp0);
|
|
/* See if we need to include DLNA namespace reference */
|
|
args.iface = h->iface;
|
|
args.filter = set_filter_flags(Filter, h);
|
|
if( args.filter & FILTER_DLNA_NAMESPACE )
|
|
{
|
|
ret = strcatf(&str, DLNA_NAMESPACE);
|
|
}
|
|
strcatf(&str, ">\n");
|
|
|
|
args.returned = 0;
|
|
args.requested = RequestedCount;
|
|
args.client = h->req_client ? h->req_client->type->type : 0;
|
|
args.flags = h->req_client ? h->req_client->type->flags : 0;
|
|
args.str = &str;
|
|
DPRINTF(E_DEBUG, L_HTTP, "Searching ContentDirectory:\n"
|
|
" * ObjectID: %s\n"
|
|
" * Count: %d\n"
|
|
" * StartingIndex: %d\n"
|
|
" * SearchCriteria: %s\n"
|
|
" * Filter: %s\n"
|
|
" * SortCriteria: %s\n",
|
|
ContainerID, RequestedCount, StartingIndex,
|
|
SearchCriteria, Filter, SortCriteria);
|
|
|
|
magic = check_magic_container(ContainerID, args.flags);
|
|
if (magic && magic->objectid && *(magic->objectid))
|
|
ContainerID = *(magic->objectid);
|
|
|
|
if( strcmp(ContainerID, "0") == 0 )
|
|
ContainerID = "*";
|
|
|
|
if( strcmp(ContainerID, MUSIC_ALL_ID) == 0 ||
|
|
GETFLAG(DLNA_STRICT_MASK) )
|
|
groupBy[0] = '\0';
|
|
|
|
where = parse_search_criteria(SearchCriteria, sep);
|
|
DPRINTF(E_DEBUG, L_HTTP, "Translated SearchCriteria: %s\n", where);
|
|
|
|
totalMatches = sql_get_int_field(db, "SELECT (select count(distinct DETAIL_ID)"
|
|
" from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
|
|
" where (OBJECT_ID glob '%q%s') and (%s))"
|
|
" + "
|
|
"(select count(*) from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
|
|
" where (OBJECT_ID = '%q') and (%s))",
|
|
ContainerID, sep, where, ContainerID, where);
|
|
if( totalMatches < 0 )
|
|
{
|
|
/* Must be invalid SQL, so most likely bad or unhandled search criteria. */
|
|
SoapError(h, 708, "Unsupported or invalid search criteria");
|
|
goto search_error;
|
|
}
|
|
/* Does the object even exist? */
|
|
if( !totalMatches )
|
|
{
|
|
if( !object_exists(ContainerID) )
|
|
{
|
|
SoapError(h, 710, "No such container");
|
|
goto search_error;
|
|
}
|
|
}
|
|
ret = 0;
|
|
__SORT_LIMIT
|
|
orderBy = parse_sort_criteria(SortCriteria, &ret);
|
|
/* If it's a DLNA client, return an error for bad sort criteria */
|
|
if( ret < 0 && ((args.flags & FLAG_DLNA) || GETFLAG(DLNA_STRICT_MASK)) )
|
|
{
|
|
SoapError(h, 709, "Unsupported or invalid sort criteria");
|
|
goto search_error;
|
|
}
|
|
|
|
sql = sqlite3_mprintf( SELECT_COLUMNS
|
|
"from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
|
|
" where OBJECT_ID glob '%q%s' and (%s) %s "
|
|
"%z %s"
|
|
" limit %d, %d",
|
|
ContainerID, sep, where, groupBy,
|
|
(*ContainerID == '*') ? NULL :
|
|
sqlite3_mprintf("UNION ALL " SELECT_COLUMNS
|
|
"from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
|
|
" where OBJECT_ID = '%q' and (%s) ", ContainerID, where),
|
|
orderBy, StartingIndex, RequestedCount);
|
|
DPRINTF(E_DEBUG, L_HTTP, "Search SQL: %s\n", sql);
|
|
ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
|
|
if( ret != SQLITE_OK )
|
|
{
|
|
if( !(args.flags & RESPONSE_TRUNCATED) )
|
|
DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql);
|
|
sqlite3_free(zErrMsg);
|
|
}
|
|
sqlite3_free(sql);
|
|
ret = strcatf(&str, "</DIDL-Lite></Result>\n"
|
|
"<NumberReturned>%u</NumberReturned>\n"
|
|
"<TotalMatches>%u</TotalMatches>\n"
|
|
"<UpdateID>%u</UpdateID>"
|
|
"</u:SearchResponse>",
|
|
args.returned, totalMatches, updateID);
|
|
BuildSendAndCloseSoapResp(h, str.data, str.off);
|
|
search_error:
|
|
ClearNameValueList(&data);
|
|
free(orderBy);
|
|
free(where);
|
|
free(str.data);
|
|
}
|
|
|
|
/*
|
|
If a control point calls QueryStateVariable on a state variable that is not
|
|
buffered in memory within (or otherwise available from) the service,
|
|
the service must return a SOAP fault with an errorCode of 404 Invalid Var.
|
|
|
|
QueryStateVariable remains useful as a limited test tool but may not be
|
|
part of some future versions of UPnP.
|
|
*/
|
|
static void
|
|
QueryStateVariable(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:%sResponse "
|
|
"xmlns:u=\"%s\">"
|
|
"<return>%s</return>"
|
|
"</u:%sResponse>";
|
|
|
|
char body[512];
|
|
struct NameValueParserData data;
|
|
const char * var_name;
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
|
|
/*var_name = GetValueFromNameValueList(&data, "QueryStateVariable"); */
|
|
/*var_name = GetValueFromNameValueListIgnoreNS(&data, "varName");*/
|
|
var_name = GetValueFromNameValueList(&data, "varName");
|
|
|
|
DPRINTF(E_INFO, L_HTTP, "QueryStateVariable(%.40s)\n", var_name);
|
|
|
|
if(!var_name)
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
}
|
|
else if(strcmp(var_name, "ConnectionStatus") == 0)
|
|
{
|
|
int bodylen;
|
|
bodylen = snprintf(body, sizeof(body), resp,
|
|
action, "urn:schemas-upnp-org:control-1-0",
|
|
"Connected", action);
|
|
BuildSendAndCloseSoapResp(h, body, bodylen);
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_WARN, L_HTTP, "%s: Unknown: %s\n", action, THISORNUL(var_name));
|
|
SoapError(h, 404, "Invalid Var");
|
|
}
|
|
|
|
ClearNameValueList(&data);
|
|
}
|
|
|
|
static int _set_watch_count(long long id, const char *old, const char *new)
|
|
{
|
|
int64_t rowid = sqlite3_last_insert_rowid(db);
|
|
int ret;
|
|
|
|
ret = sql_exec(db, "INSERT or IGNORE into BOOKMARKS (ID, WATCH_COUNT)"
|
|
" VALUES (%lld, %Q)", id, new ?: "1");
|
|
if (sqlite3_last_insert_rowid(db) != rowid)
|
|
return 0;
|
|
|
|
if (!new) /* Increment */
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT ="
|
|
" ifnull(WATCH_COUNT,'0') + 1"
|
|
" where ID = %lld", id);
|
|
else if (old && old[0])
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
|
|
" where WATCH_COUNT = %Q and ID = %lld",
|
|
new, old, id);
|
|
else
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
|
|
" where ID = %lld",
|
|
new, id);
|
|
return ret;
|
|
}
|
|
|
|
/* For some reason, Kodi does URI encoding and appends a trailing slash */
|
|
static void _kodi_decode(char *str)
|
|
{
|
|
while (*str)
|
|
{
|
|
switch (*str) {
|
|
case '%':
|
|
{
|
|
if (isxdigit(str[1]) && isxdigit(str[2]))
|
|
{
|
|
char x[3] = { str[1], str[2], '\0' };
|
|
*str++ = (char)strtol(x, NULL, 16);
|
|
memmove(str, str+2, strlen(str+1));
|
|
}
|
|
break;
|
|
}
|
|
case '/':
|
|
if (!str[1])
|
|
*str = '\0';
|
|
default:
|
|
str++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int duration_sec(const char *str)
|
|
{
|
|
int hr, min, sec;
|
|
|
|
if (sscanf(str, "%d:%d:%d", &hr, &min, &sec) == 3)
|
|
return (hr * 3600) + (min * 60) + sec;
|
|
|
|
return atoi(str);
|
|
}
|
|
|
|
static void UpdateObject(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:UpdateObjectResponse"
|
|
" xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
|
|
"</u:UpdateObjecResponse>";
|
|
|
|
struct NameValueParserData data;
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
|
|
|
|
char *ObjectID = GetValueFromNameValueList(&data, "ObjectID");
|
|
char *CurrentTagValue = GetValueFromNameValueList(&data, "CurrentTagValue");
|
|
char *NewTagValue = GetValueFromNameValueList(&data, "NewTagValue");
|
|
const char *rid = ObjectID;
|
|
char tag[32], current[32], new[32];
|
|
char *item, *saveptr = NULL;
|
|
int64_t detailID;
|
|
int ret = 1;
|
|
|
|
if (!ObjectID || !CurrentTagValue || !NewTagValue)
|
|
{
|
|
SoapError(h, 402, "Invalid Args");
|
|
ClearNameValueList(&data);
|
|
return;
|
|
}
|
|
|
|
_kodi_decode(ObjectID);
|
|
DPRINTF(E_DEBUG, L_HTTP, "UpdateObject %s: %s => %s\n", ObjectID, CurrentTagValue, NewTagValue);
|
|
|
|
in_magic_container(ObjectID, 0, &rid);
|
|
detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid);
|
|
if (detailID <= 0)
|
|
{
|
|
SoapError(h, 701, "No such object");
|
|
ClearNameValueList(&data);
|
|
return;
|
|
}
|
|
|
|
for (item = strtok_r(CurrentTagValue, ",", &saveptr); item; item = strtok_r(NULL, ",", &saveptr))
|
|
{
|
|
char *p;
|
|
if (sscanf(item, "<%31[^&]>%31[^&]", tag, current) != 2)
|
|
continue;
|
|
p = strstr(NewTagValue, tag);
|
|
if (!p || sscanf(p, "%*[^&]>%31[^&]", new) != 1)
|
|
continue;
|
|
|
|
DPRINTF(E_DEBUG, L_HTTP, "Setting %s to %s\n", tag, new);
|
|
/* Kodi uses incorrect tag "upnp:playCount" instead of "upnp:playbackCount" */
|
|
if (strcmp(tag, "upnp:playbackCount") == 0 || strcmp(tag, "upnp:playCount") == 0)
|
|
{
|
|
ret = _set_watch_count(detailID, current, new);
|
|
}
|
|
else if (strcmp(tag, "upnp:lastPlaybackPosition") == 0)
|
|
{
|
|
|
|
int sec = duration_sec(new);
|
|
if( h->req_client && (h->req_client->type->flags & FLAG_CONVERT_MS) ) {
|
|
sec /= 1000;
|
|
}
|
|
if (sec < 30)
|
|
sec = 0;
|
|
else
|
|
sec -= 1;
|
|
ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)"
|
|
" VALUES (%lld, %d)", (long long)detailID, sec);
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d"
|
|
" where SEC = %Q and ID = %lld",
|
|
sec, current, (long long)detailID);
|
|
}
|
|
else
|
|
DPRINTF(E_WARN, L_HTTP, "Tag %s unsupported for writing\n", tag);
|
|
}
|
|
|
|
if (ret == SQLITE_OK)
|
|
BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
|
|
else
|
|
SoapError(h, 501, "Action Failed");
|
|
|
|
ClearNameValueList(&data);
|
|
}
|
|
|
|
static void
|
|
SamsungGetFeatureList(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:X_GetFeatureListResponse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
|
|
"<FeatureList>"
|
|
"<Features xmlns=\"urn:schemas-upnp-org:av:avs\""
|
|
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
|
|
" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
|
|
"<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
|
|
"<container id=\"%s\" type=\"object.item.audioItem\"/>"
|
|
"<container id=\"%s\" type=\"object.item.videoItem\"/>"
|
|
"<container id=\"%s\" type=\"object.item.imageItem\"/>"
|
|
"</Feature>"
|
|
"</Features>"
|
|
"</FeatureList></u:X_GetFeatureListResponse>";
|
|
const char *audio = MUSIC_ID;
|
|
const char *video = VIDEO_ID;
|
|
const char *image = IMAGE_ID;
|
|
char body[1024];
|
|
int len;
|
|
|
|
if (runtime_vars.root_container)
|
|
{
|
|
if (strcmp(runtime_vars.root_container, BROWSEDIR_ID) == 0)
|
|
{
|
|
audio = MUSIC_DIR_ID;
|
|
video = VIDEO_DIR_ID;
|
|
image = IMAGE_DIR_ID;
|
|
}
|
|
else
|
|
{
|
|
audio = runtime_vars.root_container;
|
|
video = runtime_vars.root_container;
|
|
image = runtime_vars.root_container;
|
|
}
|
|
}
|
|
else if (h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG_DCM10))
|
|
{
|
|
audio = "A";
|
|
video = "V";
|
|
image = "I";
|
|
}
|
|
|
|
len = snprintf(body, sizeof(body), resp, audio, video, image);
|
|
|
|
BuildSendAndCloseSoapResp(h, body, len);
|
|
}
|
|
|
|
static void
|
|
SamsungSetBookmark(struct upnphttp * h, const char * action)
|
|
{
|
|
static const char resp[] =
|
|
"<u:X_SetBookmarkResponse"
|
|
" xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
|
|
"</u:X_SetBookmarkResponse>";
|
|
|
|
struct NameValueParserData data;
|
|
char *ObjectID, *PosSecond;
|
|
|
|
ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
|
|
ObjectID = GetValueFromNameValueList(&data, "ObjectID");
|
|
PosSecond = GetValueFromNameValueList(&data, "PosSecond");
|
|
|
|
if( ObjectID && PosSecond )
|
|
{
|
|
const char *rid = ObjectID;
|
|
int64_t detailID;
|
|
int sec = atoi(PosSecond);
|
|
int ret;
|
|
|
|
in_magic_container(ObjectID, 0, &rid);
|
|
detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid);
|
|
|
|
if( h->req_client && (h->req_client->type->flags & FLAG_CONVERT_MS) ) {
|
|
sec /= 1000;
|
|
}
|
|
if ( sec < 30 )
|
|
sec = 0;
|
|
ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)"
|
|
" VALUES (%lld, %d)", (long long)detailID, sec);
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d"
|
|
" where ID = %lld",
|
|
sec, (long long)detailID);
|
|
if( ret != SQLITE_OK )
|
|
DPRINTF(E_WARN, L_METADATA, "Error setting bookmark %s on ObjectID='%s'\n", PosSecond, rid);
|
|
BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
|
|
}
|
|
else
|
|
SoapError(h, 402, "Invalid Args");
|
|
|
|
ClearNameValueList(&data);
|
|
}
|
|
|
|
static const struct
|
|
{
|
|
const char * methodName;
|
|
void (*methodImpl)(struct upnphttp *, const char *);
|
|
}
|
|
soapMethods[] =
|
|
{
|
|
{ "QueryStateVariable", QueryStateVariable},
|
|
{ "Browse", BrowseContentDirectory},
|
|
{ "Search", SearchContentDirectory},
|
|
{ "GetSearchCapabilities", GetSearchCapabilities},
|
|
{ "GetSortCapabilities", GetSortCapabilities},
|
|
{ "GetSystemUpdateID", GetSystemUpdateID},
|
|
{ "GetProtocolInfo", GetProtocolInfo},
|
|
{ "GetCurrentConnectionIDs", GetCurrentConnectionIDs},
|
|
{ "GetCurrentConnectionInfo", GetCurrentConnectionInfo},
|
|
{ "IsAuthorized", IsAuthorizedValidated},
|
|
{ "IsValidated", IsAuthorizedValidated},
|
|
{ "RegisterDevice", RegisterDevice},
|
|
{ "UpdateObject", UpdateObject},
|
|
{ "X_GetFeatureList", SamsungGetFeatureList},
|
|
{ "X_SetBookmark", SamsungSetBookmark},
|
|
{ 0, 0 }
|
|
};
|
|
|
|
void
|
|
ExecuteSoapAction(struct upnphttp * h, const char * action, int n)
|
|
{
|
|
char * p;
|
|
|
|
p = strchr(action, '#');
|
|
if(p)
|
|
{
|
|
int i = 0;
|
|
int len;
|
|
int methodlen;
|
|
char * p2;
|
|
p++;
|
|
p2 = strchr(p, '"');
|
|
if(p2)
|
|
methodlen = p2 - p;
|
|
else
|
|
methodlen = n - (p - action);
|
|
DPRINTF(E_DEBUG, L_HTTP, "SoapMethod: %.*s\n", methodlen, p);
|
|
while(soapMethods[i].methodName)
|
|
{
|
|
len = strlen(soapMethods[i].methodName);
|
|
if(strncmp(p, soapMethods[i].methodName, len) == 0)
|
|
{
|
|
soapMethods[i].methodImpl(h, soapMethods[i].methodName);
|
|
return;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
DPRINTF(E_WARN, L_HTTP, "SoapMethod: Unknown: %.*s\n", methodlen, p);
|
|
}
|
|
|
|
SoapError(h, 401, "Invalid Action");
|
|
}
|
|
|