1/* MiniDLNA media server
2 * Copyright (C) 2009  Justin Maggard
3 *
4 * This file is part of MiniDLNA.
5 *
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.
9 *
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.
14 *
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/>.
17 */
18#include "config.h"
19#ifdef TIVO_SUPPORT
20#include <stdio.h>
21#include <stdlib.h>
22#include <string.h>
23#include <libgen.h>
24#include <time.h>
25#include <sys/stat.h>
26
27#include "tivo_utils.h"
28#include "upnpglobalvars.h"
29#include "upnphttp.h"
30#include "upnpsoap.h"
31#include "utils.h"
32#include "sql.h"
33#include "log.h"
34
35static void
36SendRootContainer(struct upnphttp *h)
37{
38	char *resp;
39	int len;
40
41	len = xasprintf(&resp, "<?xml version='1.0' encoding='UTF-8' ?>\n"
42			"<TiVoContainer>"
43			 "<Details>"
44			  "<ContentType>x-container/tivo-server</ContentType>"
45			  "<SourceFormat>x-container/folder</SourceFormat>"
46			  "<TotalDuration>0</TotalDuration>"
47			  "<TotalItems>3</TotalItems>"
48			  "<Title>%s</Title>"
49			 "</Details>"
50			 "<ItemStart>0</ItemStart>"
51			 "<ItemCount>3</ItemCount>"
52			 "<Item>"
53			  "<Details>"
54			   "<ContentType>x-container/tivo-photos</ContentType>"
55			   "<SourceFormat>x-container/folder</SourceFormat>"
56			   "<Title>Pictures on %s</Title>"
57			  "</Details>"
58			  "<Links>"
59			   "<Content>"
60			    "<Url>/TiVoConnect?Command=QueryContainer&amp;Container=3</Url>"
61			   "</Content>"
62			  "</Links>"
63			 "</Item>"
64			 "<Item>"
65			  "<Details>"
66			   "<ContentType>x-container/tivo-music</ContentType>"
67			   "<SourceFormat>x-container/folder</SourceFormat>"
68			   "<Title>Music on %s</Title>"
69			  "</Details>"
70			  "<Links>"
71			   "<Content>"
72			    "<Url>/TiVoConnect?Command=QueryContainer&amp;Container=1</Url>"
73			   "</Content>"
74			  "</Links>"
75			 "</Item>"
76			 "<Item>"
77			  "<Details>"
78			   "<ContentType>x-container/tivo-videos</ContentType>"
79			   "<SourceFormat>x-container/folder</SourceFormat>"
80			   "<Title>Videos on %s</Title>"
81			  "</Details>"
82			  "<Links>"
83			   "<Content>"
84			    "<Url>/TiVoConnect?Command=QueryContainer&amp;Container=2</Url>"
85	                    "<ContentType>x-container/tivo-videos</ContentType>"
86			   "</Content>"
87			  "</Links>"
88			 "</Item>"
89			"</TiVoContainer>",
90	                friendly_name, friendly_name, friendly_name, friendly_name);
91	BuildResp_upnphttp(h, resp, len);
92	free(resp);
93	SendResp_upnphttp(h);
94}
95
96static void
97SendFormats(struct upnphttp *h, const char *sformat)
98{
99	char *resp;
100	int len;
101
102	len = xasprintf(&resp, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
103			"<TiVoFormats>"
104			 "<Format>"
105			   "<ContentType>video/x-tivo-mpeg</ContentType>"
106			   "<Description/>"
107			 "</Format>"
108			 "<Format>"
109			   "<ContentType>%s</ContentType>"
110			   "<Description/>"
111			 "</Format>"
112			"</TiVoFormats>", sformat);
113	BuildResp_upnphttp(h, resp, len);
114	free(resp);
115	SendResp_upnphttp(h);
116}
117
118static char *
119tivo_unescape_tag(char *tag)
120{
121	modifyString(tag, "&amp;amp;", "&amp;", 1);
122	modifyString(tag, "&amp;amp;lt;", "&lt;", 1);
123	modifyString(tag, "&amp;lt;", "&lt;", 1);
124	modifyString(tag, "&amp;amp;gt;", "&gt;", 1);
125	modifyString(tag, "&amp;gt;", "&gt;", 1);
126	modifyString(tag, "&amp;quot;", "&quot;", 1);
127	return tag;
128}
129
130#define FLAG_SEND_RESIZED  0x01
131#define FLAG_NO_PARAMS     0x02
132#define FLAG_VIDEO         0x04
133static int
134callback(void *args, int argc, char **argv, char **azColName)
135{
136	struct Response *passed_args = (struct Response *)args;
137	char *id = argv[0], *class = argv[1], *detailID = argv[2], *size = argv[3], *title = argv[4], *duration = argv[5],
138             *bitrate = argv[6], *sampleFrequency = argv[7], *artist = argv[8], *album = argv[9], *genre = argv[10],
139             *comment = argv[11], *date = argv[12], *resolution = argv[13], *mime = argv[14];
140	struct string_s *str = passed_args->str;
141
142	if( strncmp(class, "item", 4) == 0 )
143	{
144		int flags = 0;
145		tivo_unescape_tag(title);
146		if( strncmp(mime, "audio", 5) == 0 )
147		{
148			flags |= FLAG_NO_PARAMS;
149			strcatf(str, "<Item><Details>"
150			             "<ContentType>%s</ContentType>"
151			             "<SourceFormat>%s</SourceFormat>"
152			             "<SourceSize>%s</SourceSize>",
153			             "audio/*", mime, size);
154			strcatf(str, "<SongTitle>%s</SongTitle>", title);
155			if( date )
156				strcatf(str, "<AlbumYear>%.*s</AlbumYear>", 4, date);
157		}
158		else if( strcmp(mime, "image/jpeg") == 0 )
159		{
160			flags |= FLAG_SEND_RESIZED;
161			strcatf(str, "<Item><Details>"
162			             "<ContentType>%s</ContentType>"
163			             "<SourceFormat>%s</SourceFormat>"
164			             "<SourceSize>%s</SourceSize>",
165			             "image/*", mime, size);
166			if( date )
167			{
168				struct tm tm;
169				memset(&tm, 0, sizeof(tm));
170				tm.tm_isdst = -1; // Have libc figure out if DST is in effect or not
171				strptime(date, "%Y-%m-%dT%H:%M:%S", &tm);
172				strcatf(str, "<CaptureDate>0x%X</CaptureDate>", (unsigned int)mktime(&tm));
173			}
174			if( comment )
175				strcatf(str, "<Caption>%s</Caption>", comment);
176		}
177		else if( strncmp(mime, "video", 5) == 0 )
178		{
179			char *episode;
180			flags |= FLAG_VIDEO;
181			strcatf(str, "<Item><Details>"
182			             "<ContentType>%s</ContentType>"
183			             "<SourceFormat>%s</SourceFormat>"
184			             "<SourceSize>%s</SourceSize>",
185			             mime, mime, size);
186			episode = strstr(title, " - ");
187			if( episode )
188			{
189				strcatf(str, "<EpisodeTitle>%s</EpisodeTitle>", episode+3);
190				*episode = '\0';
191			}
192			if( date )
193			{
194				struct tm tm;
195				memset(&tm, 0, sizeof(tm));
196				tm.tm_isdst = -1; // Have libc figure out if DST is in effect or not
197				strptime(date, "%Y-%m-%dT%H:%M:%S", &tm);
198				strcatf(str, "<CaptureDate>0x%X</CaptureDate>", (unsigned int)mktime(&tm));
199			}
200			if( comment )
201				strcatf(str, "<Description>%s</Description>", tivo_unescape_tag(comment));
202		}
203		else
204		{
205			return 0;
206		}
207		strcatf(str, "<Title>%s</Title>", title);
208		if( artist ) {
209			strcatf(str, "<ArtistName>%s</ArtistName>", tivo_unescape_tag(artist));
210		}
211		if( album ) {
212			strcatf(str, "<AlbumTitle>%s</AlbumTitle>", tivo_unescape_tag(album));
213		}
214		if( genre ) {
215			strcatf(str, "<MusicGenre>%s</MusicGenre>", tivo_unescape_tag(genre));
216		}
217		if( resolution ) {
218			char *width = strsep(&resolution, "x");
219			strcatf(str, "<SourceWidth>%s</SourceWidth>"
220			                   "<SourceHeight>%s</SourceHeight>",
221			                   width, resolution);
222		}
223		if( duration ) {
224			strcatf(str, "<Duration>%d</Duration>",
225			      atoi(strrchr(duration, '.')+1) + (1000*atoi(strrchr(duration, ':')+1))
226			      + (60000*atoi(strrchr(duration, ':')-2)) + (3600000*atoi(duration)));
227		}
228		if( bitrate ) {
229			strcatf(str, "<SourceBitRate>%s</SourceBitRate>", bitrate);
230		}
231		if( sampleFrequency ) {
232			strcatf(str, "<SourceSampleRate>%s</SourceSampleRate>", sampleFrequency);
233		}
234		strcatf(str, "</Details><Links>"
235		             "<Content>"
236		               "<ContentType>%s</ContentType>"
237		               "<Url>/%s/%s.%s</Url>%s"
238		             "</Content>",
239		             mime,
240		             (flags & FLAG_SEND_RESIZED) ? "Resized" : "MediaItems",
241		             detailID, mime_to_ext(mime),
242		             (flags & FLAG_NO_PARAMS) ? "<AcceptsParams>No</AcceptsParams>" : "");
243		if( flags & FLAG_VIDEO )
244		{
245			strcatf(str, "<CustomIcon>"
246			               "<ContentType>image/*</ContentType>"
247			               "<Url>urn:tivo:image:save-until-i-delete-recording</Url>"
248			             "</CustomIcon>");
249		}
250		strcatf(str, "</Links>");
251	}
252	else if( strncmp(class, "container", 9) == 0 )
253	{
254		int count;
255		/* Determine the number of children */
256#ifdef __sparc__ /* Adding filters on large containers can take a long time on slow processors */
257		count = sql_get_int_field(db, "SELECT count(*) from OBJECTS where PARENT_ID = '%s'", id);
258#else
259		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 "
260		                              " (MIME in ('image/jpeg', 'audio/mpeg', 'video/mpeg', 'video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts')"
261		                              " or CLASS glob 'container*')", id);
262#endif
263		strcatf(str, "<Item>"
264		             "<Details>"
265		               "<ContentType>x-container/folder</ContentType>"
266		               "<SourceFormat>x-container/folder</SourceFormat>"
267		               "<Title>%s</Title>"
268		               "<TotalItems>%d</TotalItems>"
269		             "</Details>"
270		             "<Links>"
271		               "<Content>"
272		                 "<Url>/TiVoConnect?Command=QueryContainer&amp;Container=%s</Url>"
273		                 "<ContentType>x-tivo-container/folder</ContentType>"
274		               "</Content>"
275		             "</Links>",
276		             tivo_unescape_tag(title), count, id);
277	}
278	strcatf(str, "</Item>");
279
280	passed_args->returned++;
281
282	return 0;
283}
284
285#define SELECT_COLUMNS "SELECT o.OBJECT_ID, o.CLASS, o.DETAIL_ID, d.SIZE, d.TITLE," \
286	               " d.DURATION, d.BITRATE, d.SAMPLERATE, d.ARTIST, d.ALBUM, d.GENRE," \
287	               " d.COMMENT, d.DATE, d.RESOLUTION, d.MIME, d.DISC, d.TRACK "
288
289static void
290SendItemDetails(struct upnphttp *h, int64_t item)
291{
292	char *sql;
293	char *zErrMsg = NULL;
294	struct Response args;
295	struct string_s str;
296	int ret;
297	memset(&args, 0, sizeof(args));
298	memset(&str, 0, sizeof(str));
299
300	str.data = malloc(32768);
301	str.size = 32768;
302	str.off = sprintf(str.data, "<?xml version='1.0' encoding='UTF-8' ?>\n<TiVoItem>");
303	args.str = &str;
304	args.requested = 1;
305	xasprintf(&sql, SELECT_COLUMNS
306	               "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
307		       " where o.DETAIL_ID = %lld group by o.DETAIL_ID", (long long)item);
308	DPRINTF(E_DEBUG, L_TIVO, "%s\n", sql);
309	ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
310	free(sql);
311	if( ret != SQLITE_OK )
312	{
313		DPRINTF(E_ERROR, L_HTTP, "SQL error: %s\n", zErrMsg);
314		sqlite3_free(zErrMsg);
315	}
316	strcatf(&str, "</TiVoItem>");
317
318	BuildResp_upnphttp(h, str.data, str.off);
319	free(str.data);
320	SendResp_upnphttp(h);
321}
322
323static void
324SendContainer(struct upnphttp *h, const char *objectID, int itemStart, int itemCount, char *anchorItem,
325              int anchorOffset, int recurse, char *sortOrder, char *filter, unsigned long int randomSeed)
326{
327	char *resp = malloc(262144);
328	char *sql, *item, *saveptr;
329	char *zErrMsg = NULL;
330	char **result;
331	char *title, *which;
332	char what[10], order[96]={0}, order2[96]={0}, myfilter[256]={0};
333	char str_buf[1024];
334	char type[8];
335	char groupBy[19] = {0};
336	struct Response args;
337	struct string_s str;
338	int totalMatches = 0;
339	int i, ret;
340	memset(&args, 0, sizeof(args));
341	memset(&str, 0, sizeof(str));
342
343	args.str = &str;
344	str.data = resp+1024;
345	str.size = 262144-1024;
346	if( itemCount >= 0 )
347	{
348		args.requested = itemCount;
349	}
350	else
351	{
352		if( itemCount == -100 )
353			itemCount = 1;
354		args.requested = itemCount * -1;
355	}
356
357	switch( *objectID )
358	{
359		case '1':
360			strcpy(type, "music");
361			break;
362		case '2':
363			strcpy(type, "videos");
364			break;
365		case '3':
366			strcpy(type, "photos");
367			break;
368		default:
369			strcpy(type, "server");
370			break;
371	}
372
373	if( strlen(objectID) == 1 )
374	{
375		switch( *objectID )
376		{
377			case '1':
378				xasprintf(&title, "Music on %s", friendly_name);
379				break;
380			case '2':
381				xasprintf(&title, "Videos on %s", friendly_name);
382				break;
383			case '3':
384				xasprintf(&title, "Pictures on %s", friendly_name);
385				break;
386			default:
387				xasprintf(&title, "Unknown on %s", friendly_name);
388				break;
389		}
390	}
391	else
392	{
393		item = sql_get_text_field(db, "SELECT NAME from OBJECTS where OBJECT_ID = '%q'", objectID);
394		if( item )
395		{
396			title = escape_tag(item, 1);
397			sqlite3_free(item);
398		}
399		else
400			title = strdup("UNKNOWN");
401	}
402
403	if( recurse )
404	{
405		which = sqlite3_mprintf("OBJECT_ID glob '%q$*'", objectID);
406		strcpy(groupBy, "group by DETAIL_ID");
407	}
408	else
409	{
410		which = sqlite3_mprintf("PARENT_ID = '%q'", objectID);
411	}
412
413	if( sortOrder )
414	{
415		if( strcasestr(sortOrder, "Random") )
416		{
417			sprintf(order, "tivorandom(%lu)", randomSeed);
418			if( itemCount < 0 )
419				sprintf(order2, "tivorandom(%lu) DESC", randomSeed);
420			else
421				sprintf(order2, "tivorandom(%lu)", randomSeed);
422		}
423		else
424		{
425			short title_state = 0;
426			item = strtok_r(sortOrder, ",", &saveptr);
427			while( item != NULL )
428			{
429				int reverse=0;
430				if( *item == '!' )
431				{
432					reverse = 1;
433					item++;
434				}
435				if( strcasecmp(item, "Type") == 0 )
436				{
437					strcat(order, "CLASS");
438					strcat(order2, "CLASS");
439				}
440				else if( strcasecmp(item, "Title") == 0 )
441				{
442					/* Explicitly sort music by track then title. */
443					if( title_state < 2 && *objectID == '1' )
444					{
445						if( !title_state )
446						{
447							strcat(order, "DISC");
448							strcat(order2, "DISC");
449							title_state = 1;
450						}
451						else
452						{
453							strcat(order, "TRACK");
454							strcat(order2, "TRACK");
455							title_state = 2;
456						}
457					}
458					else
459					{
460						strcat(order, "TITLE");
461						strcat(order2, "TITLE");
462						title_state = -1;
463					}
464				}
465				else if( strcasecmp(item, "CreationDate") == 0 ||
466				         strcasecmp(item, "CaptureDate") == 0 )
467				{
468					strcat(order, "DATE");
469					strcat(order2, "DATE");
470				}
471				else
472				{
473					DPRINTF(E_INFO, L_TIVO, "Unhandled SortOrder [%s]\n", item);
474					goto unhandled_order;
475				}
476
477				if( reverse )
478				{
479					strcat(order, " DESC");
480					if( itemCount >= 0 )
481						strcat(order2, " DESC");
482					else
483						strcat(order2, " ASC");
484				}
485				else
486				{
487					strcat(order, " ASC");
488					if( itemCount >= 0 )
489						strcat(order2, " ASC");
490					else
491						strcat(order2, " DESC");
492				}
493				strcat(order, ", ");
494				strcat(order2, ", ");
495				unhandled_order:
496				if( title_state <= 0 )
497					item = strtok_r(NULL, ",", &saveptr);
498			}
499			if( title_state != -1 )
500			{
501				strcat(order, "TITLE ASC, ");
502				if( itemCount >= 0 )
503					strcat(order2, "TITLE ASC, ");
504				else
505					strcat(order2, "TITLE DESC, ");
506			}
507			strcat(order, "DETAIL_ID ASC");
508			if( itemCount >= 0 )
509				strcat(order2, "DETAIL_ID ASC");
510			else
511				strcat(order2, "DETAIL_ID DESC");
512		}
513	}
514	else
515	{
516		sprintf(order, "CLASS, NAME, DETAIL_ID");
517		if( itemCount < 0 )
518			sprintf(order2, "CLASS DESC, NAME DESC, DETAIL_ID DESC");
519		else
520			sprintf(order2, "CLASS, NAME, DETAIL_ID");
521	}
522
523	if( filter )
524	{
525		item = strtok_r(filter, ",", &saveptr);
526		for( i=0; item != NULL; i++ )
527		{
528			if( i )
529			{
530				strcat(myfilter, " or ");
531			}
532			if( (strcasecmp(item, "x-container/folder") == 0) ||
533			    (strncasecmp(item, "x-tivo-container/", 17) == 0) )
534			{
535				strcat(myfilter, "CLASS glob 'container*'");
536			}
537			else if( strncasecmp(item, "image", 5) == 0 )
538			{
539				strcat(myfilter, "MIME = 'image/jpeg'");
540			}
541			else if( strncasecmp(item, "audio", 5) == 0 )
542			{
543				strcat(myfilter, "MIME = 'audio/mpeg'");
544			}
545			else if( strncasecmp(item, "video", 5) == 0 )
546			{
547				strcat(myfilter, "MIME in ('video/mpeg', 'video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts')");
548			}
549			else
550			{
551				DPRINTF(E_INFO, L_TIVO, "Unhandled Filter [%s]\n", item);
552				if( i )
553				{
554					ret = strlen(myfilter);
555					myfilter[ret-4] = '\0';
556				}
557				i--;
558			}
559			item = strtok_r(NULL, ",", &saveptr);
560		}
561	}
562	else
563	{
564		strcpy(myfilter, "MIME in ('image/jpeg', 'audio/mpeg', 'video/mpeg', 'video/x-tivo-mpeg', 'video/x-tivo-mpeg-ts') or CLASS glob 'container*'");
565	}
566
567	if( anchorItem )
568	{
569		if( strstr(anchorItem, "QueryContainer") )
570		{
571			strcpy(what, "OBJECT_ID");
572			saveptr = strrchr(anchorItem, '=');
573			if( saveptr )
574				anchorItem = saveptr + 1;
575		}
576		else
577		{
578			strcpy(what, "DETAIL_ID");
579		}
580		sqlite3Prng.isInit = 0;
581		sql = sqlite3_mprintf("SELECT %s from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
582		                      " where %s and (%s)"
583	                              " %s"
584		                      " order by %s", what, which, myfilter, groupBy, order2);
585		DPRINTF(E_DEBUG, L_TIVO, "%s\n", sql);
586		if( (sql_get_table(db, sql, &result, &ret, NULL) == SQLITE_OK) && ret )
587		{
588			for( i=1; i<=ret; i++ )
589			{
590				if( strcmp(anchorItem, result[i]) == 0 )
591				{
592					if( itemCount < 0 )
593						itemStart = ret - i + itemCount;
594					else
595						itemStart += i;
596					break;
597				}
598			}
599			sqlite3_free_table(result);
600		}
601		sqlite3_free(sql);
602	}
603	args.start = itemStart+anchorOffset;
604	sqlite3Prng.isInit = 0;
605
606	ret = sql_get_int_field(db, "SELECT count(distinct DETAIL_ID) "
607	                            "from OBJECTS o left join DETAILS d on (o.DETAIL_ID = d.ID)"
608	                            " where %s and (%s)",
609	                            which, myfilter);
610	totalMatches = (ret > 0) ? ret : 0;
611	if( itemCount < 0 && !itemStart && !anchorOffset )
612	{
613		args.start = totalMatches + itemCount;
614	}
615
616	sql = sqlite3_mprintf(SELECT_COLUMNS
617	                      "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
618		              " where %s and (%s)"
619	                      " %s"
620			      " order by %s limit %d, %d",
621	                      which, myfilter, groupBy, order, args.start, args.requested);
622	DPRINTF(E_DEBUG, L_TIVO, "%s\n", sql);
623	ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg);
624	sqlite3_free(sql);
625	if( ret != SQLITE_OK )
626	{
627		DPRINTF(E_ERROR, L_HTTP, "SQL error: %s\n", zErrMsg);
628		sqlite3_free(zErrMsg);
629		Send500(h);
630		sqlite3_free(which);
631		free(title);
632		free(resp);
633		return;
634	}
635	strcatf(&str, "</TiVoContainer>");
636
637	ret = sprintf(str_buf, "<?xml version='1.0' encoding='UTF-8' ?>\n"
638			       "<TiVoContainer>"
639	                         "<Details>"
640	                           "<ContentType>x-container/tivo-%s</ContentType>"
641	                           "<SourceFormat>x-container/folder</SourceFormat>"
642	                           "<TotalItems>%d</TotalItems>"
643	                           "<Title>%s</Title>"
644	                         "</Details>"
645	                         "<ItemStart>%d</ItemStart>"
646	                         "<ItemCount>%d</ItemCount>",
647	                         type, totalMatches, title, args.start, args.returned);
648	str.data -= ret;
649	memcpy(str.data, &str_buf, ret);
650	str.size = str.off+ret;
651	free(title);
652	sqlite3_free(which);
653	BuildResp_upnphttp(h, str.data, str.size);
654	free(resp);
655	SendResp_upnphttp(h);
656}
657
658void
659ProcessTiVoCommand(struct upnphttp *h, const char *orig_path)
660{
661	char *path;
662	char *key, *val;
663	char *saveptr = NULL, *item;
664	char *command = NULL, *container = NULL, *anchorItem = NULL;
665	char *sortOrder = NULL, *filter = NULL, *sformat = NULL;
666	int64_t detailItem=0;
667	int itemStart=0, itemCount=-100, anchorOffset=0, recurse=0;
668	unsigned long int randomSeed=0;
669
670	path = strdup(orig_path);
671	DPRINTF(E_DEBUG, L_GENERAL, "Processing TiVo command %s\n", path);
672
673	item = strtok_r( path, "&", &saveptr );
674	while( item != NULL )
675	{
676		if( *item == '\0' )
677		{
678			item = strtok_r( NULL, "&", &saveptr );
679			continue;
680		}
681		decodeString(item, 1);
682		val = item;
683		key = strsep(&val, "=");
684		decodeString(val, 1);
685		DPRINTF(E_DEBUG, L_GENERAL, "%s: %s\n", key, val);
686		if( strcasecmp(key, "Command") == 0 )
687		{
688			command = val;
689		}
690		else if( strcasecmp(key, "Container") == 0 )
691		{
692			container = val;
693		}
694		else if( strcasecmp(key, "ItemStart") == 0 )
695		{
696			itemStart = atoi(val);
697		}
698		else if( strcasecmp(key, "ItemCount") == 0 )
699		{
700			itemCount = atoi(val);
701		}
702		else if( strcasecmp(key, "AnchorItem") == 0 )
703		{
704			anchorItem = basename(val);
705		}
706		else if( strcasecmp(key, "AnchorOffset") == 0 )
707		{
708			anchorOffset = atoi(val);
709		}
710		else if( strcasecmp(key, "Recurse") == 0 )
711		{
712			recurse = strcasecmp("yes", val) == 0 ? 1 : 0;
713		}
714		else if( strcasecmp(key, "SortOrder") == 0 )
715		{
716			sortOrder = val;
717		}
718		else if( strcasecmp(key, "Filter") == 0 )
719		{
720			filter = val;
721		}
722		else if( strcasecmp(key, "RandomSeed") == 0 )
723		{
724			randomSeed = strtoul(val, NULL, 10);
725		}
726		else if( strcasecmp(key, "Url") == 0 )
727		{
728			if( val )
729				detailItem = strtoll(basename(val), NULL, 10);
730		}
731		else if( strcasecmp(key, "SourceFormat") == 0 )
732		{
733			sformat = val;
734		}
735		else if( strcasecmp(key, "Format") == 0 || // Only send XML
736		         strcasecmp(key, "SerialNum") == 0 || // Unused for now
737		         strcasecmp(key, "DoGenres") == 0 ) // Not sure what this is, so ignore it
738		{
739			;;
740		}
741		else
742		{
743			DPRINTF(E_DEBUG, L_GENERAL, "Unhandled parameter [%s]\n", key);
744		}
745		item = strtok_r( NULL, "&", &saveptr );
746	}
747	if( anchorItem )
748	{
749		strip_ext(anchorItem);
750	}
751
752	if( command )
753	{
754		if( strcmp(command, "QueryContainer") == 0 )
755		{
756			if( !container || (strcmp(container, "/") == 0) )
757			{
758				SendRootContainer(h);
759			}
760			else
761			{
762				SendContainer(h, container, itemStart, itemCount, anchorItem,
763				              anchorOffset, recurse, sortOrder, filter, randomSeed);
764			}
765		}
766		else if( strcmp(command, "QueryItem") == 0 )
767		{
768			SendItemDetails(h, detailItem);
769		}
770		else if( strcmp(command, "QueryFormats") == 0 )
771		{
772			SendFormats(h, sformat);
773		}
774		else
775		{
776			DPRINTF(E_DEBUG, L_GENERAL, "Unhandled command [%s]\n", command);
777			Send501(h);
778			free(path);
779			return;
780		}
781	}
782	free(path);
783	CloseSocket_upnphttp(h);
784}
785#endif // TIVO_SUPPORT
786