1 /* MiniDLNA media server
2 * Copyright (C) 2009 Justin Maggard
4 * This file is part of MiniDLNA.
6 * MiniDLNA is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 2 as
8 * published by the Free Software Foundation.
10 * MiniDLNA is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
28 #include "tivo_utils.h"
29 #include "upnpglobalvars.h"
37 SendRootContainer(struct upnphttp
*h
)
42 len
= xasprintf(&resp
, "<?xml version='1.0' encoding='UTF-8' ?>\n"
45 "<ContentType>x-container/tivo-server</ContentType>"
46 "<SourceFormat>x-container/folder</SourceFormat>"
47 "<TotalDuration>0</TotalDuration>"
48 "<TotalItems>3</TotalItems>"
51 "<ItemStart>0</ItemStart>"
52 "<ItemCount>3</ItemCount>"
55 "<ContentType>x-container/tivo-photos</ContentType>"
56 "<SourceFormat>x-container/folder</SourceFormat>"
57 "<Title>Pictures on %s</Title>"
61 "<Url>/TiVoConnect?Command=QueryContainer&Container=3</Url>"
67 "<ContentType>x-container/tivo-music</ContentType>"
68 "<SourceFormat>x-container/folder</SourceFormat>"
69 "<Title>Music on %s</Title>"
73 "<Url>/TiVoConnect?Command=QueryContainer&Container=1</Url>"
79 "<ContentType>x-container/tivo-videos</ContentType>"
80 "<SourceFormat>x-container/folder</SourceFormat>"
81 "<Title>Videos on %s</Title>"
85 "<Url>/TiVoConnect?Command=QueryContainer&Container=2</Url>"
86 "<ContentType>x-container/tivo-videos</ContentType>"
91 friendly_name
, friendly_name
, friendly_name
, friendly_name
);
92 BuildResp_upnphttp(h
, resp
, len
);
98 SendFormats(struct upnphttp
*h
, const char *sformat
)
103 len
= xasprintf(&resp
, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
106 "<ContentType>video/x-tivo-mpeg</ContentType>"
110 "<ContentType>%s</ContentType>"
113 "</TiVoFormats>", sformat
);
114 BuildResp_upnphttp(h
, resp
, len
);
116 SendResp_upnphttp(h
);
120 tivo_unescape_tag(char *tag
)
122 modifyString(tag
, "&amp;", "&", 1);
123 modifyString(tag
, "&amp;lt;", "<", 1);
124 modifyString(tag
, "&lt;", "<", 1);
125 modifyString(tag
, "&amp;gt;", ">", 1);
126 modifyString(tag
, "&gt;", ">", 1);
127 modifyString(tag
, "&quot;", """, 1);
131 #define FLAG_SEND_RESIZED 0x01
132 #define FLAG_NO_PARAMS 0x02
133 #define FLAG_VIDEO 0x04
135 callback(void *args
, int argc
, char **argv
, char **azColName
)
137 struct Response
*passed_args
= (struct Response
*)args
;
138 char *id
= argv
[0], *class = argv
[1], *detailID
= argv
[2], *size
= argv
[3], *title
= argv
[4], *duration
= argv
[5],
139 *bitrate
= argv
[6], *sampleFrequency
= argv
[7], *artist
= argv
[8], *album
= argv
[9], *genre
= argv
[10],
140 *comment
= argv
[11], *date
= argv
[12], *resolution
= argv
[13], *mime
= argv
[14];
141 struct string_s
*str
= passed_args
->str
;
143 if( strncmp(class, "item", 4) == 0 )
146 tivo_unescape_tag(title
);
147 if( strncmp(mime
, "audio", 5) == 0 )
149 flags
|= FLAG_NO_PARAMS
;
150 strcatf(str
, "<Item><Details>"
151 "<ContentType>%s</ContentType>"
152 "<SourceFormat>%s</SourceFormat>"
153 "<SourceSize>%s</SourceSize>",
154 "audio/*", mime
, size
);
155 strcatf(str
, "<SongTitle>%s</SongTitle>", title
);
157 strcatf(str
, "<AlbumYear>%.*s</AlbumYear>", 4, date
);
159 else if( strcmp(mime
, "image/jpeg") == 0 )
161 flags
|= FLAG_SEND_RESIZED
;
162 strcatf(str
, "<Item><Details>"
163 "<ContentType>%s</ContentType>"
164 "<SourceFormat>%s</SourceFormat>"
165 "<SourceSize>%s</SourceSize>",
166 "image/*", mime
, size
);
170 memset(&tm
, 0, sizeof(tm
));
171 tm
.tm_isdst
= -1; // Have libc figure out if DST is in effect or not
172 strptime(date
, "%Y-%m-%dT%H:%M:%S", &tm
);
173 strcatf(str
, "<CaptureDate>0x%X</CaptureDate>", (unsigned int)mktime(&tm
));
176 strcatf(str
, "<Caption>%s</Caption>", comment
);
178 else if( strncmp(mime
, "video", 5) == 0 )
182 strcatf(str
, "<Item><Details>"
183 "<ContentType>%s</ContentType>"
184 "<SourceFormat>%s</SourceFormat>"
185 "<SourceSize>%s</SourceSize>",
187 episode
= strstr(title
, " - ");
190 strcatf(str
, "<EpisodeTitle>%s</EpisodeTitle>", episode
+3);
196 memset(&tm
, 0, sizeof(tm
));
197 tm
.tm_isdst
= -1; // Have libc figure out if DST is in effect or not
198 strptime(date
, "%Y-%m-%dT%H:%M:%S", &tm
);
199 strcatf(str
, "<CaptureDate>0x%X</CaptureDate>", (unsigned int)mktime(&tm
));
202 strcatf(str
, "<Description>%s</Description>", tivo_unescape_tag(comment
));
208 strcatf(str
, "<Title>%s</Title>", title
);
210 strcatf(str
, "<ArtistName>%s</ArtistName>", tivo_unescape_tag(artist
));
213 strcatf(str
, "<AlbumTitle>%s</AlbumTitle>", tivo_unescape_tag(album
));
216 strcatf(str
, "<MusicGenre>%s</MusicGenre>", tivo_unescape_tag(genre
));
219 char *width
= strsep(&resolution
, "x");
220 strcatf(str
, "<SourceWidth>%s</SourceWidth>"
221 "<SourceHeight>%s</SourceHeight>",
225 strcatf(str
, "<Duration>%d</Duration>",
226 atoi(strrchr(duration
, '.')+1) + (1000*atoi(strrchr(duration
, ':')+1))
227 + (60000*atoi(strrchr(duration
, ':')-2)) + (3600000*atoi(duration
)));
230 strcatf(str
, "<SourceBitRate>%s</SourceBitRate>", bitrate
);
232 if( sampleFrequency
) {
233 strcatf(str
, "<SourceSampleRate>%s</SourceSampleRate>", sampleFrequency
);
235 strcatf(str
, "</Details><Links>"
237 "<ContentType>%s</ContentType>"
238 "<Url>/%s/%s.%s</Url>%s"
241 (flags
& FLAG_SEND_RESIZED
) ? "Resized" : "MediaItems",
242 detailID
, mime_to_ext(mime
),
243 (flags
& FLAG_NO_PARAMS
) ? "<AcceptsParams>No</AcceptsParams>" : "");
244 if( flags
& FLAG_VIDEO
)
246 strcatf(str
, "<CustomIcon>"
247 "<ContentType>image/*</ContentType>"
248 "<Url>urn:tivo:image:save-until-i-delete-recording</Url>"
251 strcatf(str
, "</Links>");
253 else if( strncmp(class, "container", 9) == 0 )
256 /* Determine the number of children */
257 #ifdef __sparc__ /* Adding filters on large containers can take a long time on slow processors */
258 count
= sql_get_int_field(db
, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", id
);
260 count
= sql_get_int_field(db
, "SELECT count(*) from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID) where PARENT_ID = '%s' and "
261 " (MIME in ('image/jpeg', 'audio/mpeg', 'video/mpeg', 'video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts')"
262 " or CLASS glob 'container*')", id
);
264 strcatf(str
, "<Item>"
266 "<ContentType>x-container/folder</ContentType>"
267 "<SourceFormat>x-container/folder</SourceFormat>"
269 "<TotalItems>%d</TotalItems>"
273 "<Url>/TiVoConnect?Command=QueryContainer&Container=%s</Url>"
274 "<ContentType>x-tivo-container/folder</ContentType>"
277 tivo_unescape_tag(title
), count
, id
);
279 strcatf(str
, "</Item>");
281 passed_args
->returned
++;
286 #define SELECT_COLUMNS "SELECT o.OBJECT_ID, o.CLASS, o.DETAIL_ID, d.SIZE, d.TITLE," \
287 " d.DURATION, d.BITRATE, d.SAMPLERATE, d.ARTIST, d.ALBUM, d.GENRE," \
288 " d.COMMENT, d.DATE, d.RESOLUTION, d.MIME, d.DISC, d.TRACK "
291 SendItemDetails(struct upnphttp
*h
, int64_t item
)
294 char *zErrMsg
= NULL
;
295 struct Response args
;
298 memset(&args
, 0, sizeof(args
));
299 memset(&str
, 0, sizeof(str
));
301 str
.data
= malloc(32768);
303 str
.off
= sprintf(str
.data
, "<?xml version='1.0' encoding='UTF-8' ?>\n<TiVoItem>");
306 xasprintf(&sql
, SELECT_COLUMNS
307 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
308 " where o.DETAIL_ID = %lld group by o.DETAIL_ID", (long long)item
);
309 DPRINTF(E_DEBUG
, L_TIVO
, "%s\n", sql
);
310 ret
= sqlite3_exec(db
, sql
, callback
, (void *) &args
, &zErrMsg
);
312 if( ret
!= SQLITE_OK
)
314 DPRINTF(E_ERROR
, L_HTTP
, "SQL error: %s\n", zErrMsg
);
315 sqlite3_free(zErrMsg
);
317 strcatf(&str
, "</TiVoItem>");
319 BuildResp_upnphttp(h
, str
.data
, str
.off
);
321 SendResp_upnphttp(h
);
325 SendContainer(struct upnphttp
*h
, const char *objectID
, int itemStart
, int itemCount
, char *anchorItem
,
326 int anchorOffset
, int recurse
, char *sortOrder
, char *filter
, unsigned long int randomSeed
)
328 char *resp
= malloc(262144);
329 char *sql
, *item
, *saveptr
;
330 char *zErrMsg
= NULL
;
333 char what
[10], order
[96]={0}, order2
[96]={0}, myfilter
[256]={0};
336 char groupBy
[19] = {0};
337 struct Response args
;
339 int totalMatches
= 0;
341 memset(&args
, 0, sizeof(args
));
342 memset(&str
, 0, sizeof(str
));
345 str
.data
= resp
+1024;
346 str
.size
= 262144-1024;
349 args
.requested
= itemCount
;
353 if( itemCount
== -100 )
355 args
.requested
= itemCount
* -1;
361 strcpy(type
, "music");
364 strcpy(type
, "videos");
367 strcpy(type
, "photos");
370 strcpy(type
, "server");
374 if( strlen(objectID
) == 1 )
379 xasprintf(&title
, "Music on %s", friendly_name
);
382 xasprintf(&title
, "Videos on %s", friendly_name
);
385 xasprintf(&title
, "Pictures on %s", friendly_name
);
388 xasprintf(&title
, "Unknown on %s", friendly_name
);
394 item
= sql_get_text_field(db
, "SELECT NAME from OBJECTS where OBJECT_ID = '%q'", objectID
);
397 title
= escape_tag(item
, 1);
401 title
= strdup("UNKNOWN");
406 which
= sqlite3_mprintf("OBJECT_ID glob '%q$*'", objectID
);
407 strcpy(groupBy
, "group by DETAIL_ID");
411 which
= sqlite3_mprintf("PARENT_ID = '%q'", objectID
);
416 if( strcasestr(sortOrder
, "Random") )
418 sprintf(order
, "tivorandom(%lu)", randomSeed
);
420 sprintf(order2
, "tivorandom(%lu) DESC", randomSeed
);
422 sprintf(order2
, "tivorandom(%lu)", randomSeed
);
426 short title_state
= 0;
427 item
= strtok_r(sortOrder
, ",", &saveptr
);
428 while( item
!= NULL
)
436 if( strcasecmp(item
, "Type") == 0 )
438 strcat(order
, "CLASS");
439 strcat(order2
, "CLASS");
441 else if( strcasecmp(item
, "Title") == 0 )
443 /* Explicitly sort music by track then title. */
444 if( title_state
< 2 && *objectID
== '1' )
448 strcat(order
, "DISC");
449 strcat(order2
, "DISC");
454 strcat(order
, "TRACK");
455 strcat(order2
, "TRACK");
461 strcat(order
, "TITLE");
462 strcat(order2
, "TITLE");
466 else if( strcasecmp(item
, "CreationDate") == 0 ||
467 strcasecmp(item
, "CaptureDate") == 0 )
469 strcat(order
, "DATE");
470 strcat(order2
, "DATE");
474 DPRINTF(E_INFO
, L_TIVO
, "Unhandled SortOrder [%s]\n", item
);
475 goto unhandled_order
;
480 strcat(order
, " DESC");
482 strcat(order2
, " DESC");
484 strcat(order2
, " ASC");
488 strcat(order
, " ASC");
490 strcat(order2
, " ASC");
492 strcat(order2
, " DESC");
495 strcat(order2
, ", ");
497 if( title_state
<= 0 )
498 item
= strtok_r(NULL
, ",", &saveptr
);
500 if( title_state
!= -1 )
502 strcat(order
, "TITLE ASC, ");
504 strcat(order2
, "TITLE ASC, ");
506 strcat(order2
, "TITLE DESC, ");
508 strcat(order
, "DETAIL_ID ASC");
510 strcat(order2
, "DETAIL_ID ASC");
512 strcat(order2
, "DETAIL_ID DESC");
517 sprintf(order
, "CLASS, NAME, DETAIL_ID");
519 sprintf(order2
, "CLASS DESC, NAME DESC, DETAIL_ID DESC");
521 sprintf(order2
, "CLASS, NAME, DETAIL_ID");
526 item
= strtok_r(filter
, ",", &saveptr
);
527 for( i
=0; item
!= NULL
; i
++ )
531 strcat(myfilter
, " or ");
533 if( (strcasecmp(item
, "x-container/folder") == 0) ||
534 (strncasecmp(item
, "x-tivo-container/", 17) == 0) )
536 strcat(myfilter
, "CLASS glob 'container*'");
538 else if( strncasecmp(item
, "image", 5) == 0 )
540 strcat(myfilter
, "MIME = 'image/jpeg'");
542 else if( strncasecmp(item
, "audio", 5) == 0 )
544 strcat(myfilter
, "MIME = 'audio/mpeg'");
546 else if( strncasecmp(item
, "video", 5) == 0 )
548 strcat(myfilter
, "MIME in ('video/mpeg', 'video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts')");
552 DPRINTF(E_INFO
, L_TIVO
, "Unhandled Filter [%s]\n", item
);
555 ret
= strlen(myfilter
);
556 myfilter
[ret
-4] = '\0';
560 item
= strtok_r(NULL
, ",", &saveptr
);
565 strcpy(myfilter
, "MIME in ('image/jpeg', 'audio/mpeg', 'video/mpeg', 'video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts') or CLASS glob 'container*'");
570 if( strstr(anchorItem
, "QueryContainer") )
572 strcpy(what
, "OBJECT_ID");
573 saveptr
= strrchr(anchorItem
, '=');
575 anchorItem
= saveptr
+ 1;
579 strcpy(what
, "DETAIL_ID");
581 sqlite3Prng
.isInit
= 0;
582 sql
= sqlite3_mprintf("SELECT %s from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
585 " order by %s", what
, which
, myfilter
, groupBy
, order2
);
586 DPRINTF(E_DEBUG
, L_TIVO
, "%s\n", sql
);
587 if( (sql_get_table(db
, sql
, &result
, &ret
, NULL
) == SQLITE_OK
) && ret
)
589 for( i
=1; i
<=ret
; i
++ )
591 if( strcmp(anchorItem
, result
[i
]) == 0 )
594 itemStart
= ret
- i
+ itemCount
;
600 sqlite3_free_table(result
);
604 args
.start
= itemStart
+anchorOffset
;
605 sqlite3Prng
.isInit
= 0;
607 ret
= sql_get_int_field(db
, "SELECT count(distinct DETAIL_ID) "
608 "from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
609 " where %s and (%s)",
611 totalMatches
= (ret
> 0) ? ret
: 0;
612 if( itemCount
< 0 && !itemStart
&& !anchorOffset
)
614 args
.start
= totalMatches
+ itemCount
;
617 sql
= sqlite3_mprintf(SELECT_COLUMNS
618 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
621 " order by %s limit %d, %d",
622 which
, myfilter
, groupBy
, order
, args
.start
, args
.requested
);
623 DPRINTF(E_DEBUG
, L_TIVO
, "%s\n", sql
);
624 ret
= sqlite3_exec(db
, sql
, callback
, (void *) &args
, &zErrMsg
);
626 if( ret
!= SQLITE_OK
)
628 DPRINTF(E_ERROR
, L_HTTP
, "SQL error: %s\n", zErrMsg
);
629 sqlite3_free(zErrMsg
);
636 strcatf(&str
, "</TiVoContainer>");
638 ret
= sprintf(str_buf
, "<?xml version='1.0' encoding='UTF-8' ?>\n"
641 "<ContentType>x-container/tivo-%s</ContentType>"
642 "<SourceFormat>x-container/folder</SourceFormat>"
643 "<TotalItems>%d</TotalItems>"
646 "<ItemStart>%d</ItemStart>"
647 "<ItemCount>%d</ItemCount>",
648 type
, totalMatches
, title
, args
.start
, args
.returned
);
650 memcpy(str
.data
, &str_buf
, ret
);
651 str
.size
= str
.off
+ret
;
654 BuildResp_upnphttp(h
, str
.data
, str
.size
);
656 SendResp_upnphttp(h
);
660 ProcessTiVoCommand(struct upnphttp
*h
, const char *orig_path
)
664 char *saveptr
= NULL
, *item
;
665 char *command
= NULL
, *container
= NULL
, *anchorItem
= NULL
;
666 char *sortOrder
= NULL
, *filter
= NULL
, *sformat
= NULL
;
667 int64_t detailItem
=0;
668 int itemStart
=0, itemCount
=-100, anchorOffset
=0, recurse
=0;
669 unsigned long int randomSeed
=0;
671 path
= strdup(orig_path
);
672 DPRINTF(E_DEBUG
, L_GENERAL
, "Processing TiVo command %s\n", path
);
674 item
= strtok_r( path
, "&", &saveptr
);
675 while( item
!= NULL
)
679 item
= strtok_r( NULL
, "&", &saveptr
);
682 decodeString(item
, 1);
684 key
= strsep(&val
, "=");
685 decodeString(val
, 1);
686 DPRINTF(E_DEBUG
, L_GENERAL
, "%s: %s\n", key
, val
);
687 if( strcasecmp(key
, "Command") == 0 )
691 else if( strcasecmp(key
, "Container") == 0 )
695 else if( strcasecmp(key
, "ItemStart") == 0 )
697 itemStart
= atoi(val
);
699 else if( strcasecmp(key
, "ItemCount") == 0 )
701 itemCount
= atoi(val
);
703 else if( strcasecmp(key
, "AnchorItem") == 0 )
705 anchorItem
= basename(val
);
707 else if( strcasecmp(key
, "AnchorOffset") == 0 )
709 anchorOffset
= atoi(val
);
711 else if( strcasecmp(key
, "Recurse") == 0 )
713 recurse
= strcasecmp("yes", val
) == 0 ? 1 : 0;
715 else if( strcasecmp(key
, "SortOrder") == 0 )
719 else if( strcasecmp(key
, "Filter") == 0 )
723 else if( strcasecmp(key
, "RandomSeed") == 0 )
725 randomSeed
= strtoul(val
, NULL
, 10);
727 else if( strcasecmp(key
, "Url") == 0 )
730 detailItem
= strtoll(basename(val
), NULL
, 10);
732 else if( strcasecmp(key
, "SourceFormat") == 0 )
736 else if( strcasecmp(key
, "Format") == 0 || // Only send XML
737 strcasecmp(key
, "SerialNum") == 0 || // Unused for now
738 strcasecmp(key
, "DoGenres") == 0 ) // Not sure what this is, so ignore it
744 DPRINTF(E_DEBUG
, L_GENERAL
, "Unhandled parameter [%s]\n", key
);
746 item
= strtok_r( NULL
, "&", &saveptr
);
750 strip_ext(anchorItem
);
755 if( strcmp(command
, "QueryContainer") == 0 )
757 if( !container
|| (strcmp(container
, "/") == 0) )
759 SendRootContainer(h
);
763 SendContainer(h
, container
, itemStart
, itemCount
, anchorItem
,
764 anchorOffset
, recurse
, sortOrder
, filter
, randomSeed
);
767 else if( strcmp(command
, "QueryItem") == 0 )
769 SendItemDetails(h
, detailItem
);
771 else if( strcmp(command
, "QueryFormats") == 0 )
773 SendFormats(h
, sformat
);
777 DPRINTF(E_DEBUG
, L_GENERAL
, "Unhandled command [%s]\n", command
);
784 CloseSocket_upnphttp(h
);
786 #endif // TIVO_SUPPORT