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/>.
26 #include "tivo_utils.h"
27 #include "upnpglobalvars.h"
35 SendRootContainer(struct upnphttp
* h
)
40 len
= asprintf(&resp
, "<?xml version='1.0' encoding='UTF-8' ?>\n"
43 "<ContentType>x-container/tivo-server</ContentType>"
44 "<SourceFormat>x-container/folder</SourceFormat>"
45 "<TotalDuration>0</TotalDuration>"
46 "<TotalItems>3</TotalItems>"
49 "<ItemStart>0</ItemStart>"
50 "<ItemCount>3</ItemCount>"
53 "<ContentType>x-container/tivo-photos</ContentType>"
54 "<SourceFormat>x-container/folder</SourceFormat>"
55 "<Title>Pictures on %s</Title>"
59 "<Url>/TiVoConnect?Command=QueryContainer&Container=3</Url>"
65 "<ContentType>x-container/tivo-music</ContentType>"
66 "<SourceFormat>x-container/folder</SourceFormat>"
67 "<Title>Music on %s</Title>"
71 "<Url>/TiVoConnect?Command=QueryContainer&Container=1</Url>"
77 "<ContentType>x-container/tivo-videos</ContentType>"
78 "<SourceFormat>x-container/folder</SourceFormat>"
79 "<Title>Videos on %s</Title>"
83 "<Url>/TiVoConnect?Command=QueryContainer&Container=2</Url>"
84 "<ContentType>x-container/tivo-videos</ContentType>"
89 friendly_name
, friendly_name
, friendly_name
, friendly_name
);
90 BuildResp_upnphttp(h
, resp
, len
);
96 unescape_tag(char * tag
)
98 modifyString(tag
, "&amp;", "&", 0);
99 modifyString(tag
, "&amp;lt;", "<", 0);
100 modifyString(tag
, "&lt;", "<", 0);
101 modifyString(tag
, "&amp;gt;", ">", 0);
102 modifyString(tag
, "&gt;", ">", 0);
106 #define FLAG_SEND_RESIZED 0x01
107 #define FLAG_NO_PARAMS 0x02
108 #define FLAG_VIDEO 0x04
109 int callback(void *args
, int argc
, char **argv
, char **azColName
)
111 struct Response
*passed_args
= (struct Response
*)args
;
112 char *id
= argv
[0], *class = argv
[1], *detailID
= argv
[2], *size
= argv
[3], *title
= argv
[4], *duration
= argv
[5],
113 *bitrate
= argv
[6], *sampleFrequency
= argv
[7], *artist
= argv
[8], *album
= argv
[9], *genre
= argv
[10],
114 *comment
= argv
[11], *date
= argv
[12], *resolution
= argv
[13], *mime
= argv
[14], *path
= argv
[15];
115 struct string_s
*str
= passed_args
->str
;
117 if( strncmp(class, "item", 4) == 0 )
121 if( strncmp(mime
, "audio", 5) == 0 )
123 flags
|= FLAG_NO_PARAMS
;
124 strcatf(str
, "<Item><Details>"
125 "<ContentType>audio/*</ContentType>"
126 "<SourceFormat>%s</SourceFormat>"
127 "<SourceSize>%s</SourceSize>"
128 "<SongTitle>%s</SongTitle>", mime
, size
, title
);
131 strcatf(str
, "<AlbumYear>%.*s</AlbumYear>", 4, date
);
134 else if( strcmp(mime
, "image/jpeg") == 0 )
136 flags
|= FLAG_SEND_RESIZED
;
137 strcatf(str
, "<Item><Details>"
138 "<ContentType>image/*</ContentType>"
139 "<SourceFormat>image/jpeg</SourceFormat>"
140 "<SourceSize>%s</SourceSize>", size
);
144 memset(&tm
, 0, sizeof(tm
));
145 tm
.tm_isdst
= -1; // Have libc figure out if DST is in effect or not
146 strptime(date
, "%Y-%m-%dT%H:%M:%S", &tm
);
147 strcatf(str
, "<CaptureDate>0x%X</CaptureDate>", (unsigned int)mktime(&tm
));
151 strcatf(str
, "<Caption>%s</Caption>", comment
);
154 else if( strncmp(mime
, "video", 5) == 0 )
157 flags
|= FLAG_NO_PARAMS
;
159 strcatf(str
, "<Item><Details>"
160 "<ContentType>video/x-tivo-mpeg</ContentType>"
161 "<SourceFormat>%s</SourceFormat>"
162 "<SourceSize>%s</SourceSize>", mime
, size
);
163 episode
= strstr(title
, " - ");
166 strcatf(str
, "<Title>%.*s</Title>"
167 "<EpisodeTitle>%s</EpisodeTitle>",
168 (int)(episode
-title
), title
, episode
+3);
172 strcatf(str
, "<Title>%s</Title>", title
);
177 memset(&tm
, 0, sizeof(tm
));
178 tm
.tm_isdst
= -1; // Have libc figure out if DST is in effect or not
179 strptime(date
, "%Y-%m-%dT%H:%M:%S", &tm
);
180 strcatf(str
, "<CaptureDate>0x%X</CaptureDate>", (unsigned int)mktime(&tm
));
184 strcatf(str
, "<Description>%s</Description>", comment
);
191 strcatf(str
, "<Title>%s</Title>", unescape_tag(title
));
193 strcatf(str
, "<ArtistName>%s</ArtistName>", unescape_tag(artist
));
196 strcatf(str
, "<AlbumTitle>%s</AlbumTitle>", unescape_tag(album
));
199 strcatf(str
, "<MusicGenre>%s</MusicGenre>", unescape_tag(genre
));
202 char *width
= strsep(&resolution
, "x");
203 strcatf(str
, "<SourceWidth>%s</SourceWidth>"
204 "<SourceHeight>%s</SourceHeight>",
208 strcatf(str
, "<Duration>%d</Duration>",
209 atoi(strrchr(duration
, '.')+1) + (1000*atoi(strrchr(duration
, ':')+1))
210 + (60000*atoi(strrchr(duration
, ':')-2)) + (3600000*atoi(duration
)));
213 strcatf(str
, "<SourceBitRate>%s</SourceBitRate>", bitrate
);
215 if( sampleFrequency
) {
216 strcatf(str
, "<SourceSampleRate>%s</SourceSampleRate>", sampleFrequency
);
218 strcatf(str
, "</Details><Links><Content>"
219 "<ContentType>%s</ContentType>"
220 "<Url>/%s/%s.dat</Url>%s</Content>",
222 (flags
& FLAG_SEND_RESIZED
)?"Resized":"MediaItems", detailID
,
223 (flags
& FLAG_NO_PARAMS
)?"<AcceptsParams>No</AcceptsParams>":"");
224 if( flags
& FLAG_VIDEO
)
226 char *esc_name
= escape_tag(basename(path
), 1);
227 strcatf(str
, "<CustomIcon>"
228 "<ContentType>video/*</ContentType>"
229 "<Url>urn:tivo:image:save-until-i-delete-recording</Url>"
231 "<Push><Container>Videos</Container></Push>"
232 "<File>%s</File> </Links>", esc_name
);
237 strcatf(str
, "</Links>");
240 else if( strncmp(class, "container", 9) == 0 )
243 /* Determine the number of children */
244 #ifdef __sparc__ /* Adding filters on large containers can take a long time on slow processors */
245 count
= sql_get_int_field(db
, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", id
);
247 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 "
248 " (MIME in ('image/jpeg', 'audio/mpeg', 'video/mpeg', 'video/x-tivo-mpeg')"
249 " or CLASS glob 'container*')", id
);
251 strcatf(str
, "<Item>"
253 "<ContentType>x-container/folder</ContentType>"
254 "<SourceFormat>x-container/folder</SourceFormat>"
256 "<TotalItems>%d</TotalItems>"
260 "<Url>/TiVoConnect?Command=QueryContainer&Container=%s</Url>"
261 "<ContentType>x-tivo-container/folder</ContentType>"
264 unescape_tag(title
), count
, id
);
266 strcatf(str
, "</Item>");
268 passed_args
->returned
++;
273 #define SELECT_COLUMNS "SELECT o.OBJECT_ID, o.CLASS, o.DETAIL_ID, d.SIZE, d.TITLE," \
274 " d.DURATION, d.BITRATE, d.SAMPLERATE, d.ARTIST, d.ALBUM, d.GENRE," \
275 " d.COMMENT, d.DATE, d.RESOLUTION, d.MIME, d.PATH, d.DISC, d.TRACK "
278 SendItemDetails(struct upnphttp
* h
, sqlite_int64 item
)
281 char *zErrMsg
= NULL
;
282 struct Response args
;
285 memset(&args
, 0, sizeof(args
));
286 memset(&str
, 0, sizeof(str
));
288 str
.data
= malloc(32768);
290 str
.off
= sprintf(str
.data
, "<?xml version='1.0' encoding='UTF-8' ?>\n<TiVoItem>");
293 asprintf(&sql
, SELECT_COLUMNS
294 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
295 " where o.DETAIL_ID = %lld group by o.DETAIL_ID", item
);
296 DPRINTF(E_DEBUG
, L_TIVO
, "%s\n", sql
);
297 ret
= sqlite3_exec(db
, sql
, callback
, (void *) &args
, &zErrMsg
);
299 if( ret
!= SQLITE_OK
)
301 DPRINTF(E_ERROR
, L_HTTP
, "SQL error: %s\n", zErrMsg
);
302 sqlite3_free(zErrMsg
);
304 strcatf(&str
, "</TiVoItem>");
306 BuildResp_upnphttp(h
, str
.data
, str
.off
);
308 SendResp_upnphttp(h
);
312 SendContainer(struct upnphttp
* h
, const char * objectID
, int itemStart
, int itemCount
, char * anchorItem
,
313 int anchorOffset
, int recurse
, char * sortOrder
, char * filter
, unsigned long int randomSeed
)
315 char *resp
= malloc(262144);
316 char *sql
, *item
, *saveptr
;
317 char *zErrMsg
= NULL
;
320 char what
[10], order
[96]={0}, order2
[96]={0}, myfilter
[256]={0};
323 char groupBy
[19] = {0};
324 struct Response args
;
326 int totalMatches
= 0;
328 memset(&args
, 0, sizeof(args
));
329 memset(&str
, 0, sizeof(str
));
332 str
.data
= resp
+1024;
333 str
.size
= 262144-1024;
336 args
.requested
= itemCount
;
340 if( itemCount
== -100 )
342 args
.requested
= itemCount
* -1;
348 strcpy(type
, "music");
351 strcpy(type
, "videos");
354 strcpy(type
, "photos");
357 strcpy(type
, "server");
361 if( strlen(objectID
) == 1 )
366 asprintf(&title
, "Music on %s", friendly_name
);
369 asprintf(&title
, "Videos on %s", friendly_name
);
372 asprintf(&title
, "Pictures on %s", friendly_name
);
375 asprintf(&title
, "Unknown on %s", friendly_name
);
381 item
= sql_get_text_field(db
, "SELECT NAME from OBJECTS where OBJECT_ID = '%s'", objectID
);
384 title
= escape_tag(item
, 1);
388 title
= strdup("UNKNOWN");
393 asprintf(&which
, "OBJECT_ID glob '%s$*'", objectID
);
394 strcpy(groupBy
, "group by DETAIL_ID");
398 asprintf(&which
, "PARENT_ID = '%s'", objectID
);
403 if( strcasestr(sortOrder
, "Random") )
405 sprintf(order
, "tivorandom(%lu)", randomSeed
);
407 sprintf(order2
, "tivorandom(%lu) DESC", randomSeed
);
409 sprintf(order2
, "tivorandom(%lu)", randomSeed
);
413 short title_state
= 0;
414 item
= strtok_r(sortOrder
, ",", &saveptr
);
415 while( item
!= NULL
)
423 if( strcasecmp(item
, "Type") == 0 )
425 strcat(order
, "CLASS");
426 strcat(order2
, "CLASS");
428 else if( strcasecmp(item
, "Title") == 0 )
430 /* Explicitly sort music by track then title. */
431 if( title_state
< 2 && *objectID
== '1' )
435 strcat(order
, "DISC");
436 strcat(order2
, "DISC");
441 strcat(order
, "TRACK");
442 strcat(order2
, "TRACK");
448 strcat(order
, "TITLE");
449 strcat(order2
, "TITLE");
453 else if( strcasecmp(item
, "CreationDate") == 0 ||
454 strcasecmp(item
, "CaptureDate") == 0 )
456 strcat(order
, "DATE");
457 strcat(order2
, "DATE");
461 DPRINTF(E_INFO
, L_TIVO
, "Unhandled SortOrder [%s]\n", item
);
462 goto unhandled_order
;
467 strcat(order
, " DESC");
469 strcat(order2
, " DESC");
471 strcat(order2
, " ASC");
475 strcat(order
, " ASC");
477 strcat(order2
, " ASC");
479 strcat(order2
, " DESC");
482 strcat(order2
, ", ");
484 if( title_state
<= 0 )
485 item
= strtok_r(NULL
, ",", &saveptr
);
487 if( title_state
!= -1 )
489 strcat(order
, "TITLE ASC, ");
491 strcat(order2
, "TITLE ASC, ");
493 strcat(order2
, "TITLE DESC, ");
495 strcat(order
, "DETAIL_ID ASC");
497 strcat(order2
, "DETAIL_ID ASC");
499 strcat(order2
, "DETAIL_ID DESC");
504 sprintf(order
, "CLASS, NAME, DETAIL_ID");
506 sprintf(order2
, "CLASS DESC, NAME DESC, DETAIL_ID DESC");
508 sprintf(order2
, "CLASS, NAME, DETAIL_ID");
513 item
= strtok_r(filter
, ",", &saveptr
);
514 for( i
=0; item
!= NULL
; i
++ )
518 strcat(myfilter
, " or ");
520 if( (strcasecmp(item
, "x-container/folder") == 0) ||
521 (strncasecmp(item
, "x-tivo-container/", 17) == 0) )
523 strcat(myfilter
, "CLASS glob 'container*'");
525 else if( strncasecmp(item
, "image", 5) == 0 )
527 strcat(myfilter
, "MIME = 'image/jpeg'");
529 else if( strncasecmp(item
, "audio", 5) == 0 )
531 strcat(myfilter
, "MIME = 'audio/mpeg'");
533 else if( strncasecmp(item
, "video", 5) == 0 )
535 strcat(myfilter
, "MIME in ('video/mpeg', 'video/x-tivo-mpeg')");
539 DPRINTF(E_INFO
, L_TIVO
, "Unhandled Filter [%s]\n", item
);
542 ret
= strlen(myfilter
);
543 myfilter
[ret
-4] = '\0';
547 item
= strtok_r(NULL
, ",", &saveptr
);
552 strcpy(myfilter
, "MIME in ('image/jpeg', 'audio/mpeg', 'video/mpeg', 'video/x-tivo-mpeg') or CLASS glob 'container*'");
557 if( strstr(anchorItem
, "QueryContainer") )
559 strcpy(what
, "OBJECT_ID");
560 saveptr
= strrchr(anchorItem
, '=');
562 anchorItem
= saveptr
+ 1;
566 strcpy(what
, "DETAIL_ID");
568 sqlite3Prng
.isInit
= 0;
569 sql
= sqlite3_mprintf("SELECT %s from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
572 " order by %s", what
, which
, myfilter
, groupBy
, order2
);
573 DPRINTF(E_DEBUG
, L_TIVO
, "%s\n", sql
);
574 if( (sql_get_table(db
, sql
, &result
, &ret
, NULL
) == SQLITE_OK
) && ret
)
576 for( i
=1; i
<=ret
; i
++ )
578 if( strcmp(anchorItem
, result
[i
]) == 0 )
581 itemStart
= ret
- i
+ itemCount
;
587 sqlite3_free_table(result
);
591 args
.start
= itemStart
+anchorOffset
;
592 sqlite3Prng
.isInit
= 0;
594 ret
= sql_get_int_field(db
, "SELECT count(distinct DETAIL_ID) "
595 "from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
596 " where %s and (%s)",
598 totalMatches
= (ret
> 0) ? ret
: 0;
599 if( itemCount
< 0 && !itemStart
&& !anchorOffset
)
601 args
.start
= totalMatches
+ itemCount
;
604 sql
= sqlite3_mprintf(SELECT_COLUMNS
605 "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
608 " order by %s limit %d, %d",
609 which
, myfilter
, groupBy
, order
, args
.start
, args
.requested
);
610 DPRINTF(E_DEBUG
, L_TIVO
, "%s\n", sql
);
611 ret
= sqlite3_exec(db
, sql
, callback
, (void *) &args
, &zErrMsg
);
613 if( ret
!= SQLITE_OK
)
615 DPRINTF(E_ERROR
, L_HTTP
, "SQL error: %s\n", zErrMsg
);
616 sqlite3_free(zErrMsg
);
623 strcatf(&str
, "</TiVoContainer>");
625 ret
= sprintf(str_buf
, "<?xml version='1.0' encoding='UTF-8' ?>\n"
628 "<ContentType>x-container/tivo-%s</ContentType>"
629 "<SourceFormat>x-container/folder</SourceFormat>"
630 "<TotalItems>%d</TotalItems>"
633 "<ItemStart>%d</ItemStart>"
634 "<ItemCount>%d</ItemCount>",
635 type
, totalMatches
, title
, args
.start
, args
.returned
);
637 memcpy(str
.data
, &str_buf
, ret
);
638 str
.size
= str
.off
+ret
;
641 BuildResp_upnphttp(h
, str
.data
, str
.size
);
643 SendResp_upnphttp(h
);
647 ProcessTiVoCommand(struct upnphttp
* h
, const char * orig_path
)
651 char *saveptr
= NULL
, *item
;
652 char *command
= NULL
, *container
= NULL
, *anchorItem
= NULL
;
653 char *sortOrder
= NULL
, *filter
= NULL
;
654 sqlite_int64 detailItem
=0;
655 int itemStart
=0, itemCount
=-100, anchorOffset
=0, recurse
=0;
656 unsigned long int randomSeed
=0;
658 path
= strdup(orig_path
);
659 DPRINTF(E_DEBUG
, L_GENERAL
, "Processing TiVo command %s\n", path
);
661 item
= strtok_r( path
, "&", &saveptr
);
662 while( item
!= NULL
)
666 item
= strtok_r( NULL
, "&", &saveptr
);
669 decodeString(item
, 1);
671 key
= strsep(&val
, "=");
672 decodeString(val
, 1);
673 DPRINTF(E_DEBUG
, L_GENERAL
, "%s: %s\n", key
, val
);
674 if( strcasecmp(key
, "Command") == 0 )
678 else if( strcasecmp(key
, "Container") == 0 )
682 else if( strcasecmp(key
, "ItemStart") == 0 )
684 itemStart
= atoi(val
);
686 else if( strcasecmp(key
, "ItemCount") == 0 )
688 itemCount
= atoi(val
);
690 else if( strcasecmp(key
, "AnchorItem") == 0 )
692 anchorItem
= basename(val
);
694 else if( strcasecmp(key
, "AnchorOffset") == 0 )
696 anchorOffset
= atoi(val
);
698 else if( strcasecmp(key
, "Recurse") == 0 )
700 recurse
= strcasecmp("yes", val
) == 0 ? 1 : 0;
702 else if( strcasecmp(key
, "SortOrder") == 0 )
706 else if( strcasecmp(key
, "Filter") == 0 )
710 else if( strcasecmp(key
, "RandomSeed") == 0 )
712 randomSeed
= strtoul(val
, NULL
, 10);
714 else if( strcasecmp(key
, "Url") == 0 )
717 detailItem
= strtoll(basename(val
), NULL
, 10);
719 else if( strcasecmp(key
, "Format") == 0 || // Only send XML
720 strcasecmp(key
, "SerialNum") == 0 || // Unused for now
721 strcasecmp(key
, "DoGenres") == 0 ) // Not sure what this is, so ignore it
727 DPRINTF(E_DEBUG
, L_GENERAL
, "Unhandled parameter [%s]\n", key
);
729 item
= strtok_r( NULL
, "&", &saveptr
);
733 strip_ext(anchorItem
);
738 if( strcmp(command
, "QueryContainer") == 0 )
740 if( !container
|| (strcmp(container
, "/") == 0) )
742 SendRootContainer(h
);
746 SendContainer(h
, container
, itemStart
, itemCount
, anchorItem
, anchorOffset
, recurse
, sortOrder
, filter
, randomSeed
);
749 else if( strcmp(command
, "QueryItem") == 0 )
751 SendItemDetails(h
, detailItem
);
755 DPRINTF(E_DEBUG
, L_GENERAL
, "Unhandled command [%s]\n", command
);
762 CloseSocket_upnphttp(h
);
764 #endif // TIVO_SUPPORT