Wrap up version 1.3.3.
[minidlna.git] / upnpsoap.c
blob20f6b60342aff1d747da6653a61a7d335c48397f
1 /* MiniDLNA project
3 * http://sourceforge.net/projects/minidlna/
5 * MiniDLNA media server
6 * Copyright (C) 2008-2017 Justin Maggard
8 * This file is part of MiniDLNA.
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.
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.
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/>.
22 * Portions of the code from the MiniUPnP project:
24 * Copyright (c) 2006-2007, Thomas Bernard
25 * All rights reserved.
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.
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.
49 #include "config.h"
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>
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"
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))
84 /* Standard Errors:
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.
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)
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>";
124 char body[2048];
125 int bodylen;
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);
134 static void
135 BuildSendAndCloseSoapResp(struct upnphttp * h,
136 const char * body, int bodylen)
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>";
144 static const char afterbody[] =
145 "</s:Body>"
146 "</s:Envelope>\r\n";
148 if (!body || bodylen < 0)
150 Send500(h);
151 return;
154 BuildHeader_upnphttp(h, 200, "OK", sizeof(beforebody) - 1
155 + sizeof(afterbody) - 1 + bodylen );
157 memcpy(h->res_buf + h->res_buflen, beforebody, sizeof(beforebody) - 1);
158 h->res_buflen += sizeof(beforebody) - 1;
160 memcpy(h->res_buf + h->res_buflen, body, bodylen);
161 h->res_buflen += bodylen;
163 memcpy(h->res_buf + h->res_buflen, afterbody, sizeof(afterbody) - 1);
164 h->res_buflen += sizeof(afterbody) - 1;
166 SendResp_upnphttp(h);
167 CloseSocket_upnphttp(h);
170 static void
171 GetSystemUpdateID(struct upnphttp * h, const char * action)
173 static const char resp[] =
174 "<u:%sResponse "
175 "xmlns:u=\"%s\">"
176 "<Id>%d</Id>"
177 "</u:%sResponse>";
179 char body[512];
180 int bodylen;
182 bodylen = snprintf(body, sizeof(body), resp,
183 action, "urn:schemas-upnp-org:service:ContentDirectory:1",
184 updateID, action);
185 BuildSendAndCloseSoapResp(h, body, bodylen);
188 static void
189 IsAuthorizedValidated(struct upnphttp * h, const char * action)
191 static const char resp[] =
192 "<u:%sResponse "
193 "xmlns:u=\"%s\">"
194 "<Result>%d</Result>"
195 "</u:%sResponse>";
197 char body[512];
198 struct NameValueParserData data;
199 const char * id;
201 ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, XML_STORE_EMPTY_FL);
202 id = GetValueFromNameValueList(&data, "DeviceID");
203 if(id)
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);
211 else
212 SoapError(h, 402, "Invalid Args");
214 ClearNameValueList(&data);
217 static void
218 RegisterDevice(struct upnphttp * h, const char * action)
220 static const char resp[] =
221 "<u:%sResponse "
222 "xmlns:u=\"%s\">"
223 "<RegistrationRespMsg>%s</RegistrationRespMsg>"
224 "</u:%sResponse>";
226 char body[512];
227 int bodylen;
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);
235 static void
236 GetProtocolInfo(struct upnphttp * h, const char * action)
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>";
247 char * body;
248 int bodylen;
250 bodylen = asprintf(&body, resp,
251 action, "urn:schemas-upnp-org:service:ConnectionManager:1",
252 action);
253 BuildSendAndCloseSoapResp(h, body, bodylen);
254 free(body);
257 static void
258 GetSortCapabilities(struct upnphttp * h, const char * action)
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>";
273 char body[512];
274 int bodylen;
276 bodylen = snprintf(body, sizeof(body), resp,
277 action, "urn:schemas-upnp-org:service:ContentDirectory:1",
278 action);
279 BuildSendAndCloseSoapResp(h, body, bodylen);
282 static void
283 GetSearchCapabilities(struct upnphttp * h, const char * action)
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>";
302 char body[512];
303 int bodylen;
305 bodylen = snprintf(body, sizeof(body), resp,
306 action, "urn:schemas-upnp-org:service:ContentDirectory:1",
307 action);
308 BuildSendAndCloseSoapResp(h, body, bodylen);
311 static void
312 GetCurrentConnectionIDs(struct upnphttp * h, const char * action)
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>";
321 char body[512];
322 int bodylen;
324 bodylen = snprintf(body, sizeof(body), resp,
325 action, "urn:schemas-upnp-org:service:ConnectionManager:1",
326 action);
327 BuildSendAndCloseSoapResp(h, body, bodylen);
330 static void
331 GetCurrentConnectionInfo(struct upnphttp * h, const char * action)
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>";
346 char body[sizeof(resp)+128];
347 struct NameValueParserData data;
348 const char *id_str;
349 int id;
350 char *endptr = NULL;
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)
359 SoapError(h, 402, "Invalid Args");
361 else if(id != 0)
363 SoapError(h, 701, "No such object error");
365 else
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);
373 ClearNameValueList(&data);
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)
419 static uint32_t
420 set_filter_flags(char *filter, struct upnphttp *h)
422 char *item, *saveptr = NULL;
423 uint32_t flags = 0;
424 int samsung = h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG);
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;
432 if (flags)
433 return flags;
435 if( samsung )
436 flags |= FILTER_DLNA_NAMESPACE;
437 item = strtok_r(filter, ",", &saveptr);
438 while( item != NULL )
440 if( saveptr )
441 *(item-1) = ',';
442 while( isspace(*item) )
443 item++;
444 if( strcmp(item, "@childCount") == 0 )
446 flags |= FILTER_CHILDCOUNT;
448 else if( strcmp(item, "@searchable") == 0 )
450 flags |= FILTER_SEARCHABLE;
452 else if( strcmp(item, "dc:creator") == 0 )
454 flags |= FILTER_DC_CREATOR;
456 else if( strcmp(item, "dc:date") == 0 )
458 flags |= FILTER_DC_DATE;
460 else if( strcmp(item, "dc:description") == 0 )
462 flags |= FILTER_DC_DESCRIPTION;
464 else if( strcmp(item, "dlna") == 0 )
466 flags |= FILTER_DLNA_NAMESPACE;
468 else if( strcmp(item, "@refID") == 0 )
470 flags |= FILTER_REFID;
472 else if( strcmp(item, "upnp:album") == 0 )
474 flags |= FILTER_UPNP_ALBUM;
476 else if( strcmp(item, "upnp:albumArtURI") == 0 )
478 flags |= FILTER_UPNP_ALBUMARTURI;
479 if( samsung )
480 flags |= FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID;
482 else if( strcmp(item, "upnp:albumArtURI@dlna:profileID") == 0 )
484 flags |= FILTER_UPNP_ALBUMARTURI;
485 flags |= FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID;
487 else if( strcmp(item, "upnp:artist") == 0 )
489 flags |= FILTER_UPNP_ARTIST;
491 else if( strcmp(item, "upnp:actor") == 0 )
493 flags |= FILTER_UPNP_ACTOR;
495 else if( strcmp(item, "upnp:genre") == 0 )
497 flags |= FILTER_UPNP_GENRE;
499 else if( strcmp(item, "upnp:originalTrackNumber") == 0 )
501 flags |= FILTER_UPNP_ORIGINALTRACKNUMBER;
503 else if( strcmp(item, "upnp:searchClass") == 0 )
505 flags |= FILTER_UPNP_SEARCHCLASS;
507 else if( strcmp(item, "upnp:storageUsed") == 0 )
509 flags |= FILTER_UPNP_STORAGEUSED;
511 else if( strcmp(item, "res") == 0 )
513 flags |= FILTER_RES;
515 else if( (strcmp(item, "res@bitrate") == 0) ||
516 (strcmp(item, "@bitrate") == 0) ||
517 ((strcmp(item, "bitrate") == 0) && (flags & FILTER_RES)) )
519 flags |= FILTER_RES;
520 flags |= FILTER_RES_BITRATE;
522 else if( (strcmp(item, "res@duration") == 0) ||
523 (strcmp(item, "@duration") == 0) ||
524 ((strcmp(item, "duration") == 0) && (flags & FILTER_RES)) )
526 flags |= FILTER_RES;
527 flags |= FILTER_RES_DURATION;
529 else if( (strcmp(item, "res@nrAudioChannels") == 0) ||
530 (strcmp(item, "@nrAudioChannels") == 0) ||
531 ((strcmp(item, "nrAudioChannels") == 0) && (flags & FILTER_RES)) )
533 flags |= FILTER_RES;
534 flags |= FILTER_RES_NRAUDIOCHANNELS;
536 else if( (strcmp(item, "res@resolution") == 0) ||
537 (strcmp(item, "@resolution") == 0) ||
538 ((strcmp(item, "resolution") == 0) && (flags & FILTER_RES)) )
540 flags |= FILTER_RES;
541 flags |= FILTER_RES_RESOLUTION;
543 else if( (strcmp(item, "res@sampleFrequency") == 0) ||
544 (strcmp(item, "@sampleFrequency") == 0) ||
545 ((strcmp(item, "sampleFrequency") == 0) && (flags & FILTER_RES)) )
547 flags |= FILTER_RES;
548 flags |= FILTER_RES_SAMPLEFREQUENCY;
550 else if( (strcmp(item, "res@size") == 0) ||
551 (strcmp(item, "@size") == 0) ||
552 (strcmp(item, "size") == 0) )
554 flags |= FILTER_RES;
555 flags |= FILTER_RES_SIZE;
557 else if( strcmp(item, "upnp:playbackCount") == 0 )
559 flags |= FILTER_UPNP_PLAYBACKCOUNT;
561 else if( strcmp(item, "upnp:lastPlaybackPosition") == 0 )
563 flags |= FILTER_UPNP_LASTPLAYBACKPOSITION;
565 else if( strcmp(item, "sec:CaptionInfoEx") == 0 )
567 flags |= FILTER_SEC_CAPTION_INFO_EX;
569 else if( strcmp(item, "sec:dcmInfo") == 0 )
571 flags |= FILTER_SEC_DCM_INFO;
573 else if( strcmp(item, "res@pv:subtitleFileType") == 0 )
575 flags |= FILTER_PV_SUBTITLE_FILE_TYPE;
577 else if( strcmp(item, "res@pv:subtitleFileUri") == 0 )
579 flags |= FILTER_PV_SUBTITLE_FILE_URI;
581 else if( strcmp(item, "av:mediaClass") == 0 )
583 flags |= FILTER_AV_MEDIA_CLASS;
585 else if( strcmp(item, "upnp:episodeNumber") == 0 )
587 flags |= FILTER_UPNP_EPISODENUMBER;
589 else if( strcmp(item, "upnp:episodeSeason") == 0 )
591 flags |= FILTER_UPNP_EPISODESEASON;
593 item = strtok_r(NULL, ",", &saveptr);
596 return flags;
599 static char *
600 parse_sort_criteria(char *sortCriteria, int *error)
602 char *order = NULL;
603 char *item, *saveptr;
604 int i, ret, reverse, title_sorted = 0;
605 struct string_s str;
606 *error = 0;
608 if( force_sort_criteria )
609 sortCriteria = strdup(force_sort_criteria);
610 if( !sortCriteria )
611 return NULL;
613 if( (item = strtok_r(sortCriteria, ",", &saveptr)) )
615 order = malloc(4096);
616 str.data = order;
617 str.size = 4096;
618 str.off = 0;
619 strcatf(&str, "order by ");
621 for( i = 0; item != NULL; i++ )
623 reverse=0;
624 if( i )
625 strcatf(&str, ", ");
626 if( *item == '+' )
628 item++;
630 else if( *item == '-' )
632 reverse = 1;
633 item++;
635 else
637 DPRINTF(E_ERROR, L_HTTP, "No order specified [%s]\n", item);
638 goto bad_direction;
640 if( strcasecmp(item, "upnp:class") == 0 )
642 strcatf(&str, "o.CLASS");
644 else if( strcasecmp(item, "dc:title") == 0 )
646 strcatf(&str, "d.TITLE");
647 title_sorted = 1;
649 else if( strcasecmp(item, "dc:date") == 0 )
651 strcatf(&str, "d.DATE");
653 else if( strcasecmp(item, "upnp:originalTrackNumber") == 0 ||
654 strcasecmp(item, "upnp:episodeNumber") == 0 )
656 strcatf(&str, "d.DISC%s, d.TRACK", reverse ? " DESC" : "");
658 else if( strcasecmp(item, "upnp:album") == 0 )
660 strcatf(&str, "d.ALBUM");
662 else if( strcasecmp(item, "path") == 0 )
664 strcatf(&str, "d.PATH");
666 else
668 DPRINTF(E_ERROR, L_HTTP, "Unhandled SortCriteria [%s]\n", item);
669 bad_direction:
670 *error = -1;
671 if( i )
673 ret = strlen(order);
674 order[ret-2] = '\0';
676 i--;
677 goto unhandled_order;
680 if( reverse )
681 strcatf(&str, " DESC");
682 unhandled_order:
683 item = strtok_r(NULL, ",", &saveptr);
685 if( i <= 0 )
687 free(order);
688 if( force_sort_criteria )
689 free(sortCriteria);
690 return NULL;
692 /* Add a "tiebreaker" sort order */
693 if( !title_sorted )
694 strcatf(&str, ", TITLE ASC");
696 if( force_sort_criteria )
697 free(sortCriteria);
699 return order;
702 static void
703 _alphasort_alt_title(char **title, char **alt_title, int requested, int returned, const char *disc, const char *track)
705 char *old_title = *alt_title ?: NULL;
706 char buf[8];
707 int pad;
708 int ret;
710 snprintf(buf, sizeof(buf), "%d", requested);
711 pad = strlen(buf);
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);
721 else
722 ret = asprintf(alt_title, "%0*d %s", pad, returned, *title);
724 if (ret > 0)
725 *title = *alt_title;
726 else
727 *alt_title = NULL;
728 free(old_title);
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)
735 int dstw = reqw;
736 int dsth = reqh;
738 if( (args->flags & FLAG_NO_RESIZE) && reqw > 160 && reqh > 160 )
739 return;
741 strcatf(args->str, "&lt;res ");
742 if( args->filter & FILTER_RES_RESOLUTION )
744 dstw = reqw;
745 dsth = ((((reqw<<10)/srcw)*srch)>>10);
746 if( dsth > reqh ) {
747 dsth = reqh;
748 dstw = (((reqh<<10)/srch) * srcw>>10);
750 strcatf(args->str, "resolution=\"%dx%d\" ", dstw, dsth);
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);
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)
766 strcatf(args->str, "&lt;res ");
767 if( size && (args->filter & FILTER_RES_SIZE) ) {
768 strcatf(args->str, "size=\"%s\" ", size);
770 if( duration && (args->filter & FILTER_RES_DURATION) ) {
771 strcatf(args->str, "duration=\"%s\" ", duration);
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);
779 if( sampleFrequency && (args->filter & FILTER_RES_SAMPLEFREQUENCY) ) {
780 strcatf(args->str, "sampleFrequency=\"%s\" ", sampleFrequency);
782 if( nrAudioChannels && (args->filter & FILTER_RES_NRAUDIOCHANNELS) ) {
783 strcatf(args->str, "nrAudioChannels=\"%s\" ", nrAudioChannels);
785 if( resolution && (args->filter & FILTER_RES_RESOLUTION) ) {
786 strcatf(args->str, "resolution=\"%s\" ", resolution);
788 if( args->filter & FILTER_PV_SUBTITLE )
790 if( args->flags & FLAG_HAS_CAPTIONS )
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);
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);
806 static int
807 get_child_count(const char *object, struct magic_container_s *magic)
809 int ret;
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 = '%q';", object);
818 return (ret > 0) ? ret : 0;
821 static int
822 object_exists(const char *object)
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);
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
836 static int
837 callback(void *args, int argc, char **argv, char **azColName)
839 (void)args;
840 (void)argc;
841 (void)azColName;
842 struct Response *passed_args = (struct Response *)args;
843 char *id = argv[0], *parent = argv[1], *refID = argv[2], *detailID = argv[3], *class = argv[4], *size = argv[5], *title = argv[6],
844 *duration = argv[7], *bitrate = argv[8], *sampleFrequency = argv[9], *artist = argv[10], *album = argv[11],
845 *genre = argv[12], *comment = argv[13], *nrAudioChannels = argv[14], *track = argv[15], *date = argv[16], *resolution = argv[17],
846 *tn = argv[18], *creator = argv[19], *dlna_pn = argv[20], *mime = argv[21], *album_art = argv[22], *rotate = argv[23], *disc = argv[24];
847 char dlna_buf[128];
848 const char *ext;
849 struct string_s *str = passed_args->str;
850 int ret = 0;
852 /* Make sure we have at least 8KB left of allocated memory to finish the response. */
853 if( str->off > (str->size - 8192) )
855 #if MAX_RESPONSE_SIZE > 0
856 if( (str->size+DEFAULT_RESP_SIZE) <= MAX_RESPONSE_SIZE )
858 #endif
859 str->data = realloc(str->data, (str->size+DEFAULT_RESP_SIZE));
860 if( str->data )
862 str->size += DEFAULT_RESP_SIZE;
863 DPRINTF(E_DEBUG, L_HTTP, "UPnP SOAP response enlarged to %lu. [%d results so far]\n",
864 (unsigned long)str->size, passed_args->returned);
866 else
868 DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response truncated, realloc failed\n");
869 passed_args->flags |= RESPONSE_TRUNCATED;
870 return 1;
872 #if MAX_RESPONSE_SIZE > 0
874 else
876 DPRINTF(E_ERROR, L_HTTP, "UPnP SOAP response would exceed the max response size [%lld], truncating\n", (long long int)MAX_RESPONSE_SIZE);
877 passed_args->flags |= RESPONSE_TRUNCATED;
878 return 1;
880 #endif
882 passed_args->returned++;
883 passed_args->flags &= ~RESPONSE_FLAGS;
885 if( strncmp(class, "item", 4) == 0 )
887 uint32_t dlna_flags = DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_HTTP_STALLING|DLNA_FLAG_TM_B;
888 char *alt_title = NULL;
889 /* We may need special handling for certain MIME types */
890 if( *mime == 'v' )
892 dlna_flags |= DLNA_FLAG_TM_S;
893 if (GETFLAG(SUBTITLES_MASK) &&
894 (passed_args->client >= EStandardDLNA150 || !passed_args->client))
895 passed_args->flags |= FLAG_CAPTION_RES;
897 if( passed_args->flags & FLAG_MIME_AVI_DIVX )
899 if( strcmp(mime, "video/x-msvideo") == 0 )
901 if( creator )
902 strcpy(mime+6, "divx");
903 else
904 strcpy(mime+6, "avi");
907 else if( passed_args->flags & FLAG_MIME_AVI_AVI )
909 if( strcmp(mime, "video/x-msvideo") == 0 )
911 strcpy(mime+6, "avi");
914 else if( passed_args->client == EFreeBox && dlna_pn )
916 if( strncmp(dlna_pn, "AVC_TS", 6) == 0 ||
917 strncmp(dlna_pn, "MPEG_TS", 7) == 0 )
919 strcpy(mime+6, "mp2t");
922 if( !(passed_args->flags & FLAG_DLNA) )
924 if( strcmp(mime+6, "vnd.dlna.mpeg-tts") == 0 )
926 strcpy(mime+6, "mpeg");
929 if( (passed_args->flags & FLAG_CAPTION_RES) ||
930 (passed_args->filter & (FILTER_SEC_CAPTION_INFO_EX|FILTER_PV_SUBTITLE)) )
932 if( sql_get_int_field(db, "SELECT ID from CAPTIONS where ID = '%s'", detailID) > 0 )
933 passed_args->flags |= FLAG_HAS_CAPTIONS;
935 /* From what I read, Samsung TV's expect a [wrong] MIME type of x-mkv. */
936 if( passed_args->flags & FLAG_SAMSUNG )
938 if( strcmp(mime+6, "x-matroska") == 0 )
940 strcpy(mime+8, "mkv");
943 /* LG hack: subtitles won't get used unless dc:title contains a dot. */
944 else if( passed_args->client == ELGDevice && (passed_args->flags & FLAG_HAS_CAPTIONS) )
946 ret = asprintf(&alt_title, "%s.", title);
947 if( ret > 0 )
948 title = alt_title;
949 else
950 alt_title = NULL;
952 /* Asus OPlay reboots with titles longer than 23 characters with some file types. */
953 else if( passed_args->client == EAsusOPlay && (passed_args->flags & FLAG_HAS_CAPTIONS) )
955 if( strlen(title) > 23 )
956 title[23] = '\0';
958 /* Hyundai hack: Only titles with a media extension get recognized. */
959 else if( passed_args->client == EHyundaiTV )
961 ext = mime_to_ext(mime);
962 ret = asprintf(&alt_title, "%s.%s", title, ext);
963 if( ret > 0 )
964 title = alt_title;
965 else
966 alt_title = NULL;
969 else if( *mime == 'a' )
971 dlna_flags |= DLNA_FLAG_TM_S;
972 if( strcmp(mime+6, "x-flac") == 0 )
974 if( passed_args->flags & FLAG_MIME_FLAC_FLAC )
976 strcpy(mime+6, "flac");
979 else if( strcmp(mime+6, "x-wav") == 0 )
981 if( passed_args->flags & FLAG_MIME_WAV_WAV )
983 strcpy(mime+6, "wav");
987 else
988 dlna_flags |= DLNA_FLAG_TM_I;
989 /* Force an alphabetical sort, for clients that like to do their own sorting */
990 if( GETFLAG(FORCE_ALPHASORT_MASK) )
991 _alphasort_alt_title(&title, &alt_title, passed_args->requested, passed_args->returned, disc, track);
993 if( passed_args->flags & FLAG_SKIP_DLNA_PN )
994 dlna_pn = NULL;
996 if( dlna_pn )
997 snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_PN=%s;"
998 "DLNA.ORG_OP=01;"
999 "DLNA.ORG_CI=0;"
1000 "DLNA.ORG_FLAGS=%08X%024X",
1001 dlna_pn, dlna_flags, 0);
1002 else if( passed_args->flags & FLAG_DLNA )
1003 snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_OP=01;"
1004 "DLNA.ORG_CI=0;"
1005 "DLNA.ORG_FLAGS=%08X%024X",
1006 dlna_flags, 0);
1007 else
1008 strcpy(dlna_buf, "*");
1010 ret = strcatf(str, "&lt;item id=\"%s\" parentID=\"%s\" restricted=\"1\"", id, parent);
1011 if( refID && (passed_args->filter & FILTER_REFID) ) {
1012 ret = strcatf(str, " refID=\"%s\"", refID);
1014 ret = strcatf(str, "&gt;"
1015 "&lt;dc:title&gt;%s&lt;/dc:title&gt;"
1016 "&lt;upnp:class&gt;object.%s&lt;/upnp:class&gt;",
1017 title, class);
1018 if( comment && (passed_args->filter & FILTER_DC_DESCRIPTION) ) {
1019 ret = strcatf(str, "&lt;dc:description&gt;%.384s&lt;/dc:description&gt;", comment);
1021 if( creator && (passed_args->filter & FILTER_DC_CREATOR) ) {
1022 ret = strcatf(str, "&lt;dc:creator&gt;%s&lt;/dc:creator&gt;", creator);
1024 if( date && (passed_args->filter & FILTER_DC_DATE) ) {
1025 ret = strcatf(str, "&lt;dc:date&gt;%s&lt;/dc:date&gt;", date);
1027 if( (passed_args->filter & FILTER_BOOKMARK_MASK) ) {
1028 /* Get bookmark */
1029 int sec = sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID);
1030 if( sec > 0 ) {
1031 /* This format is wrong according to the UPnP/AV spec. It should be in duration format,
1032 ** so HH:MM:SS. But Kodi seems to be the only user of this tag, and it only works with a
1033 ** raw seconds value.
1034 ** If Kodi gets fixed, we can use duration_str(sec * 1000) here */
1035 if( passed_args->flags & FLAG_CONVERT_MS ) {
1036 sec *= 1000;
1038 if( passed_args->filter & FILTER_UPNP_LASTPLAYBACKPOSITION )
1039 ret = strcatf(str, "&lt;upnp:lastPlaybackPosition&gt;%d&lt;/upnp:lastPlaybackPosition&gt;",
1040 sec);
1041 if( passed_args->filter & FILTER_SEC_DCM_INFO )
1042 ret = strcatf(str, "&lt;sec:dcmInfo&gt;CREATIONDATE=0,FOLDER=%s,BM=%d&lt;/sec:dcmInfo&gt;",
1043 title, sec);
1045 if( passed_args->filter & FILTER_UPNP_PLAYBACKCOUNT ) {
1046 ret = strcatf(str, "&lt;upnp:playbackCount&gt;%d&lt;/upnp:playbackCount&gt;",
1047 sql_get_int_field(db, "SELECT WATCH_COUNT from BOOKMARKS where ID = '%s'", detailID));
1050 free(alt_title);
1051 if( artist ) {
1052 if( (*mime == 'v') && (passed_args->filter & FILTER_UPNP_ACTOR) ) {
1053 ret = strcatf(str, "&lt;upnp:actor&gt;%s&lt;/upnp:actor&gt;", artist);
1055 if( passed_args->filter & FILTER_UPNP_ARTIST ) {
1056 ret = strcatf(str, "&lt;upnp:artist&gt;%s&lt;/upnp:artist&gt;", artist);
1059 if( album && (passed_args->filter & FILTER_UPNP_ALBUM) ) {
1060 ret = strcatf(str, "&lt;upnp:album&gt;%s&lt;/upnp:album&gt;", album);
1062 if( genre && (passed_args->filter & FILTER_UPNP_GENRE) ) {
1063 ret = strcatf(str, "&lt;upnp:genre&gt;%s&lt;/upnp:genre&gt;", genre);
1065 if( strncmp(id, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 ) {
1066 track = strrchr(id, '$')+1;
1068 if( NON_ZERO(track) ) {
1069 if( *mime == 'a' && (passed_args->filter & FILTER_UPNP_ORIGINALTRACKNUMBER) ) {
1070 ret = strcatf(str, "&lt;upnp:originalTrackNumber&gt;%s&lt;/upnp:originalTrackNumber&gt;", track);
1071 } else if( *mime == 'v' ) {
1072 if( NON_ZERO(disc) && (passed_args->filter & FILTER_UPNP_EPISODESEASON) )
1073 ret = strcatf(str, "&lt;upnp:episodeSeason&gt;%s&lt;/upnp:episodeSeason&gt;", disc);
1074 if( passed_args->filter & FILTER_UPNP_EPISODENUMBER )
1075 ret = strcatf(str, "&lt;upnp:episodeNumber&gt;%s&lt;/upnp:episodeNumber&gt;", track);
1078 if( passed_args->filter & FILTER_RES ) {
1079 ext = mime_to_ext(mime);
1080 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1081 resolution, dlna_buf, mime, detailID, ext, passed_args);
1082 if( *mime == 'i' ) {
1083 int srcw, srch;
1084 if( resolution && (sscanf(resolution, "%6dx%6d", &srcw, &srch) == 2) )
1086 if( srcw > 4096 || srch > 4096 )
1087 add_resized_res(srcw, srch, 4096, 4096, "JPEG_LRG", detailID, passed_args);
1088 if( srcw > 1024 || srch > 768 )
1089 add_resized_res(srcw, srch, 1024, 768, "JPEG_MED", detailID, passed_args);
1090 if( srcw > 640 || srch > 480 )
1091 add_resized_res(srcw, srch, 640, 480, "JPEG_SM", detailID, passed_args);
1093 if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) {
1094 ret = strcatf(str, "&lt;res protocolInfo=\"http-get:*:%s:%s\"&gt;"
1095 "http://%s:%d/Thumbnails/%s.jpg"
1096 "&lt;/res&gt;",
1097 mime, "DLNA.ORG_PN=JPEG_TN;DLNA.ORG_CI=1", lan_addr[passed_args->iface].str,
1098 runtime_vars.port, detailID);
1100 else
1101 add_resized_res(srcw, srch, 160, 160, "JPEG_TN", detailID, passed_args);
1103 else if( *mime == 'v' ) {
1104 switch( passed_args->client ) {
1105 case EToshibaTV:
1106 if( dlna_pn &&
1107 (strncmp(dlna_pn, "MPEG_TS_HD_NA", 13) == 0 ||
1108 strncmp(dlna_pn, "MPEG_TS_SD_NA", 13) == 0 ||
1109 strncmp(dlna_pn, "AVC_TS_MP_HD_AC3", 16) == 0 ||
1110 strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0))
1112 sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_NTSC");
1113 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1114 resolution, dlna_buf, mime, detailID, ext, passed_args);
1116 break;
1117 case ESonyBDP:
1118 if( dlna_pn &&
1119 (strncmp(dlna_pn, "AVC_TS", 6) == 0 ||
1120 strncmp(dlna_pn, "MPEG_TS", 7) == 0) )
1122 if( strncmp(dlna_pn, "MPEG_TS_SD_NA", 13) != 0 )
1124 sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_TS_SD_NA");
1125 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1126 resolution, dlna_buf, mime, detailID, ext, passed_args);
1128 if( strncmp(dlna_pn, "MPEG_TS_SD_EU", 13) != 0 )
1130 sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_TS_SD_EU");
1131 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1132 resolution, dlna_buf, mime, detailID, ext, passed_args);
1135 else if( (dlna_pn &&
1136 (strncmp(dlna_pn, "AVC_MP4", 7) == 0 ||
1137 strncmp(dlna_pn, "MPEG4_P2_MP4", 12) == 0)) ||
1138 strcmp(mime+6, "x-matroska") == 0 ||
1139 strcmp(mime+6, "x-msvideo") == 0 ||
1140 strcmp(mime+6, "mpeg") == 0 )
1142 strcpy(mime+6, "avi");
1143 if( !dlna_pn || strncmp(dlna_pn, "MPEG_PS_NTSC", 12) != 0 )
1145 sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_NTSC");
1146 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1147 resolution, dlna_buf, mime, detailID, ext, passed_args);
1149 if( !dlna_pn || strncmp(dlna_pn, "MPEG_PS_PAL", 11) != 0 )
1151 sprintf(dlna_buf, "DLNA.ORG_PN=%s;DLNA.ORG_OP=01;DLNA.ORG_CI=1", "MPEG_PS_PAL");
1152 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1153 resolution, dlna_buf, mime, detailID, ext, passed_args);
1156 break;
1157 case ESonyBravia:
1158 /* BRAVIA KDL-##*X### series TVs do natively support AVC/AC3 in TS, but
1159 require profile to be renamed (applies to _T and _ISO variants also) */
1160 if( dlna_pn &&
1161 (strncmp(dlna_pn, "AVC_TS_MP_SD_AC3", 16) == 0 ||
1162 strncmp(dlna_pn, "AVC_TS_MP_HD_AC3", 16) == 0 ||
1163 strncmp(dlna_pn, "AVC_TS_HP_HD_AC3", 16) == 0))
1165 sprintf(dlna_buf, "DLNA.ORG_PN=AVC_TS_HD_50_AC3%s", dlna_pn + 16);
1166 add_res(size, duration, bitrate, sampleFrequency, nrAudioChannels,
1167 resolution, dlna_buf, mime, detailID, ext, passed_args);
1169 break;
1170 case ESamsungSeriesCDE:
1171 case ELGDevice:
1172 case ELGNetCastDevice:
1173 case EAsusOPlay:
1174 default:
1175 if( passed_args->flags & FLAG_HAS_CAPTIONS )
1177 if( passed_args->flags & FLAG_CAPTION_RES )
1178 ret = strcatf(str, "&lt;res protocolInfo=\"http-get:*:text/srt:*\"&gt;"
1179 "http://%s:%d/Captions/%s.srt"
1180 "&lt;/res&gt;",
1181 lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
1182 if( passed_args->filter & FILTER_SEC_CAPTION_INFO_EX )
1183 ret = strcatf(str, "&lt;sec:CaptionInfoEx sec:type=\"srt\"&gt;"
1184 "http://%s:%d/Captions/%s.srt"
1185 "&lt;/sec:CaptionInfoEx&gt;",
1186 lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
1188 break;
1192 if( NON_ZERO(album_art) )
1194 /* Video and audio album art is handled differently */
1195 if( *mime == 'v' && (passed_args->filter & FILTER_RES) && !(passed_args->flags & FLAG_MS_PFS) ) {
1196 ret = strcatf(str, "&lt;res protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN\"&gt;"
1197 "http://%s:%d/AlbumArt/%s-%s.jpg"
1198 "&lt;/res&gt;",
1199 lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
1200 if (passed_args->client == ESamsungSeriesCDE ) {
1201 ret = strcatf(str, "&lt;res dlna:profileID=\"JPEG_SM\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\""
1202 " protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;"
1203 "DLNA.ORG_OP=01;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=%08X%024X\" resolution=\"320x320\"&gt;"
1204 "http://%s:%d/AlbumArt/%s-%s.jpg"
1205 "&lt;/res&gt;",
1206 DLNA_FLAG_DLNA_V1_5|DLNA_FLAG_TM_B|DLNA_FLAG_TM_I, 0,
1207 lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
1209 } else if( passed_args->filter & FILTER_UPNP_ALBUMARTURI ) {
1210 ret = strcatf(str, "&lt;upnp:albumArtURI");
1211 if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) {
1212 ret = strcatf(str, " dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"");
1214 ret = strcatf(str, "&gt;http://%s:%d/AlbumArt/%s-%s.jpg&lt;/upnp:albumArtURI&gt;",
1215 lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
1218 if( (passed_args->flags & FLAG_MS_PFS) && *mime == 'i' ) {
1219 if( passed_args->client == EMediaRoom && !album )
1220 ret = strcatf(str, "&lt;upnp:album&gt;%s&lt;/upnp:album&gt;", "[No Keywords]");
1222 /* EVA2000 doesn't seem to handle embedded thumbnails */
1223 if( !(passed_args->flags & FLAG_RESIZE_THUMBS) && NON_ZERO(tn) && IS_ZERO(rotate) ) {
1224 ret = strcatf(str, "&lt;upnp:albumArtURI&gt;"
1225 "http://%s:%d/Thumbnails/%s.jpg"
1226 "&lt;/upnp:albumArtURI&gt;",
1227 lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
1228 } else {
1229 ret = strcatf(str, "&lt;upnp:albumArtURI&gt;"
1230 "http://%s:%d/Resized/%s.jpg?width=160,height=160"
1231 "&lt;/upnp:albumArtURI&gt;",
1232 lan_addr[passed_args->iface].str, runtime_vars.port, detailID);
1235 ret = strcatf(str, "&lt;/item&gt;");
1237 else if( strncmp(class, "container", 9) == 0 )
1239 ret = strcatf(str, "&lt;container id=\"%s\" parentID=\"%s\" restricted=\"1\" ", id, parent);
1240 if( passed_args->filter & FILTER_SEARCHABLE ) {
1241 ret = strcatf(str, "searchable=\"%d\" ", check_magic_container(id, passed_args->flags) ? 0 : 1);
1243 if( passed_args->filter & FILTER_CHILDCOUNT ) {
1244 ret = strcatf(str, "childCount=\"%d\"", get_child_count(id, check_magic_container(id, passed_args->flags)));
1246 /* If the client calls for BrowseMetadata on root, we have to include our "upnp:searchClass"'s, unless they're filtered out */
1247 if( passed_args->requested == 1 && strcmp(id, "0") == 0 && (passed_args->filter & FILTER_UPNP_SEARCHCLASS) ) {
1248 ret = strcatf(str, "&gt;"
1249 "&lt;upnp:searchClass includeDerived=\"1\"&gt;object.item.audioItem&lt;/upnp:searchClass&gt;"
1250 "&lt;upnp:searchClass includeDerived=\"1\"&gt;object.item.imageItem&lt;/upnp:searchClass&gt;"
1251 "&lt;upnp:searchClass includeDerived=\"1\"&gt;object.item.videoItem&lt;/upnp:searchClass");
1253 ret = strcatf(str, "&gt;"
1254 "&lt;dc:title&gt;%s&lt;/dc:title&gt;"
1255 "&lt;upnp:class&gt;object.%s&lt;/upnp:class&gt;",
1256 title, class);
1257 if( (passed_args->filter & FILTER_UPNP_STORAGEUSED) || strcmp(class+10, "storageFolder") == 0 ) {
1258 /* TODO: Implement real folder size tracking */
1259 ret = strcatf(str, "&lt;upnp:storageUsed&gt;%s&lt;/upnp:storageUsed&gt;", (size ? size : "-1"));
1261 if( creator && (passed_args->filter & FILTER_DC_CREATOR) ) {
1262 ret = strcatf(str, "&lt;dc:creator&gt;%s&lt;/dc:creator&gt;", creator);
1264 if( genre && (passed_args->filter & FILTER_UPNP_GENRE) ) {
1265 ret = strcatf(str, "&lt;upnp:genre&gt;%s&lt;/upnp:genre&gt;", genre);
1267 if( artist && (passed_args->filter & FILTER_UPNP_ARTIST) ) {
1268 ret = strcatf(str, "&lt;upnp:artist&gt;%s&lt;/upnp:artist&gt;", artist);
1270 if( NON_ZERO(album_art) && (passed_args->filter & FILTER_UPNP_ALBUMARTURI) ) {
1271 ret = strcatf(str, "&lt;upnp:albumArtURI ");
1272 if( passed_args->filter & FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID ) {
1273 ret = strcatf(str, "dlna:profileID=\"JPEG_TN\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\"");
1275 ret = strcatf(str, "&gt;http://%s:%d/AlbumArt/%s-%s.jpg&lt;/upnp:albumArtURI&gt;",
1276 lan_addr[passed_args->iface].str, runtime_vars.port, album_art, detailID);
1278 if( passed_args->filter & FILTER_AV_MEDIA_CLASS ) {
1279 char class;
1280 if( strncmp(id, MUSIC_ID, sizeof(MUSIC_ID)) == 0 )
1281 class = 'M';
1282 else if( strncmp(id, VIDEO_ID, sizeof(VIDEO_ID)) == 0 )
1283 class = 'V';
1284 else if( strncmp(id, IMAGE_ID, sizeof(IMAGE_ID)) == 0 )
1285 class = 'P';
1286 else
1287 class = 0;
1288 if( class )
1289 ret = strcatf(str, "&lt;av:mediaClass xmlns:av=\"urn:schemas-sony-com:av\"&gt;"
1290 "%c&lt;/av:mediaClass&gt;", class);
1292 ret = strcatf(str, "&lt;/container&gt;");
1295 return 0;
1298 static void
1299 BrowseContentDirectory(struct upnphttp * h, const char * action)
1301 (void)action;
1302 static const char resp0[] =
1303 "<u:BrowseResponse "
1304 "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
1305 "<Result>"
1306 "&lt;DIDL-Lite"
1307 CONTENT_DIRECTORY_SCHEMAS;
1308 struct magic_container_s *magic;
1309 char *zErrMsg = NULL;
1310 char *sql, *ptr;
1311 struct Response args;
1312 struct string_s str;
1313 int totalMatches = 0;
1314 int ret;
1315 const char *ObjectID, *BrowseFlag;
1316 char *Filter, *SortCriteria;
1317 const char *objectid_sql = "o.OBJECT_ID";
1318 const char *parentid_sql = "o.PARENT_ID";
1319 const char *refid_sql = "o.REF_ID";
1320 char where[256] = "";
1321 char *orderBy = NULL;
1322 struct NameValueParserData data;
1323 int RequestedCount = 0;
1324 int StartingIndex = 0;
1326 memset(&args, 0, sizeof(args));
1327 memset(&str, 0, sizeof(str));
1329 ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
1331 ObjectID = GetValueFromNameValueList(&data, "ObjectID");
1332 Filter = GetValueFromNameValueList(&data, "Filter");
1333 BrowseFlag = GetValueFromNameValueList(&data, "BrowseFlag");
1334 SortCriteria = GetValueFromNameValueList(&data, "SortCriteria");
1336 if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) )
1337 RequestedCount = atoi(ptr);
1338 if( RequestedCount < 0 )
1340 SoapError(h, 402, "Invalid Args");
1341 goto browse_error;
1343 if( !RequestedCount )
1344 RequestedCount = -1;
1345 if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) )
1346 StartingIndex = atoi(ptr);
1347 if( StartingIndex < 0 )
1349 SoapError(h, 402, "Invalid Args");
1350 goto browse_error;
1352 if( !BrowseFlag || (strcmp(BrowseFlag, "BrowseDirectChildren") && strcmp(BrowseFlag, "BrowseMetadata")) )
1354 SoapError(h, 402, "Invalid Args");
1355 goto browse_error;
1357 if( !ObjectID && !(ObjectID = GetValueFromNameValueList(&data, "ContainerID")) )
1359 SoapError(h, 402, "Invalid Args");
1360 goto browse_error;
1363 str.data = malloc(DEFAULT_RESP_SIZE);
1364 str.size = DEFAULT_RESP_SIZE;
1365 str.off = sprintf(str.data, "%s", resp0);
1366 /* See if we need to include DLNA namespace reference */
1367 args.iface = h->iface;
1368 args.filter = set_filter_flags(Filter, h);
1369 if( args.filter & FILTER_DLNA_NAMESPACE )
1370 ret = strcatf(&str, DLNA_NAMESPACE);
1371 if( args.filter & FILTER_PV_SUBTITLE )
1372 ret = strcatf(&str, PV_NAMESPACE);
1373 if( args.filter & FILTER_SEC )
1374 ret = strcatf(&str, SEC_NAMESPACE);
1375 strcatf(&str, "&gt;\n");
1377 args.returned = 0;
1378 args.requested = RequestedCount;
1379 args.client = h->req_client ? h->req_client->type->type : 0;
1380 args.flags = h->req_client ? h->req_client->type->flags : 0;
1381 args.str = &str;
1382 DPRINTF(E_DEBUG, L_HTTP, "Browsing ContentDirectory:\n"
1383 " * ObjectID: %s\n"
1384 " * Count: %d\n"
1385 " * StartingIndex: %d\n"
1386 " * BrowseFlag: %s\n"
1387 " * Filter: %s\n"
1388 " * SortCriteria: %s\n",
1389 ObjectID, RequestedCount, StartingIndex,
1390 BrowseFlag, Filter, SortCriteria);
1392 if( strcmp(BrowseFlag+6, "Metadata") == 0 )
1394 const char *id = ObjectID;
1395 args.requested = 1;
1396 magic = in_magic_container(ObjectID, args.flags, &id);
1397 if (magic)
1399 if (magic->objectid_sql && strcmp(id, ObjectID) != 0)
1400 objectid_sql = magic->objectid_sql;
1401 if (magic->parentid_sql && strcmp(id, ObjectID) != 0)
1402 parentid_sql = magic->parentid_sql;
1403 if (magic->refid_sql)
1404 refid_sql = magic->refid_sql;
1406 sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS
1407 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
1408 " where OBJECT_ID = '%q';",
1409 objectid_sql, parentid_sql, refid_sql, id);
1410 ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
1411 totalMatches = args.returned;
1413 else
1415 magic = check_magic_container(ObjectID, args.flags);
1416 if (magic)
1418 if (magic->objectid && *(magic->objectid))
1419 ObjectID = *(magic->objectid);
1420 if (magic->objectid_sql)
1421 objectid_sql = magic->objectid_sql;
1422 if (magic->parentid_sql)
1423 parentid_sql = magic->parentid_sql;
1424 if (magic->refid_sql)
1425 refid_sql = magic->refid_sql;
1426 if (magic->where)
1427 strncpyt(where, magic->where, sizeof(where));
1428 if (magic->orderby && !GETFLAG(DLNA_STRICT_MASK))
1429 orderBy = strdup(magic->orderby);
1430 if (magic->max_count > 0)
1432 int limit = MAX(magic->max_count - StartingIndex, 0);
1433 ret = get_child_count(ObjectID, magic);
1434 totalMatches = MIN(ret, limit);
1435 if (RequestedCount > limit || RequestedCount < 0)
1436 RequestedCount = limit;
1439 if (!where[0])
1440 sqlite3_snprintf(sizeof(where), where, "PARENT_ID = '%q'", ObjectID);
1442 if (!totalMatches)
1443 totalMatches = get_child_count(ObjectID, magic);
1444 ret = 0;
1445 if (SortCriteria && !orderBy)
1447 __SORT_LIMIT
1448 orderBy = parse_sort_criteria(SortCriteria, &ret);
1450 else if (!orderBy)
1452 if( strncmp(ObjectID, MUSIC_PLIST_ID, strlen(MUSIC_PLIST_ID)) == 0 )
1454 if( strcmp(ObjectID, MUSIC_PLIST_ID) == 0 )
1455 ret = xasprintf(&orderBy, "order by d.TITLE");
1456 else
1457 ret = xasprintf(&orderBy, "order by length(OBJECT_ID), OBJECT_ID");
1459 else if( args.flags & FLAG_FORCE_SORT )
1461 __SORT_LIMIT
1462 ret = xasprintf(&orderBy, "order by o.CLASS, d.DISC, d.TRACK, d.TITLE");
1464 /* LG TV ordering bug */
1465 else if( args.client == ELGDevice )
1466 ret = xasprintf(&orderBy, "order by o.CLASS, d.TITLE");
1467 else
1468 orderBy = parse_sort_criteria(SortCriteria, &ret);
1469 if( ret == -1 )
1471 free(orderBy);
1472 orderBy = NULL;
1473 ret = 0;
1476 /* If it's a DLNA client, return an error for bad sort criteria */
1477 if( ret < 0 && ((args.flags & FLAG_DLNA) || GETFLAG(DLNA_STRICT_MASK)) )
1479 SoapError(h, 709, "Unsupported or invalid sort criteria");
1480 goto browse_error;
1483 sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS
1484 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
1485 " where %s %s limit %d, %d;",
1486 objectid_sql, parentid_sql, refid_sql,
1487 where, THISORNUL(orderBy), StartingIndex, RequestedCount);
1488 DPRINTF(E_DEBUG, L_HTTP, "Browse SQL: %s\n", sql);
1489 ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
1491 if( ret != SQLITE_OK )
1493 if( args.flags & RESPONSE_TRUNCATED )
1495 sqlite3_free(zErrMsg);
1497 else
1499 DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql);
1500 sqlite3_free(zErrMsg);
1501 SoapError(h, 709, "Unsupported or invalid sort criteria");
1502 goto browse_error;
1505 sqlite3_free(sql);
1506 /* Does the object even exist? */
1507 if( !totalMatches )
1509 if( !object_exists(ObjectID) )
1511 SoapError(h, 701, "No such object error");
1512 goto browse_error;
1515 ret = strcatf(&str, "&lt;/DIDL-Lite&gt;</Result>\n"
1516 "<NumberReturned>%u</NumberReturned>\n"
1517 "<TotalMatches>%u</TotalMatches>\n"
1518 "<UpdateID>%u</UpdateID>"
1519 "</u:BrowseResponse>",
1520 args.returned, totalMatches, updateID);
1521 BuildSendAndCloseSoapResp(h, str.data, str.off);
1522 browse_error:
1523 ClearNameValueList(&data);
1524 free(orderBy);
1525 free(str.data);
1528 static inline void
1529 charcat(struct string_s *str, char c)
1531 if (str->size <= str->off)
1533 str->data[str->size-1] = '\0';
1534 return;
1536 str->data[str->off] = c;
1537 str->off += 1;
1540 static inline char *
1541 parse_search_criteria(const char *str, char *sep)
1543 struct string_s criteria;
1544 int len;
1545 int literal = 0, like = 0, class = 0;
1546 const char *s;
1548 if (!str)
1549 return strdup("1 = 1");
1551 len = strlen(str) + 32;
1552 criteria.data = malloc(len);
1553 criteria.size = len;
1554 criteria.off = 0;
1556 s = str;
1558 while (isspace(*s))
1559 s++;
1561 while (*s)
1563 if (literal)
1565 switch (*s) {
1566 case '&':
1567 if (strncmp(s, "&quot;", 6) == 0)
1568 s += 5;
1569 else if (strncmp(s, "&apos;", 6) == 0)
1571 strcatf(&criteria, "'");
1572 s += 6;
1573 continue;
1575 else
1576 break;
1577 case '"':
1578 literal = 0;
1579 if (like)
1581 charcat(&criteria, '%');
1582 like--;
1584 charcat(&criteria, '"');
1585 break;
1586 case '\\':
1587 if (strncmp(s, "\\&quot;", 7) == 0)
1589 strcatf(&criteria, "&amp;quot;");
1590 s += 7;
1591 continue;
1593 break;
1594 case 'o':
1595 if (class)
1597 class = 0;
1598 if (strncmp(s, "object.", 7) == 0)
1599 s += 7;
1600 else if (strncmp(s, "object\"", 7) == 0 ||
1601 strncmp(s, "object&quot;", 12) == 0)
1603 s += 6;
1604 continue;
1607 default:
1608 charcat(&criteria, *s);
1609 break;
1612 else
1614 switch (*s) {
1615 case '\\':
1616 if (strncmp(s, "\\&quot;", 7) == 0)
1618 strcatf(&criteria, "&amp;quot;");
1619 s += 7;
1620 continue;
1622 else
1623 charcat(&criteria, *s);
1624 break;
1625 case '"':
1626 literal = 1;
1627 charcat(&criteria, *s);
1628 if (like == 2)
1630 charcat(&criteria, '%');
1631 like--;
1633 break;
1634 case '&':
1635 if (strncmp(s, "&quot;", 6) == 0)
1637 literal = 1;
1638 strcatf(&criteria, "\"");
1639 if (like == 2)
1641 charcat(&criteria, '%');
1642 like--;
1644 s += 5;
1646 else if (strncmp(s, "&apos;", 6) == 0)
1648 strcatf(&criteria, "'");
1649 s += 5;
1651 else if (strncmp(s, "&lt;", 4) == 0)
1653 strcatf(&criteria, "<");
1654 s += 3;
1656 else if (strncmp(s, "&gt;", 4) == 0)
1658 strcatf(&criteria, ">");
1659 s += 3;
1661 else
1662 charcat(&criteria, *s);
1663 break;
1664 case '@':
1665 if (strncmp(s, "@refID", 6) == 0)
1667 strcatf(&criteria, "REF_ID");
1668 s += 6;
1669 continue;
1671 else if (strncmp(s, "@id", 3) == 0)
1673 strcatf(&criteria, "OBJECT_ID");
1674 s += 3;
1675 continue;
1677 else if (strncmp(s, "@parentID", 9) == 0)
1679 strcatf(&criteria, "PARENT_ID");
1680 s += 9;
1681 strcpy(sep, "*");
1682 continue;
1684 else
1685 charcat(&criteria, *s);
1686 break;
1687 case 'c':
1688 if (strncmp(s, "contains", 8) == 0)
1690 strcatf(&criteria, "like");
1691 s += 8;
1692 like = 2;
1693 continue;
1695 else
1696 charcat(&criteria, *s);
1697 break;
1698 case 'd':
1699 if (strncmp(s, "derivedfrom", 11) == 0)
1701 strcatf(&criteria, "like");
1702 s += 11;
1703 like = 1;
1704 continue;
1706 else if (strncmp(s, "dc:date", 7) == 0)
1708 strcatf(&criteria, "d.DATE");
1709 s += 7;
1710 continue;
1712 else if (strncmp(s, "dc:title", 8) == 0)
1714 strcatf(&criteria, "d.TITLE");
1715 s += 8;
1716 continue;
1718 else if (strncmp(s, "dc:creator", 10) == 0)
1720 strcatf(&criteria, "d.CREATOR");
1721 s += 10;
1722 continue;
1724 else
1725 charcat(&criteria, *s);
1726 break;
1727 case 'e':
1728 if (strncmp(s, "exists", 6) == 0)
1730 s += 6;
1731 while (isspace(*s))
1732 s++;
1733 if (strncmp(s, "true", 4) == 0)
1735 strcatf(&criteria, "is not NULL");
1736 s += 3;
1738 else if (strncmp(s, "false", 5) == 0)
1740 strcatf(&criteria, "is NULL");
1741 s += 4;
1744 else
1745 charcat(&criteria, *s);
1746 break;
1747 case 'o':
1748 if (class)
1750 if (strncmp(s, "object.", 7) == 0)
1752 s += 7;
1753 charcat(&criteria, '"');
1754 while (*s && !isspace(*s))
1756 charcat(&criteria, *s);
1757 s++;
1759 charcat(&criteria, '"');
1761 class = 0;
1762 continue;
1764 case 'u':
1765 if (strncmp(s, "upnp:class", 10) == 0)
1767 strcatf(&criteria, "o.CLASS");
1768 s += 10;
1769 class = 1;
1770 continue;
1772 else if (strncmp(s, "upnp:actor", 10) == 0)
1774 strcatf(&criteria, "d.ARTIST");
1775 s += 10;
1776 continue;
1778 else if (strncmp(s, "upnp:artist", 11) == 0)
1780 strcatf(&criteria, "d.ARTIST");
1781 s += 11;
1782 continue;
1784 else if (strncmp(s, "upnp:album", 10) == 0)
1786 strcatf(&criteria, "d.ALBUM");
1787 s += 10;
1788 continue;
1790 else if (strncmp(s, "upnp:genre", 10) == 0)
1792 strcatf(&criteria, "d.GENRE");
1793 s += 10;
1794 continue;
1796 else
1797 charcat(&criteria, *s);
1798 break;
1799 case '(':
1800 if (s > str && !isspace(s[-1]))
1801 charcat(&criteria, ' ');
1802 charcat(&criteria, *s);
1803 break;
1804 case ')':
1805 charcat(&criteria, *s);
1806 if (!isspace(s[1]))
1807 charcat(&criteria, ' ');
1808 break;
1809 default:
1810 charcat(&criteria, *s);
1811 break;
1814 s++;
1816 charcat(&criteria, '\0');
1818 return criteria.data;
1821 static void
1822 SearchContentDirectory(struct upnphttp * h, const char * action)
1824 (void)action;
1825 static const char resp0[] =
1826 "<u:SearchResponse "
1827 "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
1828 "<Result>"
1829 "&lt;DIDL-Lite"
1830 CONTENT_DIRECTORY_SCHEMAS;
1831 struct magic_container_s *magic;
1832 char *zErrMsg = NULL;
1833 char *sql, *ptr;
1834 struct Response args;
1835 struct string_s str;
1836 int totalMatches;
1837 int ret;
1838 const char *ContainerID;
1839 char *Filter, *SearchCriteria, *SortCriteria;
1840 char *orderBy = NULL, *where = NULL, sep[] = "$*";
1841 char groupBy[] = "group by DETAIL_ID";
1842 struct NameValueParserData data;
1843 int RequestedCount = 0;
1844 int StartingIndex = 0;
1846 memset(&args, 0, sizeof(args));
1847 memset(&str, 0, sizeof(str));
1849 ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
1851 ContainerID = GetValueFromNameValueList(&data, "ContainerID");
1852 Filter = GetValueFromNameValueList(&data, "Filter");
1853 SearchCriteria = GetValueFromNameValueList(&data, "SearchCriteria");
1854 SortCriteria = GetValueFromNameValueList(&data, "SortCriteria");
1856 if( (ptr = GetValueFromNameValueList(&data, "RequestedCount")) )
1857 RequestedCount = atoi(ptr);
1858 if( !RequestedCount )
1859 RequestedCount = -1;
1860 if( (ptr = GetValueFromNameValueList(&data, "StartingIndex")) )
1861 StartingIndex = atoi(ptr);
1862 if( !ContainerID )
1864 if( !(ContainerID = GetValueFromNameValueList(&data, "ObjectID")) )
1866 SoapError(h, 402, "Invalid Args");
1867 goto search_error;
1871 str.data = malloc(DEFAULT_RESP_SIZE);
1872 str.size = DEFAULT_RESP_SIZE;
1873 str.off = sprintf(str.data, "%s", resp0);
1874 /* See if we need to include DLNA namespace reference */
1875 args.iface = h->iface;
1876 args.filter = set_filter_flags(Filter, h);
1877 if( args.filter & FILTER_DLNA_NAMESPACE )
1879 ret = strcatf(&str, DLNA_NAMESPACE);
1881 strcatf(&str, "&gt;\n");
1883 args.returned = 0;
1884 args.requested = RequestedCount;
1885 args.client = h->req_client ? h->req_client->type->type : 0;
1886 args.flags = h->req_client ? h->req_client->type->flags : 0;
1887 args.str = &str;
1888 DPRINTF(E_DEBUG, L_HTTP, "Searching ContentDirectory:\n"
1889 " * ObjectID: %s\n"
1890 " * Count: %d\n"
1891 " * StartingIndex: %d\n"
1892 " * SearchCriteria: %s\n"
1893 " * Filter: %s\n"
1894 " * SortCriteria: %s\n",
1895 ContainerID, RequestedCount, StartingIndex,
1896 SearchCriteria, Filter, SortCriteria);
1898 magic = check_magic_container(ContainerID, args.flags);
1899 if (magic && magic->objectid && *(magic->objectid))
1900 ContainerID = *(magic->objectid);
1902 if( strcmp(ContainerID, "0") == 0 )
1903 ContainerID = "*";
1905 if( strcmp(ContainerID, MUSIC_ALL_ID) == 0 ||
1906 GETFLAG(DLNA_STRICT_MASK) )
1907 groupBy[0] = '\0';
1909 where = parse_search_criteria(SearchCriteria, sep);
1910 DPRINTF(E_DEBUG, L_HTTP, "Translated SearchCriteria: %s\n", where);
1912 totalMatches = sql_get_int_field(db, "SELECT (select count(distinct DETAIL_ID)"
1913 " from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
1914 " where (OBJECT_ID glob '%q%s') and (%s))"
1915 " + "
1916 "(select count(*) from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
1917 " where (OBJECT_ID = '%q') and (%s))",
1918 ContainerID, sep, where, ContainerID, where);
1919 if( totalMatches < 0 )
1921 /* Must be invalid SQL, so most likely bad or unhandled search criteria. */
1922 SoapError(h, 708, "Unsupported or invalid search criteria");
1923 goto search_error;
1925 /* Does the object even exist? */
1926 if( !totalMatches )
1928 if( !object_exists(ContainerID) )
1930 SoapError(h, 710, "No such container");
1931 goto search_error;
1934 ret = 0;
1935 __SORT_LIMIT
1936 orderBy = parse_sort_criteria(SortCriteria, &ret);
1937 /* If it's a DLNA client, return an error for bad sort criteria */
1938 if( ret < 0 && ((args.flags & FLAG_DLNA) || GETFLAG(DLNA_STRICT_MASK)) )
1940 SoapError(h, 709, "Unsupported or invalid sort criteria");
1941 goto search_error;
1944 sql = sqlite3_mprintf( SELECT_COLUMNS
1945 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
1946 " where OBJECT_ID glob '%q%s' and (%s) %s "
1947 "%z %s"
1948 " limit %d, %d",
1949 ContainerID, sep, where, groupBy,
1950 (*ContainerID == '*') ? NULL :
1951 sqlite3_mprintf("UNION ALL " SELECT_COLUMNS
1952 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
1953 " where OBJECT_ID = '%q' and (%s) ", ContainerID, where),
1954 orderBy, StartingIndex, RequestedCount);
1955 DPRINTF(E_DEBUG, L_HTTP, "Search SQL: %s\n", sql);
1956 ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
1957 if( ret != SQLITE_OK )
1959 if( !(args.flags & RESPONSE_TRUNCATED) )
1960 DPRINTF(E_WARN, L_HTTP, "SQL error: %s\nBAD SQL: %s\n", zErrMsg, sql);
1961 sqlite3_free(zErrMsg);
1963 sqlite3_free(sql);
1964 ret = strcatf(&str, "&lt;/DIDL-Lite&gt;</Result>\n"
1965 "<NumberReturned>%u</NumberReturned>\n"
1966 "<TotalMatches>%u</TotalMatches>\n"
1967 "<UpdateID>%u</UpdateID>"
1968 "</u:SearchResponse>",
1969 args.returned, totalMatches, updateID);
1970 BuildSendAndCloseSoapResp(h, str.data, str.off);
1971 search_error:
1972 ClearNameValueList(&data);
1973 free(orderBy);
1974 free(where);
1975 free(str.data);
1979 If a control point calls QueryStateVariable on a state variable that is not
1980 buffered in memory within (or otherwise available from) the service,
1981 the service must return a SOAP fault with an errorCode of 404 Invalid Var.
1983 QueryStateVariable remains useful as a limited test tool but may not be
1984 part of some future versions of UPnP.
1986 static void
1987 QueryStateVariable(struct upnphttp * h, const char * action)
1989 static const char resp[] =
1990 "<u:%sResponse "
1991 "xmlns:u=\"%s\">"
1992 "<return>%s</return>"
1993 "</u:%sResponse>";
1995 char body[512];
1996 struct NameValueParserData data;
1997 const char * var_name;
1999 ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
2000 /*var_name = GetValueFromNameValueList(&data, "QueryStateVariable"); */
2001 /*var_name = GetValueFromNameValueListIgnoreNS(&data, "varName");*/
2002 var_name = GetValueFromNameValueList(&data, "varName");
2004 DPRINTF(E_INFO, L_HTTP, "QueryStateVariable(%.40s)\n", var_name);
2006 if(!var_name)
2008 SoapError(h, 402, "Invalid Args");
2010 else if(strcmp(var_name, "ConnectionStatus") == 0)
2012 int bodylen;
2013 bodylen = snprintf(body, sizeof(body), resp,
2014 action, "urn:schemas-upnp-org:control-1-0",
2015 "Connected", action);
2016 BuildSendAndCloseSoapResp(h, body, bodylen);
2018 else
2020 DPRINTF(E_WARN, L_HTTP, "%s: Unknown: %s\n", action, THISORNUL(var_name));
2021 SoapError(h, 404, "Invalid Var");
2024 ClearNameValueList(&data);
2027 static int _set_watch_count(long long id, const char *old, const char *new)
2029 int64_t rowid = sqlite3_last_insert_rowid(db);
2030 int ret;
2032 ret = sql_exec(db, "INSERT or IGNORE into BOOKMARKS (ID, WATCH_COUNT)"
2033 " VALUES (%lld, %Q)", id, new ?: "1");
2034 if (sqlite3_last_insert_rowid(db) != rowid)
2035 return 0;
2037 if (!new) /* Increment */
2038 ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT ="
2039 " ifnull(WATCH_COUNT,'0') + 1"
2040 " where ID = %lld", id);
2041 else if (old && old[0])
2042 ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
2043 " where WATCH_COUNT = %Q and ID = %lld",
2044 new, old, id);
2045 else
2046 ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
2047 " where ID = %lld",
2048 new, id);
2049 return ret;
2052 /* For some reason, Kodi does URI encoding and appends a trailing slash */
2053 static void _kodi_decode(char *str)
2055 while (*str)
2057 switch (*str) {
2058 case '%':
2060 if (isxdigit(str[1]) && isxdigit(str[2]))
2062 char x[3] = { str[1], str[2], '\0' };
2063 *str++ = (char)strtol(x, NULL, 16);
2064 memmove(str, str+2, strlen(str+1));
2066 break;
2068 case '/':
2069 if (!str[1])
2070 *str = '\0';
2071 /* fall through */
2072 default:
2073 str++;
2074 break;
2079 static int duration_sec(const char *str)
2081 int hr, min, sec;
2083 if (sscanf(str, "%d:%d:%d", &hr, &min, &sec) == 3)
2084 return (hr * 3600) + (min * 60) + sec;
2086 return atoi(str);
2089 static void UpdateObject(struct upnphttp * h, const char * action)
2091 (void)action;
2092 static const char resp[] =
2093 "<u:UpdateObjectResponse"
2094 " xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
2095 "</u:UpdateObjecResponse>";
2097 struct NameValueParserData data;
2099 ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
2101 char *ObjectID = GetValueFromNameValueList(&data, "ObjectID");
2102 char *CurrentTagValue = GetValueFromNameValueList(&data, "CurrentTagValue");
2103 char *NewTagValue = GetValueFromNameValueList(&data, "NewTagValue");
2104 const char *rid = ObjectID;
2105 char tag[32], current[32], new[32];
2106 char *item, *saveptr = NULL;
2107 int64_t detailID;
2108 int ret = 1;
2110 if (!ObjectID || !CurrentTagValue || !NewTagValue)
2112 SoapError(h, 402, "Invalid Args");
2113 ClearNameValueList(&data);
2114 return;
2117 _kodi_decode(ObjectID);
2118 DPRINTF(E_DEBUG, L_HTTP, "UpdateObject %s: %s => %s\n", ObjectID, CurrentTagValue, NewTagValue);
2120 in_magic_container(ObjectID, 0, &rid);
2121 detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid);
2122 if (detailID <= 0)
2124 SoapError(h, 701, "No such object");
2125 ClearNameValueList(&data);
2126 return;
2129 for (item = strtok_r(CurrentTagValue, ",", &saveptr); item; item = strtok_r(NULL, ",", &saveptr))
2131 char *p;
2132 if (sscanf(item, "&lt;%31[^&]&gt;%31[^&]", tag, current) != 2)
2133 continue;
2134 p = strstr(NewTagValue, tag);
2135 if (!p || sscanf(p, "%*[^&]&gt;%31[^&]", new) != 1)
2136 continue;
2138 DPRINTF(E_DEBUG, L_HTTP, "Setting %s to %s\n", tag, new);
2139 /* Kodi uses incorrect tag "upnp:playCount" instead of "upnp:playbackCount" */
2140 if (strcmp(tag, "upnp:playbackCount") == 0 || strcmp(tag, "upnp:playCount") == 0)
2142 ret = _set_watch_count(detailID, current, new);
2144 else if (strcmp(tag, "upnp:lastPlaybackPosition") == 0)
2147 int sec = duration_sec(new);
2148 if( h->req_client && (h->req_client->type->flags & FLAG_CONVERT_MS) ) {
2149 sec /= 1000;
2151 if (sec < 30)
2152 sec = 0;
2153 else
2154 sec -= 1;
2155 ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)"
2156 " VALUES (%lld, %d)", (long long)detailID, sec);
2157 ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d"
2158 " where SEC = %Q and ID = %lld",
2159 sec, current, (long long)detailID);
2161 else
2162 DPRINTF(E_WARN, L_HTTP, "Tag %s unsupported for writing\n", tag);
2165 if (ret == SQLITE_OK)
2166 BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
2167 else
2168 SoapError(h, 501, "Action Failed");
2170 ClearNameValueList(&data);
2173 static void
2174 SamsungGetFeatureList(struct upnphttp * h, const char * action)
2176 (void)action;
2177 static const char resp[] =
2178 "<u:X_GetFeatureListResponse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
2179 "<FeatureList>"
2180 "&lt;Features xmlns=\"urn:schemas-upnp-org:av:avs\""
2181 " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
2182 " xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\"&gt;"
2183 "&lt;Feature name=\"samsung.com_BASICVIEW\" version=\"1\"&gt;"
2184 "&lt;container id=\"%s\" type=\"object.item.audioItem\"/&gt;"
2185 "&lt;container id=\"%s\" type=\"object.item.videoItem\"/&gt;"
2186 "&lt;container id=\"%s\" type=\"object.item.imageItem\"/&gt;"
2187 "&lt;/Feature&gt;"
2188 "&lt;/Features&gt;"
2189 "</FeatureList></u:X_GetFeatureListResponse>";
2190 const char *audio = MUSIC_ID;
2191 const char *video = VIDEO_ID;
2192 const char *image = IMAGE_ID;
2193 char body[1024];
2194 int len;
2196 if (runtime_vars.root_container)
2198 if (strcmp(runtime_vars.root_container, BROWSEDIR_ID) == 0)
2200 audio = MUSIC_DIR_ID;
2201 video = VIDEO_DIR_ID;
2202 image = IMAGE_DIR_ID;
2204 else
2206 audio = runtime_vars.root_container;
2207 video = runtime_vars.root_container;
2208 image = runtime_vars.root_container;
2211 else if (h->req_client && (h->req_client->type->flags & FLAG_SAMSUNG_DCM10))
2213 audio = "A";
2214 video = "V";
2215 image = "I";
2218 len = snprintf(body, sizeof(body), resp, audio, video, image);
2220 BuildSendAndCloseSoapResp(h, body, len);
2223 static void
2224 SamsungSetBookmark(struct upnphttp * h, const char * action)
2226 (void)action;
2227 static const char resp[] =
2228 "<u:X_SetBookmarkResponse"
2229 " xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
2230 "</u:X_SetBookmarkResponse>";
2232 struct NameValueParserData data;
2233 char *ObjectID, *PosSecond;
2235 ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0);
2236 ObjectID = GetValueFromNameValueList(&data, "ObjectID");
2237 PosSecond = GetValueFromNameValueList(&data, "PosSecond");
2239 if( ObjectID && PosSecond )
2241 const char *rid = ObjectID;
2242 int64_t detailID;
2243 int sec = atoi(PosSecond);
2244 int ret;
2246 in_magic_container(ObjectID, 0, &rid);
2247 detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid);
2249 if( h->req_client && (h->req_client->type->flags & FLAG_CONVERT_MS) ) {
2250 sec /= 1000;
2252 if ( sec < 30 )
2253 sec = 0;
2254 ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)"
2255 " VALUES (%lld, %d)", (long long)detailID, sec);
2256 ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d"
2257 " where ID = %lld",
2258 sec, (long long)detailID);
2259 if( ret != SQLITE_OK )
2260 DPRINTF(E_WARN, L_METADATA, "Error setting bookmark %s on ObjectID='%s'\n", PosSecond, rid);
2261 BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1);
2263 else
2264 SoapError(h, 402, "Invalid Args");
2266 ClearNameValueList(&data);
2269 static const struct
2271 const char * methodName;
2272 void (*methodImpl)(struct upnphttp *, const char *);
2274 soapMethods[] =
2276 { "QueryStateVariable", QueryStateVariable},
2277 { "Browse", BrowseContentDirectory},
2278 { "Search", SearchContentDirectory},
2279 { "GetSearchCapabilities", GetSearchCapabilities},
2280 { "GetSortCapabilities", GetSortCapabilities},
2281 { "GetSystemUpdateID", GetSystemUpdateID},
2282 { "GetProtocolInfo", GetProtocolInfo},
2283 { "GetCurrentConnectionIDs", GetCurrentConnectionIDs},
2284 { "GetCurrentConnectionInfo", GetCurrentConnectionInfo},
2285 { "IsAuthorized", IsAuthorizedValidated},
2286 { "IsValidated", IsAuthorizedValidated},
2287 { "RegisterDevice", RegisterDevice},
2288 { "UpdateObject", UpdateObject},
2289 { "X_GetFeatureList", SamsungGetFeatureList},
2290 { "X_SetBookmark", SamsungSetBookmark},
2291 { 0, 0 }
2294 void
2295 ExecuteSoapAction(struct upnphttp * h, const char * action, int n)
2297 char * p;
2299 p = strchr(action, '#');
2300 if(p)
2302 int i = 0;
2303 int len;
2304 int methodlen;
2305 char * p2;
2306 p++;
2307 p2 = strchr(p, '"');
2308 if(p2)
2309 methodlen = p2 - p;
2310 else
2311 methodlen = n - (p - action);
2312 DPRINTF(E_DEBUG, L_HTTP, "SoapMethod: %.*s\n", methodlen, p);
2313 while(soapMethods[i].methodName)
2315 len = strlen(soapMethods[i].methodName);
2316 if(strncmp(p, soapMethods[i].methodName, len) == 0)
2318 soapMethods[i].methodImpl(h, soapMethods[i].methodName);
2319 return;
2321 i++;
2324 DPRINTF(E_WARN, L_HTTP, "SoapMethod: Unknown: %.*s\n", methodlen, p);
2327 SoapError(h, 401, "Invalid Action");