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.
54 #include <sys/socket.h>
58 #include <sys/types.h>
59 #include <arpa/inet.h>
60 #include <netinet/in.h>
65 #include "upnpglobalvars.h"
69 #include "containers.h"
70 #include "upnpreplyparse.h"
71 #include "getifaddr.h"
76 #ifdef __sparc__ /* Sorting takes too long on slow processors with very large containers */
77 # define __SORT_LIMIT if( totalMatches < 10000 )
81 #define NON_ZERO(x) (x && atoi(x))
82 #define IS_ZERO(x) (!x || !atoi(x))
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__)
104 _SoapError(struct upnphttp
* h
, int errCode
, const char * errDesc
, const char *func
)
106 static const char resp
[] =
108 "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
109 "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
112 "<faultcode>s:Client</faultcode>"
113 "<faultstring>UPnPError</faultstring>"
115 "<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\">"
116 "<errorCode>%d</errorCode>"
117 "<errorDescription>%s</errorDescription>"
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
);
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/\">"
144 static const char afterbody
[] =
148 if (!body
|| bodylen
< 0)
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
);
171 GetSystemUpdateID(struct upnphttp
* h
, const char * action
)
173 static const char resp
[] =
182 bodylen
= snprintf(body
, sizeof(body
), resp
,
183 action
, "urn:schemas-upnp-org:service:ContentDirectory:1",
185 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
189 IsAuthorizedValidated(struct upnphttp
* h
, const char * action
)
191 static const char resp
[] =
194 "<Result>%d</Result>"
198 struct NameValueParserData data
;
201 ParseNameValue(h
->req_buf
+ h
->req_contentoff
, h
->req_contentlen
, &data
, XML_STORE_EMPTY_FL
);
202 id
= GetValueFromNameValueList(&data
, "DeviceID");
206 bodylen
= snprintf(body
, sizeof(body
), resp
,
207 action
, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
209 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
212 SoapError(h
, 402, "Invalid Args");
214 ClearNameValueList(&data
);
218 RegisterDevice(struct upnphttp
* h
, const char * action
)
220 static const char resp
[] =
223 "<RegistrationRespMsg>%s</RegistrationRespMsg>"
229 bodylen
= snprintf(body
, sizeof(body
), resp
,
230 action
, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
232 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
236 GetProtocolInfo(struct upnphttp
* h
, const char * action
)
238 static const char resp
[] =
242 RESOURCE_PROTOCOL_INFO_VALUES
250 bodylen
= asprintf(&body
, resp
,
251 action
, "urn:schemas-upnp-org:service:ConnectionManager:1",
253 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
258 GetSortCapabilities(struct upnphttp
* h
, const char * action
)
260 static const char resp
[] =
268 "upnp:episodeNumber,"
269 "upnp:originalTrackNumber"
276 bodylen
= snprintf(body
, sizeof(body
), resp
,
277 action
, "urn:schemas-upnp-org:service:ContentDirectory:1",
279 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
283 GetSearchCapabilities(struct upnphttp
* h
, const char * action
)
285 static const char resp
[] =
286 "<u:%sResponse xmlns:u=\"%s\">"
305 bodylen
= snprintf(body
, sizeof(body
), resp
,
306 action
, "urn:schemas-upnp-org:service:ContentDirectory:1",
308 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
312 GetCurrentConnectionIDs(struct upnphttp
* h
, const char * action
)
314 /* TODO: Use real data. - JM */
315 static const char resp
[] =
318 "<ConnectionIDs>0</ConnectionIDs>"
324 bodylen
= snprintf(body
, sizeof(body
), resp
,
325 action
, "urn:schemas-upnp-org:service:ConnectionManager:1",
327 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
331 GetCurrentConnectionInfo(struct upnphttp
* h
, const char * action
)
333 /* TODO: Use real data. - JM */
334 static const char resp
[] =
338 "<AVTransportID>-1</AVTransportID>"
339 "<ProtocolInfo></ProtocolInfo>"
340 "<PeerConnectionManager></PeerConnectionManager>"
341 "<PeerConnectionID>-1</PeerConnectionID>"
342 "<Direction>Output</Direction>"
343 "<Status>Unknown</Status>"
346 char body
[sizeof(resp
)+128];
347 struct NameValueParserData data
;
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
);
356 id
= strtol(id_str
, &endptr
, 10);
357 if (!id_str
|| endptr
== id_str
)
359 SoapError(h
, 402, "Invalid Args");
363 SoapError(h
, 701, "No such object error");
368 bodylen
= snprintf(body
, sizeof(body
), resp
,
369 action
, "urn:schemas-upnp-org:service:ConnectionManager:1",
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
414 #define STANDARD_FILTER_MASK 0x01FFFFFF
415 #define FILTER_BOOKMARK_MASK (FILTER_UPNP_PLAYBACKCOUNT | \
416 FILTER_UPNP_LASTPLAYBACKPOSITION | \
420 set_filter_flags(char *filter
, struct upnphttp
*h
)
422 char *item
, *saveptr
= NULL
;
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
;
430 flags
|= FILTER_SEC_CAPTION_INFO_EX
| FILTER_SEC_DCM_INFO
;
436 flags
|= FILTER_DLNA_NAMESPACE
;
437 item
= strtok_r(filter
, ",", &saveptr
);
438 while( item
!= NULL
)
442 while( isspace(*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
;
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 )
515 else if( (strcmp(item
, "res@bitrate") == 0) ||
516 (strcmp(item
, "@bitrate") == 0) ||
517 ((strcmp(item
, "bitrate") == 0) && (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
)) )
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
)) )
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
)) )
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
)) )
548 flags
|= FILTER_RES_SAMPLEFREQUENCY
;
550 else if( (strcmp(item
, "res@size") == 0) ||
551 (strcmp(item
, "@size") == 0) ||
552 (strcmp(item
, "size") == 0) )
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
);
600 parse_sort_criteria(char *sortCriteria
, int *error
)
603 char *item
, *saveptr
;
604 int i
, ret
, reverse
, title_sorted
= 0;
608 if( force_sort_criteria
)
609 sortCriteria
= strdup(force_sort_criteria
);
613 if( (item
= strtok_r(sortCriteria
, ",", &saveptr
)) )
615 order
= malloc(4096);
619 strcatf(&str
, "order by ");
621 for( i
= 0; item
!= NULL
; i
++ )
630 else if( *item
== '-' )
637 DPRINTF(E_ERROR
, L_HTTP
, "No order specified [%s]\n", item
);
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");
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");
668 DPRINTF(E_ERROR
, L_HTTP
, "Unhandled SortCriteria [%s]\n", item
);
677 goto unhandled_order
;
681 strcatf(&str
, " DESC");
683 item
= strtok_r(NULL
, ",", &saveptr
);
688 if( force_sort_criteria
)
692 /* Add a "tiebreaker" sort order */
694 strcatf(&str
, ", TITLE ASC");
696 if( force_sort_criteria
)
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
;
710 snprintf(buf
, sizeof(buf
), "%d", requested
);
713 if (NON_ZERO(track
) && !strstr(*title
, track
)) {
715 ret
= asprintf(alt_title
, "%0*d %s.%s %s",
716 pad
, returned
, disc
, track
, *title
);
718 ret
= asprintf(alt_title
, "%0*d %s %s",
719 pad
, returned
, track
, *title
);
722 ret
= asprintf(alt_title
, "%0*d %s", pad
, returned
, *title
);
732 add_resized_res(int srcw
, int srch
, int reqw
, int reqh
, char *dlna_pn
,
733 char *detailID
, struct Response
*args
)
738 if( (args
->flags
& FLAG_NO_RESIZE
) && reqw
> 160 && reqh
> 160 )
741 strcatf(args
->str
, "<res ");
742 if( args
->filter
& FILTER_RES_RESOLUTION
)
745 dsth
= ((((reqw
<<10)/srcw
)*srch
)>>10);
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\">"
754 "http://%s:%d/Resized/%s.jpg?width=%d,height=%d"
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
);
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
, "<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
)
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\">"
800 "http://%s:%d/MediaItems/%s.%s"
802 mime
, dlna_pn
, lan_addr
[args
->iface
].str
,
803 runtime_vars
.port
, detailID
, ext
);
807 get_child_count(const char *object
, struct magic_container_s
*magic
)
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
));
816 ret
= sql_get_int_field(db
, "SELECT count(*) from OBJECTS where PARENT_ID = '%q';", object
);
818 return (ret
> 0) ? ret
: 0;
822 object_exists(const char *object
)
825 ret
= sql_get_int_field(db
, "SELECT count(*) from OBJECTS where OBJECT_ID = '%q'",
826 strcmp(object
, "*") == 0 ? "0" : object
);
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
837 callback(void *args
, int argc
, char **argv
, char **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];
849 struct string_s
*str
= passed_args
->str
;
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
)
859 str
->data
= realloc(str
->data
, (str
->size
+DEFAULT_RESP_SIZE
));
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
);
868 DPRINTF(E_ERROR
, L_HTTP
, "UPnP SOAP response truncated, realloc failed\n");
869 passed_args
->flags
|= RESPONSE_TRUNCATED
;
872 #if MAX_RESPONSE_SIZE > 0
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
;
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 */
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 )
902 strcpy(mime
+6, "divx");
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
);
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 )
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
);
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");
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
)
997 snprintf(dlna_buf
, sizeof(dlna_buf
), "DLNA.ORG_PN=%s;"
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;"
1005 "DLNA.ORG_FLAGS=%08X%024X",
1008 strcpy(dlna_buf
, "*");
1010 ret
= strcatf(str
, "<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
, ">"
1015 "<dc:title>%s</dc:title>"
1016 "<upnp:class>object.%s</upnp:class>",
1018 if( comment
&& (passed_args
->filter
& FILTER_DC_DESCRIPTION
) ) {
1019 ret
= strcatf(str
, "<dc:description>%.384s</dc:description>", comment
);
1021 if( creator
&& (passed_args
->filter
& FILTER_DC_CREATOR
) ) {
1022 ret
= strcatf(str
, "<dc:creator>%s</dc:creator>", creator
);
1024 if( date
&& (passed_args
->filter
& FILTER_DC_DATE
) ) {
1025 ret
= strcatf(str
, "<dc:date>%s</dc:date>", date
);
1027 if( (passed_args
->filter
& FILTER_BOOKMARK_MASK
) ) {
1029 int sec
= sql_get_int_field(db
, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID
);
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
) {
1038 if( passed_args
->filter
& FILTER_UPNP_LASTPLAYBACKPOSITION
)
1039 ret
= strcatf(str
, "<upnp:lastPlaybackPosition>%d</upnp:lastPlaybackPosition>",
1041 if( passed_args
->filter
& FILTER_SEC_DCM_INFO
)
1042 ret
= strcatf(str
, "<sec:dcmInfo>CREATIONDATE=0,FOLDER=%s,BM=%d</sec:dcmInfo>",
1045 if( passed_args
->filter
& FILTER_UPNP_PLAYBACKCOUNT
) {
1046 ret
= strcatf(str
, "<upnp:playbackCount>%d</upnp:playbackCount>",
1047 sql_get_int_field(db
, "SELECT WATCH_COUNT from BOOKMARKS where ID = '%s'", detailID
));
1052 if( (*mime
== 'v') && (passed_args
->filter
& FILTER_UPNP_ACTOR
) ) {
1053 ret
= strcatf(str
, "<upnp:actor>%s</upnp:actor>", artist
);
1055 if( passed_args
->filter
& FILTER_UPNP_ARTIST
) {
1056 ret
= strcatf(str
, "<upnp:artist>%s</upnp:artist>", artist
);
1059 if( album
&& (passed_args
->filter
& FILTER_UPNP_ALBUM
) ) {
1060 ret
= strcatf(str
, "<upnp:album>%s</upnp:album>", album
);
1062 if( genre
&& (passed_args
->filter
& FILTER_UPNP_GENRE
) ) {
1063 ret
= strcatf(str
, "<upnp:genre>%s</upnp:genre>", 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
, "<upnp:originalTrackNumber>%s</upnp:originalTrackNumber>", track
);
1071 } else if( *mime
== 'v' ) {
1072 if( NON_ZERO(disc
) && (passed_args
->filter
& FILTER_UPNP_EPISODESEASON
) )
1073 ret
= strcatf(str
, "<upnp:episodeSeason>%s</upnp:episodeSeason>", disc
);
1074 if( passed_args
->filter
& FILTER_UPNP_EPISODENUMBER
)
1075 ret
= strcatf(str
, "<upnp:episodeNumber>%s</upnp:episodeNumber>", 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' ) {
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
, "<res protocolInfo=\"http-get:*:%s:%s\">"
1095 "http://%s:%d/Thumbnails/%s.jpg"
1097 mime
, "DLNA.ORG_PN=JPEG_TN;DLNA.ORG_CI=1", lan_addr
[passed_args
->iface
].str
,
1098 runtime_vars
.port
, detailID
);
1101 add_resized_res(srcw
, srch
, 160, 160, "JPEG_TN", detailID
, passed_args
);
1103 else if( *mime
== 'v' ) {
1104 switch( passed_args
->client
) {
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
);
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
);
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) */
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
);
1170 case ESamsungSeriesCDE
:
1172 case ELGNetCastDevice
:
1175 if( passed_args
->flags
& FLAG_HAS_CAPTIONS
)
1177 if( passed_args
->flags
& FLAG_CAPTION_RES
)
1178 ret
= strcatf(str
, "<res protocolInfo=\"http-get:*:text/srt:*\">"
1179 "http://%s:%d/Captions/%s.srt"
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
, "<sec:CaptionInfoEx sec:type=\"srt\">"
1184 "http://%s:%d/Captions/%s.srt"
1185 "</sec:CaptionInfoEx>",
1186 lan_addr
[passed_args
->iface
].str
, runtime_vars
.port
, detailID
);
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
, "<res protocolInfo=\"http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN\">"
1197 "http://%s:%d/AlbumArt/%s-%s.jpg"
1199 lan_addr
[passed_args
->iface
].str
, runtime_vars
.port
, album_art
, detailID
);
1200 if (passed_args
->client
== ESamsungSeriesCDE
) {
1201 ret
= strcatf(str
, "<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\">"
1204 "http://%s:%d/AlbumArt/%s-%s.jpg"
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
, "<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
, ">http://%s:%d/AlbumArt/%s-%s.jpg</upnp:albumArtURI>",
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
, "<upnp:album>%s</upnp:album>", "[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
, "<upnp:albumArtURI>"
1225 "http://%s:%d/Thumbnails/%s.jpg"
1226 "</upnp:albumArtURI>",
1227 lan_addr
[passed_args
->iface
].str
, runtime_vars
.port
, detailID
);
1229 ret
= strcatf(str
, "<upnp:albumArtURI>"
1230 "http://%s:%d/Resized/%s.jpg?width=160,height=160"
1231 "</upnp:albumArtURI>",
1232 lan_addr
[passed_args
->iface
].str
, runtime_vars
.port
, detailID
);
1235 ret
= strcatf(str
, "</item>");
1237 else if( strncmp(class, "container", 9) == 0 )
1239 ret
= strcatf(str
, "<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
, ">"
1249 "<upnp:searchClass includeDerived=\"1\">object.item.audioItem</upnp:searchClass>"
1250 "<upnp:searchClass includeDerived=\"1\">object.item.imageItem</upnp:searchClass>"
1251 "<upnp:searchClass includeDerived=\"1\">object.item.videoItem</upnp:searchClass");
1253 ret
= strcatf(str
, ">"
1254 "<dc:title>%s</dc:title>"
1255 "<upnp:class>object.%s</upnp: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
, "<upnp:storageUsed>%s</upnp:storageUsed>", (size
? size
: "-1"));
1261 if( creator
&& (passed_args
->filter
& FILTER_DC_CREATOR
) ) {
1262 ret
= strcatf(str
, "<dc:creator>%s</dc:creator>", creator
);
1264 if( genre
&& (passed_args
->filter
& FILTER_UPNP_GENRE
) ) {
1265 ret
= strcatf(str
, "<upnp:genre>%s</upnp:genre>", genre
);
1267 if( artist
&& (passed_args
->filter
& FILTER_UPNP_ARTIST
) ) {
1268 ret
= strcatf(str
, "<upnp:artist>%s</upnp:artist>", artist
);
1270 if( NON_ZERO(album_art
) && (passed_args
->filter
& FILTER_UPNP_ALBUMARTURI
) ) {
1271 ret
= strcatf(str
, "<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
, ">http://%s:%d/AlbumArt/%s-%s.jpg</upnp:albumArtURI>",
1276 lan_addr
[passed_args
->iface
].str
, runtime_vars
.port
, album_art
, detailID
);
1278 if( passed_args
->filter
& FILTER_AV_MEDIA_CLASS
) {
1280 if( strncmp(id
, MUSIC_ID
, sizeof(MUSIC_ID
)) == 0 )
1282 else if( strncmp(id
, VIDEO_ID
, sizeof(VIDEO_ID
)) == 0 )
1284 else if( strncmp(id
, IMAGE_ID
, sizeof(IMAGE_ID
)) == 0 )
1289 ret
= strcatf(str
, "<av:mediaClass xmlns:av=\"urn:schemas-sony-com:av\">"
1290 "%c</av:mediaClass>", class);
1292 ret
= strcatf(str
, "</container>");
1299 BrowseContentDirectory(struct upnphttp
* h
, const char * action
)
1302 static const char resp0
[] =
1303 "<u:BrowseResponse "
1304 "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
1307 CONTENT_DIRECTORY_SCHEMAS
;
1308 struct magic_container_s
*magic
;
1309 char *zErrMsg
= NULL
;
1311 struct Response args
;
1312 struct string_s str
;
1313 int totalMatches
= 0;
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");
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");
1352 if( !BrowseFlag
|| (strcmp(BrowseFlag
, "BrowseDirectChildren") && strcmp(BrowseFlag
, "BrowseMetadata")) )
1354 SoapError(h
, 402, "Invalid Args");
1357 if( !ObjectID
&& !(ObjectID
= GetValueFromNameValueList(&data
, "ContainerID")) )
1359 SoapError(h
, 402, "Invalid Args");
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
, ">\n");
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;
1382 DPRINTF(E_DEBUG
, L_HTTP
, "Browsing ContentDirectory:\n"
1385 " * StartingIndex: %d\n"
1386 " * BrowseFlag: %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
;
1396 magic
= in_magic_container(ObjectID
, args
.flags
, &id
);
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
;
1415 magic
= check_magic_container(ObjectID
, args
.flags
);
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
;
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
;
1440 sqlite3_snprintf(sizeof(where
), where
, "PARENT_ID = '%q'", ObjectID
);
1443 totalMatches
= get_child_count(ObjectID
, magic
);
1445 if (SortCriteria
&& !orderBy
)
1448 orderBy
= parse_sort_criteria(SortCriteria
, &ret
);
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");
1457 ret
= xasprintf(&orderBy
, "order by length(OBJECT_ID), OBJECT_ID");
1459 else if( args
.flags
& FLAG_FORCE_SORT
)
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");
1468 orderBy
= parse_sort_criteria(SortCriteria
, &ret
);
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");
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
);
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");
1506 /* Does the object even exist? */
1509 if( !object_exists(ObjectID
) )
1511 SoapError(h
, 701, "No such object error");
1515 ret
= strcatf(&str
, "</DIDL-Lite></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
);
1523 ClearNameValueList(&data
);
1529 charcat(struct string_s
*str
, char c
)
1531 if (str
->size
<= str
->off
)
1533 str
->data
[str
->size
-1] = '\0';
1536 str
->data
[str
->off
] = c
;
1540 static inline char *
1541 parse_search_criteria(const char *str
, char *sep
)
1543 struct string_s criteria
;
1545 int literal
= 0, like
= 0, class = 0;
1549 return strdup("1 = 1");
1551 len
= strlen(str
) + 32;
1552 criteria
.data
= malloc(len
);
1553 criteria
.size
= len
;
1567 if (strncmp(s
, """, 6) == 0)
1569 else if (strncmp(s
, "'", 6) == 0)
1571 strcatf(&criteria
, "'");
1581 charcat(&criteria
, '%');
1584 charcat(&criteria
, '"');
1587 if (strncmp(s
, "\\"", 7) == 0)
1589 strcatf(&criteria
, "&quot;");
1598 if (strncmp(s
, "object.", 7) == 0)
1600 else if (strncmp(s
, "object\"", 7) == 0 ||
1601 strncmp(s
, "object"", 12) == 0)
1608 charcat(&criteria
, *s
);
1616 if (strncmp(s
, "\\"", 7) == 0)
1618 strcatf(&criteria
, "&quot;");
1623 charcat(&criteria
, *s
);
1627 charcat(&criteria
, *s
);
1630 charcat(&criteria
, '%');
1635 if (strncmp(s
, """, 6) == 0)
1638 strcatf(&criteria
, "\"");
1641 charcat(&criteria
, '%');
1646 else if (strncmp(s
, "'", 6) == 0)
1648 strcatf(&criteria
, "'");
1651 else if (strncmp(s
, "<", 4) == 0)
1653 strcatf(&criteria
, "<");
1656 else if (strncmp(s
, ">", 4) == 0)
1658 strcatf(&criteria
, ">");
1662 charcat(&criteria
, *s
);
1665 if (strncmp(s
, "@refID", 6) == 0)
1667 strcatf(&criteria
, "REF_ID");
1671 else if (strncmp(s
, "@id", 3) == 0)
1673 strcatf(&criteria
, "OBJECT_ID");
1677 else if (strncmp(s
, "@parentID", 9) == 0)
1679 strcatf(&criteria
, "PARENT_ID");
1685 charcat(&criteria
, *s
);
1688 if (strncmp(s
, "contains", 8) == 0)
1690 strcatf(&criteria
, "like");
1696 charcat(&criteria
, *s
);
1699 if (strncmp(s
, "derivedfrom", 11) == 0)
1701 strcatf(&criteria
, "like");
1706 else if (strncmp(s
, "dc:date", 7) == 0)
1708 strcatf(&criteria
, "d.DATE");
1712 else if (strncmp(s
, "dc:title", 8) == 0)
1714 strcatf(&criteria
, "d.TITLE");
1718 else if (strncmp(s
, "dc:creator", 10) == 0)
1720 strcatf(&criteria
, "d.CREATOR");
1725 charcat(&criteria
, *s
);
1728 if (strncmp(s
, "exists", 6) == 0)
1733 if (strncmp(s
, "true", 4) == 0)
1735 strcatf(&criteria
, "is not NULL");
1738 else if (strncmp(s
, "false", 5) == 0)
1740 strcatf(&criteria
, "is NULL");
1745 charcat(&criteria
, *s
);
1750 if (strncmp(s
, "object.", 7) == 0)
1753 charcat(&criteria
, '"');
1754 while (*s
&& !isspace(*s
))
1756 charcat(&criteria
, *s
);
1759 charcat(&criteria
, '"');
1765 if (strncmp(s
, "upnp:class", 10) == 0)
1767 strcatf(&criteria
, "o.CLASS");
1772 else if (strncmp(s
, "upnp:actor", 10) == 0)
1774 strcatf(&criteria
, "d.ARTIST");
1778 else if (strncmp(s
, "upnp:artist", 11) == 0)
1780 strcatf(&criteria
, "d.ARTIST");
1784 else if (strncmp(s
, "upnp:album", 10) == 0)
1786 strcatf(&criteria
, "d.ALBUM");
1790 else if (strncmp(s
, "upnp:genre", 10) == 0)
1792 strcatf(&criteria
, "d.GENRE");
1797 charcat(&criteria
, *s
);
1800 if (s
> str
&& !isspace(s
[-1]))
1801 charcat(&criteria
, ' ');
1802 charcat(&criteria
, *s
);
1805 charcat(&criteria
, *s
);
1807 charcat(&criteria
, ' ');
1810 charcat(&criteria
, *s
);
1816 charcat(&criteria
, '\0');
1818 return criteria
.data
;
1822 SearchContentDirectory(struct upnphttp
* h
, const char * action
)
1825 static const char resp0
[] =
1826 "<u:SearchResponse "
1827 "xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
1830 CONTENT_DIRECTORY_SCHEMAS
;
1831 struct magic_container_s
*magic
;
1832 char *zErrMsg
= NULL
;
1834 struct Response args
;
1835 struct string_s str
;
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
);
1864 if( !(ContainerID
= GetValueFromNameValueList(&data
, "ObjectID")) )
1866 SoapError(h
, 402, "Invalid Args");
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
, ">\n");
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;
1888 DPRINTF(E_DEBUG
, L_HTTP
, "Searching ContentDirectory:\n"
1891 " * StartingIndex: %d\n"
1892 " * SearchCriteria: %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 )
1905 if( strcmp(ContainerID
, MUSIC_ALL_ID
) == 0 ||
1906 GETFLAG(DLNA_STRICT_MASK
) )
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))"
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");
1925 /* Does the object even exist? */
1928 if( !object_exists(ContainerID
) )
1930 SoapError(h
, 710, "No such container");
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");
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 "
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
);
1964 ret
= strcatf(&str
, "</DIDL-Lite></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
);
1972 ClearNameValueList(&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.
1987 QueryStateVariable(struct upnphttp
* h
, const char * action
)
1989 static const char resp
[] =
1992 "<return>%s</return>"
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
);
2008 SoapError(h
, 402, "Invalid Args");
2010 else if(strcmp(var_name
, "ConnectionStatus") == 0)
2013 bodylen
= snprintf(body
, sizeof(body
), resp
,
2014 action
, "urn:schemas-upnp-org:control-1-0",
2015 "Connected", action
);
2016 BuildSendAndCloseSoapResp(h
, body
, bodylen
);
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
);
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
)
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",
2046 ret
= sql_exec(db
, "UPDATE BOOKMARKS set WATCH_COUNT = %Q"
2052 /* For some reason, Kodi does URI encoding and appends a trailing slash */
2053 static void _kodi_decode(char *str
)
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));
2079 static int duration_sec(const char *str
)
2083 if (sscanf(str
, "%d:%d:%d", &hr
, &min
, &sec
) == 3)
2084 return (hr
* 3600) + (min
* 60) + sec
;
2089 static void UpdateObject(struct upnphttp
* h
, const char * 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
;
2110 if (!ObjectID
|| !CurrentTagValue
|| !NewTagValue
)
2112 SoapError(h
, 402, "Invalid Args");
2113 ClearNameValueList(&data
);
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
);
2124 SoapError(h
, 701, "No such object");
2125 ClearNameValueList(&data
);
2129 for (item
= strtok_r(CurrentTagValue
, ",", &saveptr
); item
; item
= strtok_r(NULL
, ",", &saveptr
))
2132 if (sscanf(item
, "<%31[^&]>%31[^&]", tag
, current
) != 2)
2134 p
= strstr(NewTagValue
, tag
);
2135 if (!p
|| sscanf(p
, "%*[^&]>%31[^&]", new) != 1)
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
) ) {
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
);
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);
2168 SoapError(h
, 501, "Action Failed");
2170 ClearNameValueList(&data
);
2174 SamsungGetFeatureList(struct upnphttp
* h
, const char * action
)
2177 static const char resp
[] =
2178 "<u:X_GetFeatureListResponse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
2180 "<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\">"
2183 "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
2184 "<container id=\"%s\" type=\"object.item.audioItem\"/>"
2185 "<container id=\"%s\" type=\"object.item.videoItem\"/>"
2186 "<container id=\"%s\" type=\"object.item.imageItem\"/>"
2189 "</FeatureList></u:X_GetFeatureListResponse>";
2190 const char *audio
= MUSIC_ID
;
2191 const char *video
= VIDEO_ID
;
2192 const char *image
= IMAGE_ID
;
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
;
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
))
2218 len
= snprintf(body
, sizeof(body
), resp
, audio
, video
, image
);
2220 BuildSendAndCloseSoapResp(h
, body
, len
);
2224 SamsungSetBookmark(struct upnphttp
* h
, const char * 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
;
2243 int sec
= atoi(PosSecond
);
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
) ) {
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"
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);
2264 SoapError(h
, 402, "Invalid Args");
2266 ClearNameValueList(&data
);
2271 const char * methodName
;
2272 void (*methodImpl
)(struct upnphttp
*, const char *);
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
},
2295 ExecuteSoapAction(struct upnphttp
* h
, const char * action
, int n
)
2299 p
= strchr(action
, '#');
2307 p2
= strchr(p
, '"');
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
);
2324 DPRINTF(E_WARN
, L_HTTP
, "SoapMethod: Unknown: %.*s\n", methodlen
, p
);
2327 SoapError(h
, 401, "Invalid Action");