"Fossies" - the Fresh Open Source Software Archive

Member "minidlna-1.3.0/upnpsoap.c" (24 Nov 2020, 70211 Bytes) of package /linux/privat/minidlna-1.3.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "upnpsoap.c" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.2.1_vs_1.3.0.

    1 /* MiniDLNA project
    2  *
    3  * http://sourceforge.net/projects/minidlna/
    4  *
    5  * MiniDLNA media server
    6  * Copyright (C) 2008-2017  Justin Maggard
    7  *
    8  * This file is part of MiniDLNA.
    9  *
   10  * MiniDLNA is free software; you can redistribute it and/or modify
   11  * it under the terms of the GNU General Public License version 2 as
   12  * published by the Free Software Foundation.
   13  *
   14  * MiniDLNA is distributed in the hope that it will be useful,
   15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
   16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   17  * GNU General Public License for more details.
   18  *
   19  * You should have received a copy of the GNU General Public License
   20  * along with MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
   21  *
   22  * Portions of the code from the MiniUPnP project:
   23  *
   24  * Copyright (c) 2006-2007, Thomas Bernard
   25  * All rights reserved.
   26  *
   27  * Redistribution and use in source and binary forms, with or without
   28  * modification, are permitted provided that the following conditions are met:
   29  *     * Redistributions of source code must retain the above copyright
   30  *       notice, this list of conditions and the following disclaimer.
   31  *     * Redistributions in binary form must reproduce the above copyright
   32  *       notice, this list of conditions and the following disclaimer in the
   33  *       documentation and/or other materials provided with the distribution.
   34  *     * The name of the author may not be used to endorse or promote products
   35  *       derived from this software without specific prior written permission.
   36  *
   37  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   38  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   39  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   40  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
   41  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   42  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   43  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   44  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   45  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   46  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   47  * POSSIBILITY OF SUCH DAMAGE.
   48  */
   49 #include "config.h"
   50 
   51 #include <stdio.h>
   52 #include <stdlib.h>
   53 #include <string.h>
   54 #include <sys/socket.h>
   55 #include <unistd.h>
   56 #include <dirent.h>
   57 #include <sys/stat.h>
   58 #include <sys/types.h>
   59 #include <arpa/inet.h>
   60 #include <netinet/in.h>
   61 #include <netdb.h>
   62 #include <ctype.h>
   63 
   64 #include "event.h"
   65 #include "upnpglobalvars.h"
   66 #include "utils.h"
   67 #include "upnphttp.h"
   68 #include "upnpsoap.h"
   69 #include "containers.h"
   70 #include "upnpreplyparse.h"
   71 #include "getifaddr.h"
   72 #include "scanner.h"
   73 #include "sql.h"
   74 #include "log.h"
   75 
   76 #ifdef __sparc__ /* Sorting takes too long on slow processors with very large containers */
   77 # define __SORT_LIMIT if( totalMatches < 10000 )
   78 #else
   79 # define __SORT_LIMIT
   80 #endif
   81 #define NON_ZERO(x) (x && atoi(x))
   82 #define IS_ZERO(x) (!x || !atoi(x))
   83 
   84 /* Standard Errors:
   85  *
   86  * errorCode errorDescription Description
   87  * -------- ---------------- -----------
   88  * 401      Invalid Action  No action by that name at this service.
   89  * 402      Invalid Args    Could be any of the following: not enough in args,
   90  *                          too many in args, no in arg by that name,
   91  *                          one or more in args are of the wrong data type.
   92  * 403      Out of Sync     Out of synchronization.
   93  * 501      Action Failed   May be returned in current state of service
   94  *                          prevents invoking that action.
   95  * 600-699  TBD             Common action errors. Defined by UPnP Forum
   96  *                          Technical Committee.
   97  * 700-799  TBD             Action-specific errors for standard actions.
   98  *                          Defined by UPnP Forum working committee.
   99  * 800-899  TBD             Action-specific errors for non-standard actions.
  100  *                          Defined by UPnP vendor.
  101 */
  102 #define SoapError(x,y,z) _SoapError(x,y,z,__func__)
  103 static void
  104 _SoapError(struct upnphttp * h, int errCode, const char * errDesc, const char *func)
  105 {
  106     static const char resp[] =
  107         "<s:Envelope "
  108         "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
  109         "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
  110         "<s:Body>"
  111         "<s:Fault>"
  112         "<faultcode>s:Client</faultcode>"
  113         "<faultstring>UPnPError</faultstring>"
  114         "<detail>"
  115         "<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\">"
  116         "<errorCode>%d</errorCode>"
  117         "<errorDescription>%s</errorDescription>"
  118         "</UPnPError>"
  119         "</detail>"
  120         "</s:Fault>"
  121         "</s:Body>"
  122         "</s:Envelope>";
  123 
  124     char body[2048];
  125     int bodylen;
  126 
  127     DPRINTF(E_WARN, L_HTTP, "%s Returning UPnPError %d: %s\n", func, errCode, errDesc);
  128     bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc);
  129     BuildResp2_upnphttp(h, 500, "Internal Server Error", body, bodylen);
  130     SendResp_upnphttp(h);
  131     CloseSocket_upnphttp(h);
  132 }
  133 
  134 static void
  135 BuildSendAndCloseSoapResp(struct upnphttp * h,
  136                           const char * body, int bodylen)
  137 {
  138     static const char beforebody[] =
  139         "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
  140         "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
  141         "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
  142         "<s:Body>";
  143 
  144     static const char afterbody[] =
  145         "</s:Body>"
  146         "</s:Envelope>\r\n";
  147 
  148     if (!body || bodylen < 0)
  149     {
  150         Send500(h);
  151         return;
  152     }
  153 
  154     BuildHeader_upnphttp(h, 200, "OK",  sizeof(beforebody) - 1
  155         + sizeof(afterbody) - 1 + bodylen );
  156 
  157     memcpy(h->res_buf + h->res_buflen, beforebody, sizeof(beforebody) - 1);
  158     h->res_buflen += sizeof(beforebody) - 1;
  159 
  160     memcpy(h->res_buf + h->res_buflen, body, bodylen);
  161     h->res_buflen += bodylen;
  162 
  163     memcpy(h->res_buf + h->res_buflen, afterbody, sizeof(afterbody) - 1);
  164     h->res_buflen += sizeof(afterbody) - 1;
  165 
  166     SendResp_upnphttp(h);
  167     CloseSocket_upnphttp(h);
  168 }
  169 
  170 static void
  171 GetSystemUpdateID(struct upnphttp * h, const char * action)
  172 {
  173     static const char resp[] =
  174         "<u:%sResponse "
  175         "xmlns:u=\"%s\">"
  176         "<Id>%d</Id>"
  177         "</u:%sResponse>";
  178 
  179     char body[512];
  180     int bodylen;
  181 
  182     bodylen = snprintf(body, sizeof(body), resp,
  183         action, "urn:schemas-upnp-org:service:ContentDirectory:1",
  184         updateID, action);
  185     BuildSendAndCloseSoapResp(h, body, bodylen);
  186 }
  187 
  188 static void
  189 IsAuthorizedValidated(struct upnphttp * h, const char * action)
  190 {
  191     static const char resp[] =
  192         "<u:%sResponse "
  193         "xmlns:u=\"%s\">"
  194         "<Result>%d</Result>"
  195         "</u:%sResponse>";
  196 
  197     char body[512];
  198     struct NameValueParserData data;
  199     const char * id;
  200 
  201     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, XML_STORE_EMPTY_FL);
  202     id = GetValueFromNameValueList(&data, "DeviceID");
  203     if(id)
  204     {
  205         int bodylen;
  206         bodylen = snprintf(body, sizeof(body), resp,
  207             action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
  208             1, action);
  209         BuildSendAndCloseSoapResp(h, body, bodylen);
  210     }
  211     else
  212         SoapError(h, 402, "Invalid Args");
  213 
  214     ClearNameValueList(&data);
  215 }
  216 
  217 static void
  218 RegisterDevice(struct upnphttp * h, const char * action)
  219 {
  220     static const char resp[] =
  221         "<u:%sResponse "
  222         "xmlns:u=\"%s\">"
  223         "<RegistrationRespMsg>%s</RegistrationRespMsg>"
  224         "</u:%sResponse>";
  225 
  226     char body[512];
  227     int bodylen;
  228 
  229     bodylen = snprintf(body, sizeof(body), resp,
  230         action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
  231         uuidvalue, action);
  232     BuildSendAndCloseSoapResp(h, body, bodylen);
  233 }
  234 
  235 static void
  236 GetProtocolInfo(struct upnphttp * h, const char * action)
  237 {
  238     static const char resp[] =
  239         "<u:%sResponse "
  240         "xmlns:u=\"%s\">"
  241         "<Source>"
  242         RESOURCE_PROTOCOL_INFO_VALUES
  243         "</Source>"
  244         "<Sink></Sink>"
  245         "</u:%sResponse>";
  246 
  247     char * body;
  248     int bodylen;
  249 
  250     bodylen = asprintf(&body, resp,
  251         action, "urn:schemas-upnp-org:service:ConnectionManager:1",
  252         action);
  253     BuildSendAndCloseSoapResp(h, body, bodylen);
  254     free(body);
  255 }
  256 
  257 static void
  258 GetSortCapabilities(struct upnphttp * h, const char * action)
  259 {
  260     static const char resp[] =
  261         "<u:%sResponse "
  262         "xmlns:u=\"%s\">"
  263         "<SortCaps>"
  264           "dc:title,"
  265           "dc:date,"
  266           "upnp:class,"
  267           "upnp:album,"
  268           "upnp:episodeNumber,"
  269           "upnp:originalTrackNumber"
  270         "</SortCaps>"
  271         "</u:%sResponse>";
  272 
  273     char body[512];
  274     int bodylen;
  275 
  276     bodylen = snprintf(body, sizeof(body), resp,
  277         action, "urn:schemas-upnp-org:service:ContentDirectory:1",
  278         action);
  279     BuildSendAndCloseSoapResp(h, body, bodylen);
  280 }
  281 
  282 static void
  283 GetSearchCapabilities(struct upnphttp * h, const char * action)
  284 {
  285     static const char resp[] =
  286         "<u:%sResponse xmlns:u=\"%s\">"
  287         "<SearchCaps>"
  288           "dc:creator,"
  289           "dc:date,"
  290           "dc:title,"
  291           "upnp:album,"
  292           "upnp:actor,"
  293           "upnp:artist,"
  294           "upnp:class,"
  295           "upnp:genre,"
  296           "@id,"
  297           "@parentID,"
  298           "@refID"
  299         "</SearchCaps>"
  300         "</u:%sResponse>";
  301 
  302     char body[512];
  303     int bodylen;
  304 
  305     bodylen = snprintf(body, sizeof(body), resp,
  306         action, "urn:schemas-upnp-org:service:ContentDirectory:1",
  307         action);
  308     BuildSendAndCloseSoapResp(h, body, bodylen);
  309 }
  310 
  311 static void
  312 GetCurrentConnectionIDs(struct upnphttp * h, const char * action)
  313 {
  314     /* TODO: Use real data. - JM */
  315     static const char resp[] =
  316         "<u:%sResponse "
  317         "xmlns:u=\"%s\">"
  318         "<ConnectionIDs>0</ConnectionIDs>"
  319         "</u:%sResponse>";
  320 
  321     char body[512];
  322     int bodylen;
  323 
  324     bodylen = snprintf(body, sizeof(body), resp,
  325         action, "urn:schemas-upnp-org:service:ConnectionManager:1",
  326         action);
  327     BuildSendAndCloseSoapResp(h, body, bodylen);
  328 }
  329 
  330 static void
  331 GetCurrentConnectionInfo(struct upnphttp * h, const char * action)
  332 {
  333     /* TODO: Use real data. - JM */
  334     static const char resp[] =
  335         "<u:%sResponse "
  336         "xmlns:u=\"%s\">"
  337         "<RcsID>-1</RcsID>"
  338         "<AVTransportID>-1</AVTransportID>"
  339         "<ProtocolInfo></ProtocolInfo>"
  340         "<PeerConnectionManager></PeerConnectionManager>"
  341         "<PeerConnectionID>-1</PeerConnectionID>"
  342         "<Direction>Output</Direction>"
  343         "<Status>Unknown</Status>"
  344         "</u:%sResponse>";
  345 
  346     char body[sizeof(resp)+128];
  347     struct NameValueParserData data;
  348     const char *id_str;
  349     int id;
  350     char *endptr = NULL;
  351 
  352     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, XML_STORE_EMPTY_FL);
  353     id_str = GetValueFromNameValueList(&data, "ConnectionID");
  354     DPRINTF(E_INFO, L_HTTP, "GetCurrentConnectionInfo(%s)\n", id_str);
  355     if(id_str)
  356         id = strtol(id_str, &endptr, 10);
  357     if (!id_str || endptr == id_str)
  358     {
  359         SoapError(h, 402, "Invalid Args");
  360     }
  361     else if(id != 0)
  362     {
  363         SoapError(h, 701, "No such object error");
  364     }
  365     else
  366     {
  367         int bodylen;
  368         bodylen = snprintf(body, sizeof(body), resp,
  369             action, "urn:schemas-upnp-org:service:ConnectionManager:1",
  370             action);
  371         BuildSendAndCloseSoapResp(h, body, bodylen);
  372     }
  373     ClearNameValueList(&data);
  374 }
  375 
  376 /* Standard DLNA/UPnP filter flags */
  377 #define FILTER_CHILDCOUNT           0x00000001
  378 #define FILTER_DC_CREATOR           0x00000002
  379 #define FILTER_DC_DATE              0x00000004
  380 #define FILTER_DC_DESCRIPTION           0x00000008
  381 #define FILTER_DLNA_NAMESPACE           0x00000010
  382 #define FILTER_REFID                0x00000020
  383 #define FILTER_RES              0x00000040
  384 #define FILTER_RES_BITRATE          0x00000080
  385 #define FILTER_RES_DURATION         0x00000100
  386 #define FILTER_RES_NRAUDIOCHANNELS      0x00000200
  387 #define FILTER_RES_RESOLUTION           0x00000400
  388 #define FILTER_RES_SAMPLEFREQUENCY      0x00000800
  389 #define FILTER_RES_SIZE             0x00001000
  390 #define FILTER_SEARCHABLE           0x00002000
  391 #define FILTER_UPNP_ACTOR           0x00004000
  392 #define FILTER_UPNP_ALBUM           0x00008000
  393 #define FILTER_UPNP_ALBUMARTURI         0x00010000
  394 #define FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID  0x00020000
  395 #define FILTER_UPNP_ARTIST          0x00040000
  396 #define FILTER_UPNP_EPISODENUMBER       0x00080000
  397 #define FILTER_UPNP_EPISODESEASON       0x00100000
  398 #define FILTER_UPNP_GENRE           0x00200000
  399 #define FILTER_UPNP_ORIGINALTRACKNUMBER     0x00400000
  400 #define FILTER_UPNP_SEARCHCLASS         0x00800000
  401 #define FILTER_UPNP_STORAGEUSED         0x01000000
  402 /* Not normally used, so leave out of the default filter */
  403 #define FILTER_UPNP_PLAYBACKCOUNT       0x02000000
  404 #define FILTER_UPNP_LASTPLAYBACKPOSITION    0x04000000
  405 /* Vendor-specific filter flags */
  406 #define FILTER_SEC_CAPTION_INFO_EX      0x08000000
  407 #define FILTER_SEC_DCM_INFO         0x10000000
  408 #define FILTER_SEC              0x18000000
  409 #define FILTER_PV_SUBTITLE_FILE_TYPE        0x20000000
  410 #define FILTER_PV_SUBTITLE_FILE_URI     0x40000000
  411 #define FILTER_PV_SUBTITLE          0x60000000
  412 #define FILTER_AV_MEDIA_CLASS           0x80000000
  413 /* Masks */
  414 #define STANDARD_FILTER_MASK            0x01FFFFFF
  415 #define FILTER_BOOKMARK_MASK            (FILTER_UPNP_PLAYBACKCOUNT | \
  416                          FILTER_UPNP_LASTPLAYBACKPOSITION | \
  417                          FILTER_SEC_DCM_INFO)
  418 
  419 static uint32_t
  420 set_filter_flags(char *filter, struct upnphttp *h)
  421 {
  422     char *item, *saveptr = NULL;
  423     uint32_t flags = 0;
  424     int samsung = h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG);
  425 
  426     if( !filter || (strlen(filter) <= 1) ) {
  427         /* Not the full 32 bits.  Skip vendor-specific stuff by default. */
  428         flags = STANDARD_FILTER_MASK;
  429         if (samsung)
  430             flags |= FILTER_SEC_CAPTION_INFO_EX | FILTER_SEC_DCM_INFO;
  431     }
  432     if (flags)
  433         return flags;
  434 
  435     if( samsung )
  436         flags |= FILTER_DLNA_NAMESPACE;
  437     item = strtok_r(filter, ",", &saveptr);
  438     while( item != NULL )
  439     {
  440         if( saveptr )
  441             *(item-1) = ',';
  442         while( isspace(*item) )
  443             item++;
  444         if( strcmp(item, "@childCount") == 0 )
  445         {
  446             flags |= FILTER_CHILDCOUNT;
  447         }
  448         else if( strcmp(item, "@searchable") == 0 )
  449         {
  450             flags |= FILTER_SEARCHABLE;
  451         }
  452         else if( strcmp(item, "dc:creator") == 0 )
  453         {
  454             flags |= FILTER_DC_CREATOR;
  455         }
  456         else if( strcmp(item, "dc:date") == 0 )
  457         {
  458             flags |= FILTER_DC_DATE;
  459         }
  460         else if( strcmp(item, "dc:description") == 0 )
  461         {
  462             flags |= FILTER_DC_DESCRIPTION;
  463         }
  464         else if( strcmp(item, "dlna") == 0 )
  465         {
  466             flags |= FILTER_DLNA_NAMESPACE;
  467         }
  468         else if( strcmp(item, "@refID") == 0 )
  469         {
  470             flags |= FILTER_REFID;
  471         }
  472         else if( strcmp(item, "upnp:album") == 0 )
  473         {
  474             flags |= FILTER_UPNP_ALBUM;
  475         }
  476         else if( strcmp(item, "upnp:albumArtURI") == 0 )
  477         {
  478             flags |= FILTER_UPNP_ALBUMARTURI;
  479             if( samsung )
  480                 flags |= FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID;
  481         }
  482         else if( strcmp(item, "upnp:albumArtURI@dlna:profileID") == 0 )
  483         {
  484             flags |= FILTER_UPNP_ALBUMARTURI;
  485             flags |= FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID;
  486         }
  487         else if( strcmp(item, "upnp:artist") == 0 )
  488         {
  489             flags |= FILTER_UPNP_ARTIST;
  490         }
  491         else if( strcmp(item, "upnp:actor") == 0 )
  492         {
  493             flags |= FILTER_UPNP_ACTOR;
  494         }
  495         else if( strcmp(item, "upnp:genre") == 0 )
  496         {
  497             flags |= FILTER_UPNP_GENRE;
  498         }
  499         else if( strcmp(item, "upnp:originalTrackNumber") == 0 )
  500         {
  501             flags |= FILTER_UPNP_ORIGINALTRACKNUMBER;
  502         }
  503         else if( strcmp(item, "upnp:searchClass") == 0 )
  504         {
  505             flags |= FILTER_UPNP_SEARCHCLASS;
  506         }
  507         else if( strcmp(item, "upnp:storageUsed") == 0 )
  508         {
  509             flags |= FILTER_UPNP_STORAGEUSED;
  510         }
  511         else if( strcmp(item, "res") == 0 )
  512         {
  513             flags |= FILTER_RES;
  514         }
  515         else if( (strcmp(item, "res@bitrate") == 0) ||
  516                  (strcmp(item, "@bitrate") == 0) ||
  517                  ((strcmp(item, "bitrate") == 0) && (flags & FILTER_RES)) )
  518         {
  519             flags |= FILTER_RES;
  520             flags |= FILTER_RES_BITRATE;
  521         }
  522         else if( (strcmp(item, "res@duration") == 0) ||
  523                  (strcmp(item, "@duration") == 0) ||
  524                  ((strcmp(item, "duration") == 0) && (flags & FILTER_RES)) )
  525         {
  526             flags |= FILTER_RES;
  527             flags |= FILTER_RES_DURATION;
  528         }
  529         else if( (strcmp(item, "res@nrAudioChannels") == 0) ||
  530                  (strcmp(item, "@nrAudioChannels") == 0) ||
  531                  ((strcmp(item, "nrAudioChannels") == 0) && (flags & FILTER_RES)) )
  532         {
  533             flags |= FILTER_RES;
  534             flags |= FILTER_RES_NRAUDIOCHANNELS;
  535         }
  536         else if( (strcmp(item, "res@resolution") == 0) ||
  537                  (strcmp(item, "@resolution") == 0) ||
  538                  ((strcmp(item, "resolution") == 0) && (flags & FILTER_RES)) )
  539         {
  540             flags |= FILTER_RES;
  541             flags |= FILTER_RES_RESOLUTION;
  542         }
  543         else if( (strcmp(item, "res@sampleFrequency") == 0) ||
  544                  (strcmp(item, "@sampleFrequency") == 0) ||
  545                  ((strcmp(item, "sampleFrequency") == 0) && (flags & FILTER_RES)) )
  546         {
  547             flags |= FILTER_RES;
  548             flags |= FILTER_RES_SAMPLEFREQUENCY;
  549         }
  550         else if( (strcmp(item, "res@size") == 0) ||
  551                  (strcmp(item, "@size") == 0) ||
  552                  (strcmp(item, "size") == 0) )
  553         {
  554             flags |= FILTER_RES;
  555             flags |= FILTER_RES_SIZE;
  556         }
  557         else if( strcmp(item, "upnp:playbackCount") == 0 )
  558         {
  559             flags |= FILTER_UPNP_PLAYBACKCOUNT;
  560         }
  561         else if( strcmp(item, "upnp:lastPlaybackPosition") == 0 )
  562         {
  563             flags |= FILTER_UPNP_LASTPLAYBACKPOSITION;
  564         }
  565         else if( strcmp(item, "sec:CaptionInfoEx") == 0 )
  566         {
  567             flags |= FILTER_SEC_CAPTION_INFO_EX;
  568         }
  569         else if( strcmp(item, "sec:dcmInfo") == 0 )
  570         {
  571             flags |= FILTER_SEC_DCM_INFO;
  572         }
  573         else if( strcmp(item, "res@pv:subtitleFileType") == 0 )
  574         {
  575             flags |= FILTER_PV_SUBTITLE_FILE_TYPE;
  576         }
  577         else if( strcmp(item, "res@pv:subtitleFileUri") == 0 )
  578         {
  579             flags |= FILTER_PV_SUBTITLE_FILE_URI;
  580         }
  581         else if( strcmp(item, "av:mediaClass") == 0 )
  582         {
  583             flags |= FILTER_AV_MEDIA_CLASS;
  584         }
  585         else if( strcmp(item, "upnp:episodeNumber") == 0 )
  586         {
  587             flags |= FILTER_UPNP_EPISODENUMBER;
  588         }
  589         else if( strcmp(item, "upnp:episodeSeason") == 0 )
  590         {
  591             flags |= FILTER_UPNP_EPISODESEASON;
  592         }
  593         item = strtok_r(NULL, ",", &saveptr);
  594     }
  595 
  596     return flags;
  597 }
  598 
  599 static char *
  600 parse_sort_criteria(char *sortCriteria, int *error)
  601 {
  602     char *order = NULL;
  603     char *item, *saveptr;
  604     int i, ret, reverse, title_sorted = 0;
  605     struct string_s str;
  606     *error = 0;
  607 
  608     if( force_sort_criteria )
  609         sortCriteria = strdup(force_sort_criteria);
  610     if( !sortCriteria )
  611         return NULL;
  612 
  613     if( (item = strtok_r(sortCriteria, ",", &saveptr)) )
  614     {
  615         order = malloc(4096);
  616         str.data = order;
  617         str.size = 4096;
  618         str.off = 0;
  619         strcatf(&str, "order by ");
  620     }
  621     for( i = 0; item != NULL; i++ )
  622     {
  623         reverse=0;
  624         if( i )
  625             strcatf(&str, ", ");
  626         if( *item == '+' )
  627         {
  628             item++;
  629         }
  630         else if( *item == '-' )
  631         {
  632             reverse = 1;
  633             item++;
  634         }
  635         else
  636         {
  637             DPRINTF(E_ERROR, L_HTTP, "No order specified [%s]\n", item);
  638             goto bad_direction;
  639         }
  640         if( strcasecmp(item, "upnp:class") == 0 )
  641         {
  642             strcatf(&str, "o.CLASS");
  643         }
  644         else if( strcasecmp(item, "dc:title") == 0 )
  645         {
  646             strcatf(&str, "d.TITLE");
  647             title_sorted = 1;
  648         }
  649         else if( strcasecmp(item, "dc:date") == 0 )
  650         {
  651             strcatf(&str, "d.DATE");
  652         }
  653         else if( strcasecmp(item, "upnp:originalTrackNumber") == 0 ||
  654              strcasecmp(item, "upnp:episodeNumber") == 0 )
  655         {
  656             strcatf(&str, "d.DISC%s, d.TRACK", reverse ? " DESC" : "");
  657         }
  658         else if( strcasecmp(item, "upnp:album") == 0 )
  659         {
  660             strcatf(&str, "d.ALBUM");
  661         }
  662         else if( strcasecmp(item, "path") == 0 )
  663         {
  664             strcatf(&str, "d.PATH");
  665         }
  666         else
  667         {
  668             DPRINTF(E_ERROR, L_HTTP, "Unhandled SortCriteria [%s]\n", item);
  669         bad_direction:
  670             *error = -1;
  671             if( i )
  672             {
  673                 ret = strlen(order);
  674                 order[ret-2] = '\0';
  675             }
  676             i--;
  677             goto unhandled_order;
  678         }
  679 
  680         if( reverse )
  681             strcatf(&str, " DESC");
  682         unhandled_order:
  683         item = strtok_r(NULL, ",", &saveptr);
  684     }
  685     if( i <= 0 )
  686     {
  687         free(order);
  688         if( force_sort_criteria )
  689             free(sortCriteria);
  690         return NULL;
  691     }
  692     /* Add a "tiebreaker" sort order */
  693     if( !title_sorted )
  694         strcatf(&str, ", TITLE ASC");
  695 
  696     if( force_sort_criteria )
  697         free(sortCriteria);
  698 
  699     return order;
  700 }
  701 
  702 static void
  703 _alphasort_alt_title(char **title, char **alt_title, int requested, int returned, const char *disc, const char *track)
  704 {
  705     char *old_title = *alt_title ?: NULL;
  706     char buf[8];
  707     int pad;
  708     int ret;
  709 
  710     snprintf(buf, sizeof(buf), "%d", requested);
  711     pad = strlen(buf);
  712 
  713     if (NON_ZERO(track) && !strstr(*title, track)) {
  714         if (NON_ZERO(disc))
  715             ret = asprintf(alt_title, "%0*d %s.%s %s",
  716                     pad, returned, disc, track, *title);
  717         else
  718             ret = asprintf(alt_title, "%0*d %s %s",
  719                     pad, returned, track, *title);
  720     }
  721     else
  722         ret = asprintf(alt_title, "%0*d %s", pad, returned, *title);
  723 
  724     if (ret > 0)
  725         *title = *alt_title;
  726     else
  727         *alt_title = NULL;
  728     free(old_title);
  729 }
  730 
  731 inline static void
  732 add_resized_res(int srcw, int srch, int reqw, int reqh, char *dlna_pn,
  733                 char *detailID, struct Response *args)
  734 {
  735     int dstw = reqw;
  736     int dsth = reqh;
  737 
  738     if( (args->flags & FLAG_NO_RESIZE) && reqw > 160 && reqh > 160 )
  739         return;
  740 
  741     strcatf(args->str, "&lt;res ");
  742     if( args->filter & FILTER_RES_RESOLUTION )
  743     {
  744         dstw = reqw;
  745         dsth = ((((reqw<<10)/srcw)*srch)>>10);
  746         if( dsth > reqh ) {
  747             dsth = reqh;
  748             dstw = (((reqh<<10)/srch) * srcw>>10);
  749         }
  750         strcatf(args->str, "resolution=\"%dx%d\" ", dstw, dsth);
  751     }
  752     strcatf(args->str, "protocolInfo=\"http-get:*:image/jpeg:"
  753                               "DLNA.ORG_PN=%s;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=%08X%024X\"&gt;"
  754                               "http://%s:%d/Resized/%s.jpg?width=%d,height=%d"
  755                               "&lt;/res&gt;",
  756                               dlna_pn, DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_HTTP_STALLING|DLNA_FLAG_TM_B|DLNA_FLAG_TM_I, 0,
  757                               lan_addr[args->iface].str, runtime_vars.port,
  758                               detailID, dstw, dsth);
  759 }
  760 
  761 inline static void
  762 add_res(char *size, char *duration, char *bitrate, char *sampleFrequency,
  763         char *nrAudioChannels, char *resolution, char *dlna_pn, char *mime,
  764         char *detailID, const char *ext, struct Response *args)
  765 {
  766     strcatf(args->str, "&lt;res ");
  767     if( size && (args->filter & FILTER_RES_SIZE) ) {
  768         strcatf(args->str, "size=\"%s\" ", size);
  769     }
  770     if( duration && (args->filter & FILTER_RES_DURATION) ) {
  771         strcatf(args->str, "duration=\"%s\" ", duration);
  772     }
  773     if( bitrate && (args->filter & FILTER_RES_BITRATE) ) {
  774         int br = atoi(bitrate);
  775         if(args->flags & FLAG_MS_PFS)
  776             br /= 8;
  777         strcatf(args->str, "bitrate=\"%d\" ", br);
  778     }
  779     if( sampleFrequency && (args->filter & FILTER_RES_SAMPLEFREQUENCY) ) {
  780         strcatf(args->str, "sampleFrequency=\"%s\" ", sampleFrequency);
  781     }
  782     if( nrAudioChannels && (args->filter & FILTER_RES_NRAUDIOCHANNELS) ) {
  783         strcatf(args->str, "nrAudioChannels=\"%s\" ", nrAudioChannels);
  784     }
  785     if( resolution && (args->filter & FILTER_RES_RESOLUTION) ) {
  786         strcatf(args->str, "resolution=\"%s\" ", resolution);
  787     }
  788     if( args->filter & FILTER_PV_SUBTITLE )
  789     {
  790         if( args->flags & FLAG_HAS_CAPTIONS )
  791         {
  792             if( args->filter & FILTER_PV_SUBTITLE_FILE_TYPE )
  793                 strcatf(args->str, "pv:subtitleFileType=\"SRT\" ");
  794             if( args->filter & FILTER_PV_SUBTITLE_FILE_URI )
  795                 strcatf(args->str, "pv:subtitleFileUri=\"http://%s:%d/Captions/%s.srt\" ",
  796                     lan_addr[args->iface].str, runtime_vars.port, detailID);
  797         }
  798     }
  799     strcatf(args->str, "protocolInfo=\"http-get:*:%s:%s\"&gt;"
  800                               "http://%s:%d/MediaItems/%s.%s"
  801                               "&lt;/res&gt;",
  802                               mime, dlna_pn, lan_addr[args->iface].str,
  803                               runtime_vars.port, detailID, ext);
  804 }
  805 
  806 static int
  807 get_child_count(const char *object, struct magic_container_s *magic)
  808 {
  809     int ret;
  810 
  811     if (magic && magic->child_count)
  812         ret = sql_get_int_field(db, "SELECT count(*) from %s", magic->child_count);
  813     else if (magic && magic->objectid && *(magic->objectid))
  814         ret = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s';", *(magic->objectid));
  815     else
  816         ret = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s';", object);
  817 
  818     return (ret > 0) ? ret : 0;
  819 }
  820 
  821 static int
  822 object_exists(const char *object)
  823 {
  824     int ret;
  825     ret = sql_get_int_field(db, "SELECT count(*) from OBJECTS where OBJECT_ID = '%q'",
  826                 strcmp(object, "*") == 0 ? "0" : object);
  827     return (ret > 0);
  828 }
  829 
  830 #define COLUMNS "o.DETAIL_ID, o.CLASS," \
  831                 " d.SIZE, d.TITLE, d.DURATION, d.BITRATE, d.SAMPLERATE, d.ARTIST," \
  832                 " d.ALBUM, d.GENRE, d.COMMENT, d.CHANNELS, d.TRACK, d.DATE, d.RESOLUTION," \
  833                 " d.THUMBNAIL, d.CREATOR, d.DLNA_PN, d.MIME, d.ALBUM_ART, d.ROTATION, d.DISC "
  834 #define SELECT_COLUMNS "SELECT o.OBJECT_ID, o.PARENT_ID, o.REF_ID, " COLUMNS
  835 
  836 static int
  837 callback(void *args, int argc, char **argv, char **azColName)
  838 {
  839     struct Response *passed_args = (struct Response *)args;
  840     char *id = argv[0], *parent = argv[1], *refID = argv[2], *detailID = argv[3], *class = argv[4], *size = argv[5], *title = argv[6],
  841          *duration = argv[7], *bitrate = argv[8], *sampleFrequency = argv[9], *artist = argv[10], *album = argv[11],
  842          *genre = argv[12], *comment = argv[13], *nrAudioChannels = argv[14], *track = argv[15], *date = argv[16], *resolution = argv[17],
  843          *tn = argv[18], *creator = argv[19], *dlna_pn = argv[20], *mime = argv[21], *album_art = argv[22], *rotate = argv[23], *disc = argv[24];
  844     char dlna_buf[128];
  845     const char *ext;
  846     struct string_s *str = passed_args->str;
  847     int ret = 0;
  848 
  849     /* Make sure we have at least 8KB left of allocated memory to finish the response. */
  850     if( str->off > (str->size - 8192) )
  851     {
  852 #if MAX_RESPONSE_SIZE > 0
  853         if( (str->size+DEFAULT_RESP_SIZE) <= MAX_RESPONSE_SIZE )
  854         {
  855 #endif
  856             str->data = realloc(str->data, (str->size+DEFAULT_RESP_SIZE));
  857             if( str->data )
  858             {
  859                 str->size += DEFAULT_RESP_SIZE;
  860                 DPRINTF(E_DEBUG, L_HTTP, "UPnP SOAP response enlarged to %lu. [%d results so far]\n",
  861                     (unsigned long)str->size, passed_args->returned);
  862             }
  863             else
  864             {
  865                 DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response truncated, realloc failed\n");
  866                 passed_args->flags |= RESPONSE_TRUNCATED;
  867                 return 1;
  868             }
  869 #if MAX_RESPONSE_SIZE > 0
  870         }
  871         else
  872         {
  873             DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response would exceed the max response size [%lld], truncating\n", (long long int)MAX_RESPONSE_SIZE);
  874             passed_args->flags |= RESPONSE_TRUNCATED;
  875             return 1;
  876         }
  877 #endif
  878     }
  879     passed_args->returned++;
  880     passed_args->flags &= ~RESPONSE_FLAGS;
  881 
  882     if( strncmp(class, "item", 4) == 0 )
  883     {
  884         uint32_t dlna_flags = DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_HTTP_STALLING|DLNA_FLAG_TM_B;
  885         char *alt_title = NULL;
  886         /* We may need special handling for certain MIME types */
  887         if( *mime == 'v' )
  888         {
  889             dlna_flags |= DLNA_FLAG_TM_S;
  890             if (GETFLAG(SUBTITLES_MASK) &&
  891                 (passed_args->client >= EStandardDLNA150 || !passed_args->client))
  892                 passed_args->flags |= FLAG_CAPTION_RES;
  893 
  894             if( passed_args->flags & FLAG_MIME_AVI_DIVX )
  895             {
  896                 if( strcmp(mime, "video/x-msvideo") == 0 )
  897                 {
  898                     if( creator )
  899                         strcpy(mime+6, "divx");
  900                     else
  901                         strcpy(mime+6, "avi");
  902                 }
  903             }
  904             else if( passed_args->flags & FLAG_MIME_AVI_AVI )
  905             {
  906                 if( strcmp(mime, "video/x-msvideo") == 0 )
  907                 {
  908                     strcpy(mime+6, "avi");
  909                 }
  910             }
  911             else if( passed_args->client == EFreeBox && dlna_pn )
  912             {
  913                 if( strncmp(dlna_pn, "AVC_TS", 6) == 0 ||
  914                     strncmp(dlna_pn, "MPEG_TS", 7) == 0 )
  915                 {
  916                     strcpy(mime+6, "mp2t");
  917                 }
  918             }
  919             if( !(passed_args->flags & FLAG_DLNA) )
  920             {
  921                 if( strcmp(mime+6, "vnd.dlna.mpeg-tts") == 0 )
  922                 {
  923                     strcpy(mime+6, "mpeg");
  924                 }
  925             }
  926             if( (passed_args->flags & FLAG_CAPTION_RES) ||
  927                 (passed_args->filter & (FILTER_SEC_CAPTION_INFO_EX|FILTER_PV_SUBTITLE)) )
  928             {
  929                 if( sql_get_int_field(db, "SELECT ID from CAPTIONS where ID = '%s'", detailID) > 0 )
  930                     passed_args->flags |= FLAG_HAS_CAPTIONS;
  931             }
  932             /* From what I read, Samsung TV's expect a [wrong] MIME type of x-mkv. */
  933             if( passed_args->flags & FLAG_SAMSUNG )
  934             {
  935                 if( strcmp(mime+6, "x-matroska") == 0 )
  936                 {
  937                     strcpy(mime+8, "mkv");
  938                 }
  939             }
  940             /* LG hack: subtitles won't get used unless dc:title contains a dot. */
  941             else if( passed_args->client == ELGDevice && (passed_args->flags & FLAG_HAS_CAPTIONS) )
  942             {
  943                 ret = asprintf(&alt_title, "%s.", title);
  944                 if( ret > 0 )
  945                     title = alt_title;
  946                 else
  947                     alt_title = NULL;
  948             }
  949             /* Asus OPlay reboots with titles longer than 23 characters with some file types. */
  950             else if( passed_args->client == EAsusOPlay && (passed_args->flags & FLAG_HAS_CAPTIONS) )
  951             {
  952                 if( strlen(title) > 23 )
  953                     title[23] = '\0';
  954             }
  955             /* Hyundai hack: Only titles with a media extension get recognized. */
  956             else if( passed_args->client == EHyundaiTV )
  957             {
  958                 ext = mime_to_ext(mime);
  959                 ret = asprintf(&alt_title, "%s.%s", title, ext);
  960                 if( ret > 0 )
  961                     title = alt_title;
  962                 else
  963                     alt_title = NULL;
  964             }
  965         }
  966         else if( *mime == 'a' )
  967         {
  968             dlna_flags |= DLNA_FLAG_TM_S;
  969             if( strcmp(mime+6, "x-flac") == 0 )
  970             {
  971                 if( passed_args->flags & FLAG_MIME_FLAC_FLAC )
  972                 {
  973                     strcpy(mime+6, "flac");
  974                 }
  975             }
  976             else if( strcmp(mime+6, "x-wav") == 0 )
  977             {
  978                 if( passed_args->flags & FLAG_MIME_WAV_WAV )
  979                 {
  980                     strcpy(mime+6, "wav");
  981                 }
  982             }
  983         }
  984         else
  985             dlna_flags |= DLNA_FLAG_TM_I;
  986         /* Force an alphabetical sort, for clients that like to do their own sorting */
  987         if( GETFLAG(FORCE_ALPHASORT_MASK) )
  988             _alphasort_alt_title(&title, &alt_title, passed_args->requested, passed_args->returned, disc, track);
  989 
  990         if( passed_args->flags & FLAG_SKIP_DLNA_PN )
  991             dlna_pn = NULL;
  992 
  993         if( dlna_pn )
  994             snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_PN=%s;"
  995                                                  "DLNA.ORG_OP=01;"
  996                                                  "DLNA.ORG_CI=0;"
  997                                                  "DLNA.ORG_FLAGS=%08X%024X",
  998                                                  dlna_pn, dlna_flags, 0);
  999         else if( passed_args->flags & FLAG_DLNA )
 1000             snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_OP=01;"
 1001                                                  "DLNA.ORG_CI=0;"
 1002                                                  "DLNA.ORG_FLAGS=%08X%024X",
 1003                                                  dlna_flags, 0);
 1004         else
 1005             strcpy(dlna_buf, "*");
 1006 
 1007         ret = strcatf(str, "&lt;item id=\"%s\" parentID=\"%s\" restricted=\"1\"", id, parent);
 1008         if( refID && (passed_args->filter & FILTER_REFID) ) {
 1009             ret = strcatf(str, " refID=\"%s\"", refID);
 1010         }
 1011         ret = strcatf(str, "&gt;"
 1012                            "&lt;dc:title&gt;%s&lt;/dc:title&gt;"
 1013                            "&lt;upnp:class&gt;object.%s&lt;/upnp:class&gt;",
 1014                            title, class);
 1015         if( comment && (passed_args->filter & FILTER_DC_DESCRIPTION) ) {
 1016             ret = strcatf(str, "&lt;dc:description&gt;%.384s&lt;/dc:description&gt;", comment);
 1017         }
 1018         if( creator && (passed_args->filter & FILTER_DC_CREATOR) ) {
 1019             ret = strcatf(str, "&lt;dc:creator&gt;%s&lt;/dc:creator&gt;", creator);
 1020         }
 1021         if( date && (passed_args->filter & FILTER_DC_DATE) ) {
 1022             ret = strcatf(str, "&lt;dc:date&gt;%s&lt;/dc:date&gt;", date);
 1023         }
 1024         if( (passed_args->filter & FILTER_BOOKMARK_MASK) ) {
 1025             /* Get bookmark */
 1026             int sec = sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID);
 1027             if( sec > 0 ) {
 1028                 /* This format is wrong according to the UPnP/AV spec.  It should be in duration format,
 1029                 ** so HH:MM:SS. But Kodi seems to be the only user of this tag, and it only works with a
 1030                 ** raw seconds value.
 1031                 ** If Kodi gets fixed, we can use duration_str(sec * 1000) here */
 1032                 if( passed_args->flags & FLAG_CONVERT_MS ) {
 1033                     sec *= 1000;
 1034                 }
 1035                 if( passed_args->filter & FILTER_UPNP_LASTPLAYBACKPOSITION )
 1036                     ret = strcatf(str, "&lt;upnp:lastPlaybackPosition&gt;%d&lt;/upnp:lastPlaybackPosition&gt;",
 1037                                   sec);
 1038                 if( passed_args->filter & FILTER_SEC_DCM_INFO )
 1039                     ret = strcatf(str, "&lt;sec:dcmInfo&gt;CREATIONDATE=0,FOLDER=%s,BM=%d&lt;/sec:dcmInfo&gt;",
 1040                                   title, sec);
 1041             }
 1042             if( passed_args->filter & FILTER_UPNP_PLAYBACKCOUNT ) {
 1043                 ret = strcatf(str, "&lt;upnp:playbackCount&gt;%d&lt;/upnp:playbackCount&gt;",
 1044                               sql_get_int_field(db, "SELECT WATCH_COUNT from BOOKMARKS where ID = '%s'", detailID));
 1045             }
 1046         }
 1047         free(alt_title);
 1048         if( artist ) {
 1049             if( (*mime == 'v') && (passed_args->filter & FILTER_UPNP_ACTOR) ) {
 1050                 ret = strcatf(str, "&lt;upnp:actor&gt;%s&lt;/upnp:actor&gt;", artist);
 1051             }
 1052             if( passed_args->filter & FILTER_UPNP_ARTIST ) {
 1053                 ret = strcatf(str, "&lt;upnp:artist&gt;%s&lt;/upnp:artist&gt;", artist);
 1054             }
 1055         }
 1056         if( album && (passed_args->filter & FILTER_UPNP_ALBUM) ) {
 1057             ret = strcatf(str, "&lt;upnp:album&gt;%s&lt;/upnp:album&gt;", album);
 1058         }
 1059         if( genre && (passed_args->filter & FILTER_UPNP_GENRE) ) {
 1060             ret = strcatf(str, "&lt;upnp:genre&gt;%s&lt;/upnp:genre&gt;", genre);
 1061         }
 1062         if( strncmp(id, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 ) {
 1063             track = strrchr(id, '$')+1;
 1064         }
 1065         if( NON_ZERO(track) ) {
 1066             if( *mime == 'a' && (passed_args->filter & FILTER_UPNP_ORIGINALTRACKNUMBER) ) {
 1067                 ret = strcatf(str, "&lt;upnp:originalTrackNumber&gt;%s&lt;/upnp:originalTrackNumber&gt;", track);
 1068             } else if( *mime == 'v' ) {
 1069                 if( NON_ZERO(disc) && (passed_args->filter & FILTER_UPNP_EPISODESEASON) )
 1070                     ret = strcatf(str, "&lt;upnp:episodeSeason&gt;%s&lt;/upnp:episodeSeason&gt;", disc);
 1071                 if( passed_args->filter & FILTER_UPNP_EPISODENUMBER )
 1072                     ret = strcatf(str, "&lt;upnp:episodeNumber&gt;%s&lt;/upnp:episodeNumber&gt;", track);
 1073             }
 1074         }
 1075         if( passed_args->filter & FILTER_RES ) {
 1076             ext = mime_to_ext(mime);
 1077             add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1078                     resolution, dlna_buf, mime, detailID, ext, passed_args);
 1079             if( *mime == 'i' ) {
 1080                 int srcw, srch;
 1081                 if( resolution && (sscanf(resolution, "%6dx%6d", &srcw, &srch) == 2) )
 1082                 {
 1083                     if( srcw > 4096 || srch > 4096 )
 1084                         add_resized_res(srcw, srch, 4096, 4096, "JPEG_LRG", detailID, passed_args);
 1085                     if( srcw > 1024 || srch > 768 )
 1086                         add_resized_res(srcw, srch, 1024, 768, "JPEG_MED", detailID, passed_args);
 1087                     if( srcw > 640 || srch > 480 )
 1088                         add_resized_res(srcw, srch, 640, 480, "JPEG_SM", detailID, passed_args);
 1089                 }
 1090                 if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) {
 1091                     ret = strcatf(str, "&lt;res protocolInfo=\"http-get:*:%s:%s\"&gt;"
 1092                                        "http://%s:%d/Thumbnails/%s.jpg"
 1093                                        "&lt;/res&gt;",
 1094                                        mime, "DLNA.ORG_PN=JPEG_TN;DLNA.ORG_CI=1", lan_addr[passed_args->iface].str,
 1095                                        runtime_vars.port, detailID);
 1096                 }
 1097                 else
 1098                     add_resized_res(srcw, srch, 160, 160, "JPEG_TN", detailID, passed_args);
 1099             }
 1100             else if( *mime == 'v' ) {
 1101                 switch( passed_args->client ) {
 1102                 case EToshibaTV:
 1103                     if( dlna_pn &&
 1104                         (strncmp(dlna_pn, "MPEG_TS_HD_NA", 13) == 0 ||
 1105                          strncmp(dlna_pn, "MPEG_TS_SD_NA", 13) == 0 ||
 1106                          strncmp(dlna_pn, "AVC_TS_MP_HD_AC3", 16) == 0 ||
 1107                          strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0))
 1108                     {
 1109                         sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_NTSC");
 1110                         add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1111                                 resolution, dlna_buf, mime, detailID, ext, passed_args);
 1112                     }
 1113                     break;
 1114                 case ESonyBDP:
 1115                     if( dlna_pn &&
 1116                         (strncmp(dlna_pn, "AVC_TS", 6) == 0 ||
 1117                          strncmp(dlna_pn, "MPEG_TS", 7) == 0) )
 1118                     {
 1119                         if( strncmp(dlna_pn, "MPEG_TS_SD_NA", 13) != 0 )
 1120                         {
 1121                             sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_TS_SD_NA");
 1122                             add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1123                                     resolution, dlna_buf, mime, detailID, ext, passed_args);
 1124                         }
 1125                         if( strncmp(dlna_pn, "MPEG_TS_SD_EU", 13) != 0 )
 1126                         {
 1127                             sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_TS_SD_EU");
 1128                             add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1129                                     resolution, dlna_buf, mime, detailID, ext, passed_args);
 1130                         }
 1131                     }
 1132                     else if( (dlna_pn &&
 1133                               (strncmp(dlna_pn, "AVC_MP4", 7) == 0 ||
 1134                                strncmp(dlna_pn, "MPEG4_P2_MP4", 12) == 0)) ||
 1135                              strcmp(mime+6, "x-matroska") == 0 ||
 1136                              strcmp(mime+6, "x-msvideo") == 0 ||
 1137                              strcmp(mime+6, "mpeg") == 0 )
 1138                     {
 1139                         strcpy(mime+6, "avi");
 1140                         if( !dlna_pn || strncmp(dlna_pn, "MPEG_PS_NTSC", 12) != 0 )
 1141                         {
 1142                             sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_NTSC");
 1143                             add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1144                                     resolution, dlna_buf, mime, detailID, ext, passed_args);
 1145                         }
 1146                         if( !dlna_pn || strncmp(dlna_pn, "MPEG_PS_PAL", 11) != 0 )
 1147                         {
 1148                             sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_PAL");
 1149                             add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1150                                     resolution, dlna_buf, mime, detailID, ext, passed_args);
 1151                         }
 1152                     }
 1153                     break;
 1154                 case ESonyBravia:
 1155                     /* BRAVIA KDL-##*X### series TVs do natively support AVC/AC3 in TS, but
 1156                        require profile to be renamed (applies to _T and _ISO variants also) */
 1157                     if( dlna_pn &&
 1158                         (strncmp(dlna_pn, "AVC_TS_MP_SD_AC3", 16) == 0 ||
 1159                          strncmp(dlna_pn, "AVC_TS_MP_HD_AC3", 16) == 0 ||
 1160                          strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0))
 1161                     {
 1162                             sprintf(dlna_buf, "DLNA.ORG_PN=AVC_TS_HD_50_AC3%s", dlna_pn + 16);
 1163                         add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
 1164                                 resolution, dlna_buf, mime, detailID, ext, passed_args);
 1165                     }
 1166                     break;
 1167                 case ESamsungSeriesCDE:
 1168                 case ELGDevice:
 1169                 case ELGNetCastDevice:
 1170                 case EAsusOPlay:
 1171                 default:
 1172                     if( passed_args->flags & FLAG_HAS_CAPTIONS )
 1173                     {
 1174                         if( passed_args->flags & FLAG_CAPTION_RES )
 1175                             ret = strcatf(str, "&lt;res protocolInfo=\"http-get:*:text/srt:*\"&gt;"
 1176                                          "http://%s:%d/Captions/%s.srt"
 1177                                        "&lt;/res&gt;",
 1178                                        lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
 1179                         if( passed_args->filter & FILTER_SEC_CAPTION_INFO_EX )
 1180                             ret = strcatf(str, "&lt;sec:CaptionInfoEx sec:type=\"srt\"&gt;"
 1181                                                  "http://%s:%d/Captions/%s.srt"
 1182                                                "&lt;/sec:CaptionInfoEx&gt;",
 1183                                                lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
 1184                     }
 1185                     break;
 1186                 }
 1187             }
 1188         }
 1189         if( NON_ZERO(album_art) )
 1190         {
 1191             /* Video and audio album art is handled differently */
 1192             if( *mime == 'v' && (passed_args->filter & FILTER_RES) && !(passed_args->flags & FLAG_MS_PFS) ) {
 1193                 ret = strcatf(str, "&lt;res protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN\"&gt;"
 1194                                    "http://%s:%d/AlbumArt/%s-%s.jpg"
 1195                                    "&lt;/res&gt;",
 1196                                    lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
 1197                 if (passed_args->client == ESamsungSeriesCDE ) {
 1198                     ret = strcatf(str, "&lt;res dlna:profileID=\"JPEG_SM\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\""
 1199                                " protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;"
 1200                                "DLNA.ORG_OP=01;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=%08X%024X\" resolution=\"320x320\"&gt;"
 1201                                "http://%s:%d/AlbumArt/%s-%s.jpg"
 1202                                "&lt;/res&gt;",
 1203                                DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_TM_B|DLNA_FLAG_TM_I, 0,
 1204                                lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
 1205                 }
 1206             } else if( passed_args->filter & FILTER_UPNP_ALBUMARTURI ) {
 1207                 ret = strcatf(str, "&lt;upnp:albumArtURI");
 1208                 if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) {
 1209                     ret = strcatf(str, " dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"");
 1210                 }
 1211                 ret = strcatf(str, "&gt;http://%s:%d/AlbumArt/%s-%s.jpg&lt;/upnp:albumArtURI&gt;",
 1212                                    lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
 1213             }
 1214         }
 1215         if( (passed_args->flags & FLAG_MS_PFS) && *mime == 'i' ) {
 1216             if( passed_args->client == EMediaRoom && !album )
 1217                 ret = strcatf(str, "&lt;upnp:album&gt;%s&lt;/upnp:album&gt;", "[No Keywords]");
 1218 
 1219             /* EVA2000 doesn't seem to handle embedded thumbnails */
 1220             if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) {
 1221                 ret = strcatf(str, "&lt;upnp:albumArtURI&gt;"
 1222                                    "http://%s:%d/Thumbnails/%s.jpg"
 1223                                    "&lt;/upnp:albumArtURI&gt;",
 1224                                    lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
 1225             } else {
 1226                 ret = strcatf(str, "&lt;upnp:albumArtURI&gt;"
 1227                                    "http://%s:%d/Resized/%s.jpg?width=160,height=160"
 1228                                    "&lt;/upnp:albumArtURI&gt;",
 1229                                    lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
 1230             }
 1231         }
 1232         ret = strcatf(str, "&lt;/item&gt;");
 1233     }
 1234     else if( strncmp(class, "container", 9) == 0 )
 1235     {
 1236         ret = strcatf(str, "&lt;container id=\"%s\" parentID=\"%s\" restricted=\"1\" ", id, parent);
 1237         if( passed_args->filter & FILTER_SEARCHABLE ) {
 1238             ret = strcatf(str, "searchable=\"%d\" ", check_magic_container(id, passed_args->flags) ? 0 : 1);
 1239         }
 1240         if( passed_args->filter & FILTER_CHILDCOUNT ) {
 1241             ret = strcatf(str, "childCount=\"%d\"", get_child_count(id, check_magic_container(id, passed_args->flags)));
 1242         }
 1243         /* If the client calls for BrowseMetadata on root, we have to include our "upnp:searchClass"'s, unless they're filtered out */
 1244         if( passed_args->requested == 1 && strcmp(id, "0") == 0 && (passed_args->filter & FILTER_UPNP_SEARCHCLASS) ) {
 1245             ret = strcatf(str, "&gt;"
 1246                                "&lt;upnp:searchClass includeDerived=\"1\"&gt;object.item.audioItem&lt;/upnp:searchClass&gt;"
 1247                                "&lt;upnp:searchClass includeDerived=\"1\"&gt;object.item.imageItem&lt;/upnp:searchClass&gt;"
 1248                                "&lt;upnp:searchClass includeDerived=\"1\"&gt;object.item.videoItem&lt;/upnp:searchClass");
 1249         }
 1250         ret = strcatf(str, "&gt;"
 1251                            "&lt;dc:title&gt;%s&lt;/dc:title&gt;"
 1252                            "&lt;upnp:class&gt;object.%s&lt;/upnp:class&gt;",
 1253                            title, class);
 1254         if( (passed_args->filter & FILTER_UPNP_STORAGEUSED) || strcmp(class+10, "storageFolder") == 0 ) {
 1255             /* TODO: Implement real folder size tracking */
 1256             ret = strcatf(str, "&lt;upnp:storageUsed&gt;%s&lt;/upnp:storageUsed&gt;", (size ? size : "-1"));
 1257         }
 1258         if( creator && (passed_args->filter & FILTER_DC_CREATOR) ) {
 1259             ret = strcatf(str, "&lt;dc:creator&gt;%s&lt;/dc:creator&gt;", creator);
 1260         }
 1261         if( genre && (passed_args->filter & FILTER_UPNP_GENRE) ) {
 1262             ret = strcatf(str, "&lt;upnp:genre&gt;%s&lt;/upnp:genre&gt;", genre);
 1263         }
 1264         if( artist && (passed_args->filter & FILTER_UPNP_ARTIST) ) {
 1265             ret = strcatf(str, "&lt;upnp:artist&gt;%s&lt;/upnp:artist&gt;", artist);
 1266         }
 1267         if( NON_ZERO(album_art) && (passed_args->filter & FILTER_UPNP_ALBUMARTURI) ) {
 1268             ret = strcatf(str, "&lt;upnp:albumArtURI ");
 1269             if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) {
 1270                 ret = strcatf(str, "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"");
 1271             }
 1272             ret = strcatf(str, "&gt;http://%s:%d/AlbumArt/%s-%s.jpg&lt;/upnp:albumArtURI&gt;",
 1273                                lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
 1274         }
 1275         if( passed_args->filter & FILTER_AV_MEDIA_CLASS ) {
 1276             char class;
 1277             if( strncmp(id, MUSIC_ID, sizeof(MUSIC_ID)) == 0 )
 1278                 class = 'M';
 1279             else if( strncmp(id, VIDEO_ID, sizeof(VIDEO_ID)) == 0 )
 1280                 class = 'V';
 1281             else if( strncmp(id, IMAGE_ID, sizeof(IMAGE_ID)) == 0 )
 1282                 class = 'P';
 1283             else
 1284                 class = 0;
 1285             if( class )
 1286                 ret = strcatf(str, "&lt;av:mediaClass xmlns:av=\"urn:schemas-sony-com:av\"&gt;"
 1287                                     "%c&lt;/av:mediaClass&gt;", class);
 1288         }
 1289         ret = strcatf(str, "&lt;/container&gt;");
 1290     }
 1291 
 1292     return 0;
 1293 }
 1294 
 1295 static void
 1296 BrowseContentDirectory(struct upnphttp * h, const char * action)
 1297 {
 1298     static const char resp0[] =
 1299             "<u:BrowseResponse "
 1300             "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
 1301             "<Result>"
 1302             "&lt;DIDL-Lite"
 1303             CONTENT_DIRECTORY_SCHEMAS;
 1304     struct magic_container_s *magic;
 1305     char *zErrMsg = NULL;
 1306     char *sql, *ptr;
 1307     struct Response args;
 1308     struct string_s str;
 1309     int totalMatches = 0;
 1310     int ret;
 1311     const char *ObjectID, *BrowseFlag;
 1312     char *Filter, *SortCriteria;
 1313     const char *objectid_sql = "o.OBJECT_ID";
 1314     const char *parentid_sql = "o.PARENT_ID";
 1315     const char *refid_sql = "o.REF_ID";
 1316     char where[256] = "";
 1317     char *orderBy = NULL;
 1318     struct NameValueParserData data;
 1319     int RequestedCount = 0;
 1320     int StartingIndex = 0;
 1321 
 1322     memset(&args, 0, sizeof(args));
 1323     memset(&str, 0, sizeof(str));
 1324 
 1325     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
 1326 
 1327     ObjectID = GetValueFromNameValueList(&data, "ObjectID");
 1328     Filter = GetValueFromNameValueList(&data, "Filter");
 1329     BrowseFlag = GetValueFromNameValueList(&data, "BrowseFlag");
 1330     SortCriteria = GetValueFromNameValueList(&data, "SortCriteria");
 1331 
 1332     if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) )
 1333         RequestedCount = atoi(ptr);
 1334     if( RequestedCount < 0 )
 1335     {
 1336         SoapError(h, 402, "Invalid Args");
 1337         goto browse_error;
 1338     }
 1339     if( !RequestedCount )
 1340         RequestedCount = -1;
 1341     if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) )
 1342         StartingIndex = atoi(ptr);
 1343     if( StartingIndex < 0 )
 1344     {
 1345         SoapError(h, 402, "Invalid Args");
 1346         goto browse_error;
 1347     }
 1348     if( !BrowseFlag || (strcmp(BrowseFlag, "BrowseDirectChildren") && strcmp(BrowseFlag, "BrowseMetadata")) )
 1349     {
 1350         SoapError(h, 402, "Invalid Args");
 1351         goto browse_error;
 1352     }
 1353     if( !ObjectID && !(ObjectID = GetValueFromNameValueList(&data, "ContainerID")) )
 1354     {
 1355         SoapError(h, 402, "Invalid Args");
 1356         goto browse_error;
 1357     }
 1358 
 1359     str.data = malloc(DEFAULT_RESP_SIZE);
 1360     str.size = DEFAULT_RESP_SIZE;
 1361     str.off = sprintf(str.data, "%s", resp0);
 1362     /* See if we need to include DLNA namespace reference */
 1363     args.iface = h->iface;
 1364     args.filter = set_filter_flags(Filter, h);
 1365     if( args.filter & FILTER_DLNA_NAMESPACE )
 1366         ret = strcatf(&str, DLNA_NAMESPACE);
 1367     if( args.filter & FILTER_PV_SUBTITLE )
 1368         ret = strcatf(&str, PV_NAMESPACE);
 1369     if( args.filter & FILTER_SEC )
 1370         ret = strcatf(&str, SEC_NAMESPACE);
 1371     strcatf(&str, "&gt;\n");
 1372 
 1373     args.returned = 0;
 1374     args.requested = RequestedCount;
 1375     args.client = h->req_client ? h->req_client->type->type : 0;
 1376     args.flags = h->req_client ? h->req_client->type->flags : 0;
 1377     args.str = &str;
 1378     DPRINTF(E_DEBUG, L_HTTP, "Browsing ContentDirectory:\n"
 1379                              " * ObjectID: %s\n"
 1380                              " * Count: %d\n"
 1381                              " * StartingIndex: %d\n"
 1382                              " * BrowseFlag: %s\n"
 1383                              " * Filter: %s\n"
 1384                              " * SortCriteria: %s\n",
 1385                 ObjectID, RequestedCount, StartingIndex,
 1386                             BrowseFlag, Filter, SortCriteria);
 1387 
 1388     if( strcmp(BrowseFlag+6, "Metadata") == 0 )
 1389     {
 1390         const char *id = ObjectID;
 1391         args.requested = 1;
 1392         magic = in_magic_container(ObjectID, args.flags, &id);
 1393         if (magic)
 1394         {
 1395             if (magic->objectid_sql && strcmp(id, ObjectID) != 0)
 1396                 objectid_sql = magic->objectid_sql;
 1397             if (magic->parentid_sql && strcmp(id, ObjectID) != 0)
 1398                 parentid_sql = magic->parentid_sql;
 1399             if (magic->refid_sql)
 1400                 refid_sql = magic->refid_sql;
 1401         }
 1402         sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS
 1403                       "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
 1404                       " where OBJECT_ID = '%q';",
 1405                       objectid_sql, parentid_sql, refid_sql, id);
 1406         ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
 1407         totalMatches = args.returned;
 1408     }
 1409     else
 1410     {
 1411         magic = check_magic_container(ObjectID, args.flags);
 1412         if (magic)
 1413         {
 1414             if (magic->objectid && *(magic->objectid))
 1415                 ObjectID = *(magic->objectid);
 1416             if (magic->objectid_sql)
 1417                 objectid_sql = magic->objectid_sql;
 1418             if (magic->parentid_sql)
 1419                 parentid_sql = magic->parentid_sql;
 1420             if (magic->refid_sql)
 1421                 refid_sql = magic->refid_sql;
 1422             if (magic->where)
 1423                 strncpyt(where, magic->where, sizeof(where));
 1424             if (magic->orderby && !GETFLAG(DLNA_STRICT_MASK))
 1425                 orderBy = strdup(magic->orderby);
 1426             if (magic->max_count > 0)
 1427             {
 1428                 int limit = MAX(magic->max_count - StartingIndex, 0);
 1429                 ret = get_child_count(ObjectID, magic);
 1430                 totalMatches = MIN(ret, limit);
 1431                 if (RequestedCount > limit || RequestedCount < 0)
 1432                     RequestedCount = limit;
 1433             }
 1434         }
 1435         if (!where[0])
 1436             sqlite3_snprintf(sizeof(where), where, "PARENT_ID = '%q'", ObjectID);
 1437 
 1438         if (!totalMatches)
 1439             totalMatches = get_child_count(ObjectID, magic);
 1440         ret = 0;
 1441         if (SortCriteria && !orderBy)
 1442         {
 1443             __SORT_LIMIT
 1444             orderBy = parse_sort_criteria(SortCriteria, &ret);
 1445         }
 1446         else if (!orderBy)
 1447         {
 1448             if( strncmp(ObjectID, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 )
 1449             {
 1450                 if( strcmp(ObjectID, MUSIC_PLIST_ID) == 0 )
 1451                     ret = xasprintf(&orderBy, "order by d.TITLE");
 1452                 else
 1453                     ret = xasprintf(&orderBy, "order by length(OBJECT_ID), OBJECT_ID");
 1454             }
 1455             else if( args.flags & FLAG_FORCE_SORT )
 1456             {
 1457                 __SORT_LIMIT
 1458                 ret = xasprintf(&orderBy, "order by o.CLASS, d.DISC, d.TRACK, d.TITLE");
 1459             }
 1460             /* LG TV ordering bug */
 1461             else if( args.client == ELGDevice )
 1462                 ret = xasprintf(&orderBy, "order by o.CLASS, d.TITLE");
 1463             else
 1464                 orderBy = parse_sort_criteria(SortCriteria, &ret);
 1465             if( ret == -1 )
 1466             {
 1467                 free(orderBy);
 1468                 orderBy = NULL;
 1469                 ret = 0;
 1470             }
 1471         }
 1472         /* If it's a DLNA client, return an error for bad sort criteria */
 1473         if( ret < 0 && ((args.flags & FLAG_DLNA) || GETFLAG(DLNA_STRICT_MASK)) )
 1474         {
 1475             SoapError(h, 709, "Unsupported or invalid sort criteria");
 1476             goto browse_error;
 1477         }
 1478 
 1479         sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS
 1480                       "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
 1481                       " where %s %s limit %d, %d;",
 1482                       objectid_sql, parentid_sql, refid_sql,
 1483                       where, THISORNUL(orderBy), StartingIndex, RequestedCount);
 1484         DPRINTF(E_DEBUG, L_HTTP, "Browse SQL: %s\n", sql);
 1485         ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
 1486     }
 1487     if( ret != SQLITE_OK )
 1488     {
 1489         if( args.flags & RESPONSE_TRUNCATED )
 1490         {
 1491             sqlite3_free(zErrMsg);
 1492         }
 1493         else
 1494         {
 1495             DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql);
 1496             sqlite3_free(zErrMsg);
 1497             SoapError(h, 709, "Unsupported or invalid sort criteria");
 1498             goto browse_error;
 1499         }
 1500     }
 1501     sqlite3_free(sql);
 1502     /* Does the object even exist? */
 1503     if( !totalMatches )
 1504     {
 1505         if( !object_exists(ObjectID) )
 1506         {
 1507             SoapError(h, 701, "No such object error");
 1508             goto browse_error;
 1509         }
 1510     }
 1511     ret = strcatf(&str, "&lt;/DIDL-Lite&gt;</Result>\n"
 1512                         "<NumberReturned>%u</NumberReturned>\n"
 1513                         "<TotalMatches>%u</TotalMatches>\n"
 1514                         "<UpdateID>%u</UpdateID>"
 1515                         "</u:BrowseResponse>",
 1516                         args.returned, totalMatches, updateID);
 1517     BuildSendAndCloseSoapResp(h, str.data, str.off);
 1518 browse_error:
 1519     ClearNameValueList(&data);
 1520     free(orderBy);
 1521     free(str.data);
 1522 }
 1523 
 1524 static inline void
 1525 charcat(struct string_s *str, char c)
 1526 {
 1527     if (str->size <= str->off)
 1528     {
 1529         str->data[str->size-1] = '\0';
 1530         return;
 1531     }
 1532     str->data[str->off] = c;
 1533     str->off += 1;
 1534 }
 1535 
 1536 static inline char *
 1537 parse_search_criteria(const char *str, char *sep)
 1538 {
 1539     struct string_s criteria;
 1540     int len;
 1541     int literal = 0, like = 0, class = 0;
 1542     const char *s;
 1543 
 1544     if (!str)
 1545         return strdup("1 = 1");
 1546 
 1547     len = strlen(str) + 32;
 1548     criteria.data = malloc(len);
 1549     criteria.size = len;
 1550     criteria.off = 0;
 1551 
 1552     s = str;
 1553 
 1554     while (isspace(*s))
 1555         s++;
 1556 
 1557     while (*s)
 1558     {
 1559         if (literal)
 1560         {
 1561             switch (*s) {
 1562             case '&':
 1563                 if (strncmp(s, "&quot;", 6) == 0)
 1564                     s += 5;
 1565                 else if (strncmp(s, "&apos;", 6) == 0)
 1566                 {
 1567                     strcatf(&criteria, "'");
 1568                     s += 6;
 1569                     continue;
 1570                 }
 1571                 else
 1572                     break;
 1573             case '"':
 1574                 literal = 0;
 1575                 if (like)
 1576                 {
 1577                     charcat(&criteria, '%');
 1578                     like--;
 1579                 }
 1580                 charcat(&criteria, '"');
 1581                 break;
 1582             case '\\':
 1583                 if (strncmp(s, "\\&quot;", 7) == 0)
 1584                 {
 1585                     strcatf(&criteria, "&amp;quot;");
 1586                     s += 7;
 1587                     continue;
 1588                 }
 1589                 break;
 1590             case 'o':
 1591                 if (class)
 1592                 {
 1593                     class = 0;
 1594                     if (strncmp(s, "object.", 7) == 0)
 1595                         s += 7;
 1596                     else if (strncmp(s, "object\"", 7) == 0 ||
 1597                              strncmp(s, "object&quot;", 12) == 0)
 1598                     {
 1599                         s += 6;
 1600                         continue;
 1601                     }
 1602                 }
 1603             default:
 1604                 charcat(&criteria, *s);
 1605                 break;
 1606             }
 1607         }
 1608         else
 1609         {
 1610             switch (*s) {
 1611             case '\\':
 1612                 if (strncmp(s, "\\&quot;", 7) == 0)
 1613                 {
 1614                     strcatf(&criteria, "&amp;quot;");
 1615                     s += 7;
 1616                     continue;
 1617                 }
 1618                 else
 1619                     charcat(&criteria, *s);
 1620                 break;
 1621             case '"':
 1622                 literal = 1;
 1623                 charcat(&criteria, *s);
 1624                 if (like == 2)
 1625                 {
 1626                     charcat(&criteria, '%');
 1627                     like--;
 1628                 }
 1629                 break;
 1630             case '&':
 1631                 if (strncmp(s, "&quot;", 6) == 0)
 1632                 {
 1633                     literal = 1;
 1634                     strcatf(&criteria, "\"");
 1635                     if (like == 2)
 1636                     {
 1637                         charcat(&criteria, '%');
 1638                         like--;
 1639                     }
 1640                     s += 5;
 1641                 }
 1642                 else if (strncmp(s, "&apos;", 6) == 0)
 1643                 {
 1644                     strcatf(&criteria, "'");
 1645                     s += 5;
 1646                 }
 1647                 else if (strncmp(s, "&lt;", 4) == 0)
 1648                 {
 1649                     strcatf(&criteria, "<");
 1650                     s += 3;
 1651                 }
 1652                 else if (strncmp(s, "&gt;", 4) == 0)
 1653                 {
 1654                     strcatf(&criteria, ">");
 1655                     s += 3;
 1656                 }
 1657                 else
 1658                     charcat(&criteria, *s);
 1659                 break;
 1660             case '@':
 1661                 if (strncmp(s, "@refID", 6) == 0)
 1662                 {
 1663                     strcatf(&criteria, "REF_ID");
 1664                     s += 6;
 1665                     continue;
 1666                 }
 1667                 else if (strncmp(s, "@id", 3) == 0)
 1668                 {
 1669                     strcatf(&criteria, "OBJECT_ID");
 1670                     s += 3;
 1671                     continue;
 1672                 }
 1673                 else if (strncmp(s, "@parentID", 9) == 0)
 1674                 {
 1675                     strcatf(&criteria, "PARENT_ID");
 1676                     s += 9;
 1677                     strcpy(sep, "*");
 1678                     continue;
 1679                 }
 1680                 else
 1681                     charcat(&criteria, *s);
 1682                 break;
 1683             case 'c':
 1684                 if (strncmp(s, "contains", 8) == 0)
 1685                 {
 1686                     strcatf(&criteria, "like");
 1687                     s += 8;
 1688                     like = 2;
 1689                     continue;
 1690                 }
 1691                 else
 1692                     charcat(&criteria, *s);
 1693                 break;
 1694             case 'd':
 1695                 if (strncmp(s, "derivedfrom", 11) == 0)
 1696                 {
 1697                     strcatf(&criteria, "like");
 1698                     s += 11;
 1699                     like = 1;
 1700                     continue;
 1701                 }
 1702                 else if (strncmp(s, "dc:date", 7) == 0)
 1703                 {
 1704                     strcatf(&criteria, "d.DATE");
 1705                     s += 7;
 1706                     continue;
 1707                 }
 1708                 else if (strncmp(s, "dc:title", 8) == 0)
 1709                 {
 1710                     strcatf(&criteria, "d.TITLE");
 1711                     s += 8;
 1712                     continue;
 1713                 }
 1714                 else if (strncmp(s, "dc:creator", 10) == 0)
 1715                 {
 1716                     strcatf(&criteria, "d.CREATOR");
 1717                     s += 10;
 1718                     continue;
 1719                 }
 1720                 else
 1721                     charcat(&criteria, *s);
 1722                 break;
 1723             case 'e':
 1724                 if (strncmp(s, "exists", 6) == 0)
 1725                 {
 1726                     s += 6;
 1727                     while (isspace(*s))
 1728                         s++;
 1729                     if (strncmp(s, "true", 4) == 0)
 1730                     {
 1731                         strcatf(&criteria, "is not NULL");
 1732                         s += 3;
 1733                     }
 1734                     else if (strncmp(s, "false", 5) == 0)
 1735                     {
 1736                         strcatf(&criteria, "is NULL");
 1737                         s += 4;
 1738                     }
 1739                 }
 1740                 else
 1741                     charcat(&criteria, *s);
 1742                 break;
 1743             case 'o':
 1744                 if (class)
 1745                 {
 1746                     if (strncmp(s, "object.", 7) == 0)
 1747                     {
 1748                         s += 7;
 1749                         charcat(&criteria, '"');
 1750                         while (*s && !isspace(*s))
 1751                         {
 1752                             charcat(&criteria, *s);
 1753                             s++;
 1754                         }
 1755                         charcat(&criteria, '"');
 1756                     }
 1757                     class = 0;
 1758                     continue;
 1759                 }
 1760             case 'u':
 1761                 if (strncmp(s, "upnp:class", 10) == 0)
 1762                 {
 1763                     strcatf(&criteria, "o.CLASS");
 1764                     s += 10;
 1765                     class = 1;
 1766                     continue;
 1767                 }
 1768                 else if (strncmp(s, "upnp:actor", 10) == 0)
 1769                 {
 1770                     strcatf(&criteria, "d.ARTIST");
 1771                     s += 10;
 1772                     continue;
 1773                 }
 1774                 else if (strncmp(s, "upnp:artist", 11) == 0)
 1775                 {
 1776                     strcatf(&criteria, "d.ARTIST");
 1777                     s += 11;
 1778                     continue;
 1779                 }
 1780                 else if (strncmp(s, "upnp:album", 10) == 0)
 1781                 {
 1782                     strcatf(&criteria, "d.ALBUM");
 1783                     s += 10;
 1784                     continue;
 1785                 }
 1786                 else if (strncmp(s, "upnp:genre", 10) == 0)
 1787                 {
 1788                     strcatf(&criteria, "d.GENRE");
 1789                     s += 10;
 1790                     continue;
 1791                 }
 1792                 else
 1793                     charcat(&criteria, *s);
 1794                 break;
 1795             case '(':
 1796                 if (s > str && !isspace(s[-1]))
 1797                     charcat(&criteria, ' ');
 1798                 charcat(&criteria, *s);
 1799                 break;
 1800             case ')':
 1801                 charcat(&criteria, *s);
 1802                 if (!isspace(s[1]))
 1803                     charcat(&criteria, ' ');
 1804                 break;
 1805             default:
 1806                 charcat(&criteria, *s);
 1807                 break;
 1808             }
 1809         }
 1810         s++;
 1811     }
 1812     charcat(&criteria, '\0');
 1813 
 1814     return criteria.data;
 1815 }
 1816 
 1817 static void
 1818 SearchContentDirectory(struct upnphttp * h, const char * action)
 1819 {
 1820     static const char resp0[] =
 1821             "<u:SearchResponse "
 1822             "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
 1823             "<Result>"
 1824             "&lt;DIDL-Lite"
 1825             CONTENT_DIRECTORY_SCHEMAS;
 1826     struct magic_container_s *magic;
 1827     char *zErrMsg = NULL;
 1828     char *sql, *ptr;
 1829     struct Response args;
 1830     struct string_s str;
 1831     int totalMatches;
 1832     int ret;
 1833     const char *ContainerID;
 1834     char *Filter, *SearchCriteria, *SortCriteria;
 1835     char *orderBy = NULL, *where = NULL, sep[] = "$*";
 1836     char groupBy[] = "group by DETAIL_ID";
 1837     struct NameValueParserData data;
 1838     int RequestedCount = 0;
 1839     int StartingIndex = 0;
 1840 
 1841     memset(&args, 0, sizeof(args));
 1842     memset(&str, 0, sizeof(str));
 1843 
 1844     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
 1845 
 1846     ContainerID = GetValueFromNameValueList(&data, "ContainerID");
 1847     Filter = GetValueFromNameValueList(&data, "Filter");
 1848     SearchCriteria = GetValueFromNameValueList(&data, "SearchCriteria");
 1849     SortCriteria = GetValueFromNameValueList(&data, "SortCriteria");
 1850 
 1851     if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) )
 1852         RequestedCount = atoi(ptr);
 1853     if( !RequestedCount )
 1854         RequestedCount = -1;
 1855     if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) )
 1856         StartingIndex = atoi(ptr);
 1857     if( !ContainerID )
 1858     {
 1859         if( !(ContainerID = GetValueFromNameValueList(&data, "ObjectID")) )
 1860         {
 1861             SoapError(h, 402, "Invalid Args");
 1862             goto search_error;
 1863         }
 1864     }
 1865 
 1866     str.data = malloc(DEFAULT_RESP_SIZE);
 1867     str.size = DEFAULT_RESP_SIZE;
 1868     str.off = sprintf(str.data, "%s", resp0);
 1869     /* See if we need to include DLNA namespace reference */
 1870     args.iface = h->iface;
 1871     args.filter = set_filter_flags(Filter, h);
 1872     if( args.filter & FILTER_DLNA_NAMESPACE )
 1873     {
 1874         ret = strcatf(&str, DLNA_NAMESPACE);
 1875     }
 1876     strcatf(&str, "&gt;\n");
 1877 
 1878     args.returned = 0;
 1879     args.requested = RequestedCount;
 1880     args.client = h->req_client ? h->req_client->type->type : 0;
 1881     args.flags = h->req_client ? h->req_client->type->flags : 0;
 1882     args.str = &str;
 1883     DPRINTF(E_DEBUG, L_HTTP, "Searching ContentDirectory:\n"
 1884                              " * ObjectID: %s\n"
 1885                              " * Count: %d\n"
 1886                              " * StartingIndex: %d\n"
 1887                              " * SearchCriteria: %s\n"
 1888                              " * Filter: %s\n"
 1889                              " * SortCriteria: %s\n",
 1890                 ContainerID, RequestedCount, StartingIndex,
 1891                             SearchCriteria, Filter, SortCriteria);
 1892 
 1893     magic = check_magic_container(ContainerID, args.flags);
 1894     if (magic && magic->objectid && *(magic->objectid))
 1895         ContainerID = *(magic->objectid);
 1896 
 1897     if( strcmp(ContainerID, "0") == 0 )
 1898         ContainerID = "*";
 1899 
 1900     if( strcmp(ContainerID, MUSIC_ALL_ID) == 0 ||
 1901         GETFLAG(DLNA_STRICT_MASK) )
 1902         groupBy[0] = '\0';
 1903 
 1904     where = parse_search_criteria(SearchCriteria, sep);
 1905     DPRINTF(E_DEBUG, L_HTTP, "Translated SearchCriteria: %s\n", where);
 1906 
 1907     totalMatches = sql_get_int_field(db, "SELECT (select count(distinct DETAIL_ID)"
 1908                                          " from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
 1909                                          " where (OBJECT_ID glob '%q%s') and (%s))"
 1910                                          " + "
 1911                                          "(select count(*) from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
 1912                                          " where (OBJECT_ID = '%q') and (%s))",
 1913                                          ContainerID, sep, where, ContainerID, where);
 1914     if( totalMatches < 0 )
 1915     {
 1916         /* Must be invalid SQL, so most likely bad or unhandled search criteria. */
 1917         SoapError(h, 708, "Unsupported or invalid search criteria");
 1918         goto search_error;
 1919     }
 1920     /* Does the object even exist? */
 1921     if( !totalMatches )
 1922     {
 1923         if( !object_exists(ContainerID) )
 1924         {
 1925             SoapError(h, 710, "No such container");
 1926             goto search_error;
 1927         }
 1928     }
 1929     ret = 0;
 1930     __SORT_LIMIT
 1931     orderBy = parse_sort_criteria(SortCriteria, &ret);
 1932     /* If it's a DLNA client, return an error for bad sort criteria */
 1933     if( ret < 0 && ((args.flags & FLAG_DLNA) || GETFLAG(DLNA_STRICT_MASK)) )
 1934     {
 1935         SoapError(h, 709, "Unsupported or invalid sort criteria");
 1936         goto search_error;
 1937     }
 1938 
 1939     sql = sqlite3_mprintf( SELECT_COLUMNS
 1940                           "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
 1941                           " where OBJECT_ID glob '%q%s' and (%s) %s "
 1942                           "%z %s"
 1943                           " limit %d, %d",
 1944                           ContainerID, sep, where, groupBy,
 1945                           (*ContainerID == '*') ? NULL :
 1946                           sqlite3_mprintf("UNION ALL " SELECT_COLUMNS
 1947                                           "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
 1948                                           " where OBJECT_ID = '%q' and (%s) ", ContainerID, where),
 1949                           orderBy, StartingIndex, RequestedCount);
 1950     DPRINTF(E_DEBUG, L_HTTP, "Search SQL: %s\n", sql);
 1951     ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
 1952     if( ret != SQLITE_OK )
 1953     {
 1954         if( !(args.flags & RESPONSE_TRUNCATED) )
 1955             DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql);
 1956         sqlite3_free(zErrMsg);
 1957     }
 1958     sqlite3_free(sql);
 1959     ret = strcatf(&str, "&lt;/DIDL-Lite&gt;</Result>\n"
 1960                         "<NumberReturned>%u</NumberReturned>\n"
 1961                         "<TotalMatches>%u</TotalMatches>\n"
 1962                         "<UpdateID>%u</UpdateID>"
 1963                         "</u:SearchResponse>",
 1964                         args.returned, totalMatches, updateID);
 1965     BuildSendAndCloseSoapResp(h, str.data, str.off);
 1966 search_error:
 1967     ClearNameValueList(&data);
 1968     free(orderBy);
 1969     free(where);
 1970     free(str.data);
 1971 }
 1972 
 1973 /*
 1974 If a control point calls QueryStateVariable on a state variable that is not
 1975 buffered in memory within (or otherwise available from) the service,
 1976 the service must return a SOAP fault with an errorCode of 404 Invalid Var.
 1977 
 1978 QueryStateVariable remains useful as a limited test tool but may not be
 1979 part of some future versions of UPnP.
 1980 */
 1981 static void
 1982 QueryStateVariable(struct upnphttp * h, const char * action)
 1983 {
 1984     static const char resp[] =
 1985     "<u:%sResponse "
 1986     "xmlns:u=\"%s\">"
 1987         "<return>%s</return>"
 1988     "</u:%sResponse>";
 1989 
 1990     char body[512];
 1991     struct NameValueParserData data;
 1992     const char * var_name;
 1993 
 1994     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
 1995     /*var_name = GetValueFromNameValueList(&data, "QueryStateVariable"); */
 1996     /*var_name = GetValueFromNameValueListIgnoreNS(&data, "varName");*/
 1997     var_name = GetValueFromNameValueList(&data, "varName");
 1998 
 1999     DPRINTF(E_INFO, L_HTTP, "QueryStateVariable(%.40s)\n", var_name);
 2000 
 2001     if(!var_name)
 2002     {
 2003         SoapError(h, 402, "Invalid Args");
 2004     }
 2005     else if(strcmp(var_name, "ConnectionStatus") == 0)
 2006     {
 2007         int bodylen;
 2008         bodylen = snprintf(body, sizeof(body), resp,
 2009                    action, "urn:schemas-upnp-org:control-1-0",
 2010                            "Connected", action);
 2011         BuildSendAndCloseSoapResp(h, body, bodylen);
 2012     }
 2013     else
 2014     {
 2015         DPRINTF(E_WARN, L_HTTP, "%s: Unknown: %s\n", action, THISORNUL(var_name));
 2016         SoapError(h, 404, "Invalid Var");
 2017     }
 2018 
 2019     ClearNameValueList(&data);
 2020 }
 2021 
 2022 static int _set_watch_count(long long id, const char *old, const char *new)
 2023 {
 2024     int64_t rowid = sqlite3_last_insert_rowid(db);
 2025     int ret;
 2026 
 2027     ret = sql_exec(db, "INSERT or IGNORE into BOOKMARKS (ID, WATCH_COUNT)"
 2028                " VALUES (%lld, %Q)", id, new ?: "1");
 2029     if (sqlite3_last_insert_rowid(db) != rowid)
 2030         return 0;
 2031 
 2032     if (!new) /* Increment */
 2033         ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT ="
 2034                    " ifnull(WATCH_COUNT,'0') + 1"
 2035                    " where ID = %lld", id);
 2036     else if (old && old[0])
 2037         ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
 2038                    " where WATCH_COUNT = %Q and ID = %lld",
 2039                    new, old, id);
 2040     else
 2041         ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
 2042                    " where ID = %lld",
 2043                    new, id);
 2044     return ret;
 2045 }
 2046 
 2047 /* For some reason, Kodi does URI encoding and appends a trailing slash */
 2048 static void _kodi_decode(char *str)
 2049 {
 2050     while (*str)
 2051     {
 2052         switch (*str) {
 2053         case '%':
 2054         {
 2055             if (isxdigit(str[1]) && isxdigit(str[2]))
 2056             {
 2057                 char x[3] = { str[1], str[2], '\0' };
 2058                 *str++ = (char)strtol(x, NULL, 16);
 2059                 memmove(str, str+2, strlen(str+1));
 2060             }
 2061             break;
 2062         }
 2063         case '/':
 2064             if (!str[1])
 2065                 *str = '\0';
 2066         default:
 2067             str++;
 2068             break;
 2069         }
 2070     }
 2071 }
 2072 
 2073 static int duration_sec(const char *str)
 2074 {
 2075     int hr, min, sec;
 2076 
 2077     if (sscanf(str, "%d:%d:%d", &hr, &min, &sec) == 3)
 2078         return (hr * 3600) + (min * 60) + sec;
 2079 
 2080     return atoi(str);
 2081 }
 2082 
 2083 static void UpdateObject(struct upnphttp * h, const char * action)
 2084 {
 2085     static const char resp[] =
 2086         "<u:UpdateObjectResponse"
 2087         " xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
 2088         "</u:UpdateObjecResponse>";
 2089 
 2090     struct NameValueParserData data;
 2091 
 2092     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
 2093 
 2094     char *ObjectID = GetValueFromNameValueList(&data, "ObjectID");
 2095     char *CurrentTagValue = GetValueFromNameValueList(&data, "CurrentTagValue");
 2096     char *NewTagValue = GetValueFromNameValueList(&data, "NewTagValue");
 2097     const char *rid = ObjectID;
 2098     char tag[32], current[32], new[32];
 2099     char *item, *saveptr = NULL;
 2100     int64_t detailID;
 2101     int ret = 1;
 2102 
 2103     if (!ObjectID || !CurrentTagValue || !NewTagValue)
 2104     {
 2105         SoapError(h, 402, "Invalid Args");
 2106         ClearNameValueList(&data);
 2107         return;
 2108     }
 2109 
 2110     _kodi_decode(ObjectID);
 2111     DPRINTF(E_DEBUG, L_HTTP, "UpdateObject %s: %s => %s\n", ObjectID, CurrentTagValue, NewTagValue);
 2112 
 2113     in_magic_container(ObjectID, 0, &rid);
 2114     detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid);
 2115     if (detailID <= 0)
 2116     {
 2117         SoapError(h, 701, "No such object");
 2118         ClearNameValueList(&data);
 2119         return;
 2120     }
 2121 
 2122     for (item = strtok_r(CurrentTagValue, ",", &saveptr); item; item = strtok_r(NULL, ",", &saveptr))
 2123     {
 2124         char *p;
 2125         if (sscanf(item, "&lt;%31[^&]&gt;%31[^&]", tag, current) != 2)
 2126             continue;
 2127         p = strstr(NewTagValue, tag);
 2128         if (!p || sscanf(p, "%*[^&]&gt;%31[^&]", new) != 1)
 2129             continue;
 2130 
 2131         DPRINTF(E_DEBUG, L_HTTP, "Setting %s to %s\n", tag, new);
 2132         /* Kodi uses incorrect tag "upnp:playCount" instead of "upnp:playbackCount" */
 2133         if (strcmp(tag, "upnp:playbackCount") == 0 || strcmp(tag, "upnp:playCount") == 0)
 2134         {
 2135             ret = _set_watch_count(detailID, current, new);
 2136         }
 2137         else if (strcmp(tag, "upnp:lastPlaybackPosition") == 0)
 2138         {
 2139 
 2140             int sec = duration_sec(new);
 2141             if( h->req_client && (h->req_client->type->flags & FLAG_CONVERT_MS) ) {
 2142                 sec /= 1000;
 2143             }
 2144             if (sec < 30)
 2145                 sec = 0;
 2146             else
 2147                 sec -= 1;
 2148             ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)"
 2149                        " VALUES (%lld, %d)", (long long)detailID, sec);
 2150             ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d"
 2151                        " where SEC = %Q and ID = %lld",
 2152                        sec, current, (long long)detailID);
 2153         }
 2154         else
 2155             DPRINTF(E_WARN, L_HTTP, "Tag %s unsupported for writing\n", tag);
 2156     }
 2157 
 2158     if (ret == SQLITE_OK)
 2159         BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
 2160     else
 2161         SoapError(h, 501, "Action Failed");
 2162 
 2163     ClearNameValueList(&data);
 2164 }
 2165 
 2166 static void
 2167 SamsungGetFeatureList(struct upnphttp * h, const char * action)
 2168 {
 2169     static const char resp[] =
 2170         "<u:X_GetFeatureListResponse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
 2171         "<FeatureList>"
 2172         "&lt;Features xmlns=\"urn:schemas-upnp-org:av:avs\""
 2173         " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
 2174         " xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\"&gt;"
 2175         "&lt;Feature name=\"samsung.com_BASICVIEW\" version=\"1\"&gt;"
 2176          "&lt;container id=\"%s\" type=\"object.item.audioItem\"/&gt;"
 2177          "&lt;container id=\"%s\" type=\"object.item.videoItem\"/&gt;"
 2178          "&lt;container id=\"%s\" type=\"object.item.imageItem\"/&gt;"
 2179         "&lt;/Feature&gt;"
 2180         "&lt;/Features&gt;"
 2181         "</FeatureList></u:X_GetFeatureListResponse>";
 2182     const char *audio = MUSIC_ID;
 2183     const char *video = VIDEO_ID;
 2184     const char *image = IMAGE_ID;
 2185     char body[1024];
 2186     int len;
 2187 
 2188     if (runtime_vars.root_container)
 2189     {
 2190         if (strcmp(runtime_vars.root_container, BROWSEDIR_ID) == 0)
 2191         {
 2192             audio = MUSIC_DIR_ID;
 2193             video = VIDEO_DIR_ID;
 2194             image = IMAGE_DIR_ID;
 2195         }
 2196         else
 2197         {
 2198             audio = runtime_vars.root_container;
 2199             video = runtime_vars.root_container;
 2200             image = runtime_vars.root_container;
 2201         }
 2202     }
 2203     else if (h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG_DCM10))
 2204     {
 2205         audio = "A";
 2206         video = "V";
 2207         image = "I";
 2208     }
 2209 
 2210     len = snprintf(body, sizeof(body), resp, audio, video, image);
 2211 
 2212     BuildSendAndCloseSoapResp(h, body, len);
 2213 }
 2214 
 2215 static void
 2216 SamsungSetBookmark(struct upnphttp * h, const char * action)
 2217 {
 2218     static const char resp[] =
 2219         "<u:X_SetBookmarkResponse"
 2220         " xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
 2221         "</u:X_SetBookmarkResponse>";
 2222 
 2223     struct NameValueParserData data;
 2224     char *ObjectID, *PosSecond;
 2225 
 2226     ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
 2227     ObjectID = GetValueFromNameValueList(&data, "ObjectID");
 2228     PosSecond = GetValueFromNameValueList(&data, "PosSecond");
 2229 
 2230     if( ObjectID && PosSecond )
 2231     {
 2232         const char *rid = ObjectID;
 2233         int64_t detailID;
 2234         int sec = atoi(PosSecond);
 2235         int ret;
 2236 
 2237         in_magic_container(ObjectID, 0, &rid);
 2238         detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid);
 2239 
 2240         if( h->req_client && (h->req_client->type->flags & FLAG_CONVERT_MS) ) {
 2241             sec /= 1000;
 2242         }
 2243         if ( sec < 30 )
 2244             sec = 0;
 2245         ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)"
 2246                    " VALUES (%lld, %d)", (long long)detailID, sec);
 2247         ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d"
 2248                    " where ID = %lld",
 2249                    sec, (long long)detailID);
 2250         if( ret != SQLITE_OK )
 2251             DPRINTF(E_WARN, L_METADATA, "Error setting bookmark %s on ObjectID='%s'\n", PosSecond, rid);
 2252         BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
 2253     }
 2254     else
 2255         SoapError(h, 402, "Invalid Args");
 2256 
 2257     ClearNameValueList(&data);
 2258 }
 2259 
 2260 static const struct
 2261 {
 2262     const char * methodName;
 2263     void (*methodImpl)(struct upnphttp *, const char *);
 2264 }
 2265 soapMethods[] =
 2266 {
 2267     { "QueryStateVariable", QueryStateVariable},
 2268     { "Browse", BrowseContentDirectory},
 2269     { "Search", SearchContentDirectory},
 2270     { "GetSearchCapabilities", GetSearchCapabilities},
 2271     { "GetSortCapabilities", GetSortCapabilities},
 2272     { "GetSystemUpdateID", GetSystemUpdateID},
 2273     { "GetProtocolInfo", GetProtocolInfo},
 2274     { "GetCurrentConnectionIDs", GetCurrentConnectionIDs},
 2275     { "GetCurrentConnectionInfo", GetCurrentConnectionInfo},
 2276     { "IsAuthorized", IsAuthorizedValidated},
 2277     { "IsValidated", IsAuthorizedValidated},
 2278     { "RegisterDevice", RegisterDevice},
 2279     { "UpdateObject", UpdateObject},
 2280     { "X_GetFeatureList", SamsungGetFeatureList},
 2281     { "X_SetBookmark", SamsungSetBookmark},
 2282     { 0, 0 }
 2283 };
 2284 
 2285 void
 2286 ExecuteSoapAction(struct upnphttp * h, const char * action, int n)
 2287 {
 2288     char * p;
 2289 
 2290     p = strchr(action, '#');
 2291     if(p)
 2292     {
 2293         int i = 0;
 2294         int len;
 2295         int methodlen;
 2296         char * p2;
 2297         p++;
 2298         p2 = strchr(p, '"');
 2299         if(p2)
 2300             methodlen = p2 - p;
 2301         else
 2302             methodlen = n - (p - action);
 2303         DPRINTF(E_DEBUG, L_HTTP, "SoapMethod: %.*s\n", methodlen, p);
 2304         while(soapMethods[i].methodName)
 2305         {
 2306             len = strlen(soapMethods[i].methodName);
 2307             if(strncmp(p, soapMethods[i].methodName, len) == 0)
 2308             {
 2309                 soapMethods[i].methodImpl(h, soapMethods[i].methodName);
 2310                 return;
 2311             }
 2312             i++;
 2313         }
 2314 
 2315         DPRINTF(E_WARN, L_HTTP, "SoapMethod: Unknown: %.*s\n", methodlen, p);
 2316     }
 2317 
 2318     SoapError(h, 401, "Invalid Action");
 2319 }
 2320