The idea is taken from the nginx web server, but much simplified and almost no copypaste left. This will allow minidlna to use different event dispatcher APIs, which would be defined at compile time. My personal goal is to convert minidlna to kqueue(2) on FreeBSD. This would later allow for kqueue based directory change notification, which won't conflict with select(2) like the current patch does. Other platforms will also benefit from the pluggability of the event system, Linux can switch to epoll(2) or at least to poll(2). Detailed list of changes: * event.h [New] Our internal API to unify different event dispatch systems. * select.c [New] Much simplified version of nginx's ngx_select_module.c. * minidlna.c - Split out listen socket event processing into separate function ProcessListen(), which matches event_process_t type. - Create and initialize struct event for the monitor socket, SSDP socket, HTTP socket and beacon socket. - Simplify and make more precise timeout calculation using helper timeval functions from utils.c. Treat gettimeofday() error as a fatal event. - Rip out all stuff related to select(2). Just call event_module.process(). * upnpevents.c - Embed struct event into upnp_event_notify. - Merge upnp_event_create_notify() with upnp_event_notify_connect(). Start connecting immediately after socket creation. Garbage collect now useless ECreated state. - Make upnp_event_process_notify() of event_process_t type, and use it as process callback for upnp_event_notify event. - Looks like we always create upnp_event_notify with existing subscriber, and never clear it later. Remove checks for obj->sub and assert that it is never NULL. Simplifies things. - When switching obj state, add/del it to event dispatcher accrodingly. - Garbage collect upnpevents_selectfds(). - Garbage collect select(2) related stuff from upnpevents_processfds(). Rename function to upnpevents_gc(), since the remaining functionality is garbage collecting, not file descriptor processing. Actually, this can be simplified even more. We can safely close sockets and free objects immediately, eliminating need for upnpevents_gc(). But this change would be beyond scope of this commit. * upnphttp.c, upnphttp.h Embed struct event into struct upnphttp. Adjust Process_upnphttp() to match event_process_t type. Add/del to event dispatcher once creating/closing a socket. * minissdp.c, minissdp.h Make ProcessSSDPRequest() of event_process_t type. * getifaddr.c, getifaddr.h Make ProcessMonitorEvent() of event_process_t type.
2257 lines
67 KiB
C
2257 lines
67 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.
|
|
*/
|
|
static void
|
|
SoapError(struct upnphttp * h, int errCode, const char * errDesc)
|
|
{
|
|
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, "Returning UPnPError %d: %s\n", 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
|
|
{
|
|
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 was too big, and realloc failed!\n");
|
|
return -1;
|
|
}
|
|
#if MAX_RESPONSE_SIZE > 0
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response cut short, to not exceed the max response size [%lld]!\n", (long long int)MAX_RESPONSE_SIZE);
|
|
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 && (passed_args->filter & FILTER_UPNP_LASTPLAYBACKPOSITION) ) {
|
|
/* 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 */
|
|
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));
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
free(alt_title);
|
|
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) && (zErrMsg != NULL) )
|
|
{
|
|
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;
|
|
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 (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 'u':
|
|
if (strncmp(s, "upnp:class", 10) == 0)
|
|
{
|
|
strcatf(&criteria, "o.CLASS");
|
|
s += 10;
|
|
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) && (zErrMsg != NULL) )
|
|
{
|
|
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);
|
|
}
|
|
|
|
/* 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 = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, WATCH_COUNT)"
|
|
ret = sql_exec(db, "INSERT into BOOKMARKS (ID, WATCH_COUNT)"
|
|
" VALUES (%lld, %Q)", (long long)detailID, new);
|
|
if (atoi(new))
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
|
|
" where WATCH_COUNT = %Q and ID = %lld",
|
|
new, current, (long long)detailID);
|
|
else
|
|
ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = 0"
|
|
" where ID = %lld", (long long)detailID);
|
|
}
|
|
else if (strcmp(tag, "upnp:lastPlaybackPosition") == 0)
|
|
{
|
|
int sec = duration_sec(new);
|
|
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 ( 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");
|
|
}
|
|
|