diff --git a/clients.c b/clients.c index c3bd96d..d32e92d 100644 --- a/clients.c +++ b/clients.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2013 NETGEAR + * Copyright (C) 2013-2017 NETGEAR * * This file is part of MiniDLNA. * @@ -254,6 +254,13 @@ struct client_type_s client_types[] = EUserAgent }, + { EKodi, + FLAG_DLNA | FLAG_MIME_AVI_AVI | FLAG_CAPTION_RES, + "Kodi", + "Kodi", + EUserAgent + }, + { 0, FLAG_DLNA | FLAG_MIME_AVI_AVI, "Windows", diff --git a/clients.h b/clients.h index fa39656..a393b72 100644 --- a/clients.h +++ b/clients.h @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2013 NETGEAR + * Copyright (C) 2013-2017 NETGEAR * * This file is part of MiniDLNA. * @@ -79,6 +79,7 @@ enum client_types { EAsusOPlay, EBubbleUPnP, ENetFrontLivingConnect, + EKodi, EStandardDLNA150, EStandardUPnP }; diff --git a/minidlna.c b/minidlna.c index 4fa23f6..322d04d 100644 --- a/minidlna.c +++ b/minidlna.c @@ -336,7 +336,7 @@ rescan: else if (ret == 2) DPRINTF(E_WARN, L_GENERAL, "Removed media_dir detected; rebuilding...\n"); else - DPRINTF(E_WARN, L_GENERAL, "Database version mismatch (%d=>%d); need to recreate...\n", + DPRINTF(E_WARN, L_GENERAL, "Database version mismatch (%d => %d); need to recreate...\n", ret, DB_VERSION); sqlite3_close(db); diff --git a/scanner.c b/scanner.c index 96127a7..79c2b7e 100644 --- a/scanner.c +++ b/scanner.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -95,12 +95,12 @@ insert_container(const char *item, const char *rootParent, const char *refID, co int ret = 0; result = sql_get_text_field(db, "SELECT OBJECT_ID from OBJECTS o " - "left join DETAILS d on (o.DETAIL_ID = d.ID)" - " where o.PARENT_ID = '%s'" - " and o.NAME like '%q'" - " and d.ARTIST %s %Q" - " and o.CLASS = 'container.%s' limit 1", - rootParent, item, artist?"like":"is", artist, class); + "left join DETAILS d on (o.DETAIL_ID = d.ID)" + " where o.PARENT_ID = '%s'" + " and o.NAME like '%q'" + " and d.ARTIST %s %Q" + " and o.CLASS = 'container.%s' limit 1", + rootParent, item, artist?"like":"is", artist, class); if( result ) { base = strrchr(result, '$'); @@ -398,7 +398,7 @@ insert_directory(const char *name, const char *path, const char *base, const cha char id_buf[64], parent_buf[64], refID[64]; char *dir_buf, *dir; - dir_buf = strdup(path); + dir_buf = strdup(path); dir = dirname(dir_buf); snprintf(refID, sizeof(refID), "%s%s$%X", BROWSEDIR_ID, parentID, objectID); snprintf(id_buf, sizeof(id_buf), "%s%s$%X", base, parentID, objectID); @@ -467,7 +467,7 @@ insert_file(char *name, const char *path, const char *parentID, int object, medi } else if( (types & TYPE_VIDEO) && is_video(name) ) { - orig_name = strdup(name); + orig_name = strdup(name); strcpy(base, VIDEO_DIR_ID); strcpy(class, "item.videoItem"); detailID = GetVideoMetadata(path, name); diff --git a/scanner_sqlite.h b/scanner_sqlite.h index 3a9aba1..cbc1e72 100644 --- a/scanner_sqlite.h +++ b/scanner_sqlite.h @@ -5,7 +5,7 @@ * Author : Douglas Carmichael * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -29,7 +29,8 @@ char create_objectTable_sqlite[] = "CREATE TABLE OBJECTS (" "REF_ID TEXT DEFAULT NULL, " "CLASS TEXT NOT NULL, " "DETAIL_ID INTEGER DEFAULT NULL, " - "NAME TEXT DEFAULT NULL);"; + "NAME TEXT DEFAULT NULL" + ");"; char create_detailTable_sqlite[] = "CREATE TABLE DETAILS (" "ID INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -54,12 +55,13 @@ char create_detailTable_sqlite[] = "CREATE TABLE DETAILS (" "ALBUM_ART INTEGER DEFAULT 0, " "ROTATION INTEGER, " "DLNA_PN TEXT, " - "MIME TEXT);"; + "MIME TEXT" + ");"; char create_albumArtTable_sqlite[] = "CREATE TABLE ALBUM_ART (" "ID INTEGER PRIMARY KEY AUTOINCREMENT, " "PATH TEXT NOT NULL" - ");"; + ");"; char create_captionTable_sqlite[] = "CREATE TABLE CAPTIONS (" "ID INTEGER PRIMARY KEY, " @@ -68,7 +70,8 @@ char create_captionTable_sqlite[] = "CREATE TABLE CAPTIONS (" char create_bookmarkTable_sqlite[] = "CREATE TABLE BOOKMARKS (" "ID INTEGER PRIMARY KEY, " - "SEC INTEGER" + "SEC INTEGER, " + "WATCH_COUNT INTEGER" ");"; char create_playlistTable_sqlite[] = "CREATE TABLE PLAYLISTS (" @@ -83,5 +86,3 @@ char create_settingsTable_sqlite[] = "CREATE TABLE SETTINGS (" "KEY TEXT NOT NULL, " "VALUE TEXT" ");"; - - diff --git a/sql.c b/sql.c index b2ea238..7889c31 100644 --- a/sql.c +++ b/sql.c @@ -1,5 +1,5 @@ /* MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -93,10 +93,10 @@ sql_get_int_field(sqlite3 *db, const char *fmt, ...) for (counter = 0; ((result = sqlite3_step(stmt)) == SQLITE_BUSY || result == SQLITE_LOCKED) && counter < 2; counter++) { - /* While SQLITE_BUSY has a built in timeout, - SQLITE_LOCKED does not, so sleep */ - if (result == SQLITE_LOCKED) - sleep(1); + /* While SQLITE_BUSY has a built in timeout, + * SQLITE_LOCKED does not, so sleep */ + if (result == SQLITE_LOCKED) + sleep(1); } switch (result) @@ -117,7 +117,7 @@ sql_get_int_field(sqlite3 *db, const char *fmt, ...) DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %s\n%s\n", __func__, sqlite3_errmsg(db), sql); ret = -1; break; - } + } sqlite3_free(sql); sqlite3_finalize(stmt); @@ -152,10 +152,10 @@ sql_get_int64_field(sqlite3 *db, const char *fmt, ...) for (counter = 0; ((result = sqlite3_step(stmt)) == SQLITE_BUSY || result == SQLITE_LOCKED) && counter < 2; counter++) { - /* While SQLITE_BUSY has a built in timeout, - SQLITE_LOCKED does not, so sleep */ - if (result == SQLITE_LOCKED) - sleep(1); + /* While SQLITE_BUSY has a built in timeout, + * SQLITE_LOCKED does not, so sleep */ + if (result == SQLITE_LOCKED) + sleep(1); } switch (result) @@ -176,7 +176,7 @@ sql_get_int64_field(sqlite3 *db, const char *fmt, ...) DPRINTF(E_WARN, L_DB_SQL, "%s: step failed: %s\n%s\n", __func__, sqlite3_errmsg(db), sql); ret = -1; break; - } + } sqlite3_free(sql); sqlite3_finalize(stmt); @@ -263,6 +263,7 @@ int db_upgrade(sqlite3 *db) { int db_vers; + int ret; db_vers = sql_get_int_field(db, "PRAGMA user_version"); @@ -274,6 +275,13 @@ db_upgrade(sqlite3 *db) return -1; if (db_vers < 9) return db_vers; + if (db_vers < 10) + { + DPRINTF(E_WARN, L_DB_SQL, "Updating DB version to v%d\n", 10); + ret = sql_exec(db, "ALTER TABLE BOOKMARKS ADD WATCH_COUNT INTEGER"); + if (ret != SQLITE_OK) + return 9; + } sql_exec(db, "PRAGMA user_version = %d", DB_VERSION); return 0; diff --git a/upnpdescgen.c b/upnpdescgen.c index b7867db..8891fb6 100644 --- a/upnpdescgen.c +++ b/upnpdescgen.c @@ -236,19 +236,27 @@ static const struct stateVar ConnectionManagerVars[] = static const struct argument GetSearchCapabilitiesArgs[] = { - {"SearchCaps", 2, 10}, + {"SearchCaps", 2, 11}, {0, 0} }; static const struct argument GetSortCapabilitiesArgs[] = { - {"SortCaps", 2, 11}, + {"SortCaps", 2, 12}, {0, 0} }; static const struct argument GetSystemUpdateIDArgs[] = { - {"Id", 2, 12}, + {"Id", 2, 13}, + {0, 0} +}; + +static const struct argument UpdateObjectArgs[] = +{ + {"ObjectID", 1, 1}, + {"CurrentTagValue", 1, 10}, + {"NewTagValue", 1, 10}, {0, 0} }; @@ -289,10 +297,10 @@ static const struct action ContentDirectoryActions[] = {"GetSystemUpdateID", GetSystemUpdateIDArgs}, /* R */ {"Browse", BrowseArgs}, /* R */ {"Search", SearchArgs}, /* O */ + {"UpdateObject", UpdateObjectArgs}, /* O */ #if 0 // Not implementing optional features yet... {"CreateObject", CreateObjectArgs}, /* O */ {"DestroyObject", DestroyObjectArgs}, /* O */ - {"UpdateObject", UpdateObjectArgs}, /* O */ {"ImportResource", ImportResourceArgs}, /* O */ {"ExportResource", ExportResourceArgs}, /* O */ {"StopTransferResource", StopTransferResourceArgs}, /* O */ @@ -316,6 +324,7 @@ static const struct stateVar ContentDirectoryVars[] = {"A_ARG_TYPE_Index", 3, 0}, {"A_ARG_TYPE_Count", 3, 0}, {"A_ARG_TYPE_UpdateID", 3, 0}, + {"A_ARG_TYPE_TagValueList", 0, 0}, {"SearchCapabilities", 0, 0}, {"SortCapabilities", 0, 0}, {"SystemUpdateID", 3|EVENTED, 0, 0, 255}, diff --git a/upnpglobalvars.h b/upnpglobalvars.h index 9773b0d..21b618b 100644 --- a/upnpglobalvars.h +++ b/upnpglobalvars.h @@ -3,7 +3,7 @@ * http://sourceforge.net/projects/minidlna/ * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -66,7 +66,7 @@ #endif #define USE_FORK 1 -#define DB_VERSION 9 +#define DB_VERSION 10 #ifdef ENABLE_NLS #define _(string) gettext(string) diff --git a/upnpsoap.c b/upnpsoap.c index 23473b7..de6dd88 100644 --- a/upnpsoap.c +++ b/upnpsoap.c @@ -3,7 +3,7 @@ * http://sourceforge.net/projects/minidlna/ * * MiniDLNA media server - * Copyright (C) 2008-2009 Justin Maggard + * Copyright (C) 2008-2017 Justin Maggard * * This file is part of MiniDLNA. * @@ -84,7 +84,7 @@ * -------- ---------------- ----------- * 401 Invalid Action No action by that name at this service. * 402 Invalid Args Could be any of the following: not enough in args, - * too many in args, no in arg by that name, + * too many in args, no in arg by that name, * one or more in args are of the wrong data type. * 403 Out of Sync Out of synchronization. * 501 Action Failed May be returned in current state of service @@ -93,13 +93,13 @@ * Technical Committee. * 700-799 TBD Action-specific errors for standard actions. * Defined by UPnP Forum working committee. - * 800-899 TBD Action-specific errors for non-standard actions. + * 800-899 TBD Action-specific errors for non-standard actions. * Defined by UPnP vendor. */ static void SoapError(struct upnphttp * h, int errCode, const char * errDesc) { - static const char resp[] = + static const char resp[] = "" @@ -201,13 +201,13 @@ IsAuthorizedValidated(struct upnphttp * h, const char * action) int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", - 1, action); + 1, action); BuildSendAndCloseSoapResp(h, body, bodylen); } else SoapError(h, 402, "Invalid Args"); - ClearNameValueList(&data); + ClearNameValueList(&data); } static void @@ -245,7 +245,7 @@ GetProtocolInfo(struct upnphttp * h, const char * action) bodylen = asprintf(&body, resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); free(body); } @@ -270,7 +270,7 @@ GetSortCapabilities(struct upnphttp * h, const char * action) bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -299,7 +299,7 @@ GetSearchCapabilities(struct upnphttp * h, const char * action) bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -318,7 +318,7 @@ GetCurrentConnectionIDs(struct upnphttp * h, const char * action) bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -362,43 +362,51 @@ GetCurrentConnectionInfo(struct upnphttp * h, const char * action) int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", - action); + action); BuildSendAndCloseSoapResp(h, body, bodylen); } - ClearNameValueList(&data); + ClearNameValueList(&data); } /* Standard DLNA/UPnP filter flags */ -#define FILTER_CHILDCOUNT 0x00000001 -#define FILTER_DC_CREATOR 0x00000002 -#define FILTER_DC_DATE 0x00000004 -#define FILTER_DC_DESCRIPTION 0x00000008 -#define FILTER_DLNA_NAMESPACE 0x00000010 -#define FILTER_REFID 0x00000020 -#define FILTER_RES 0x00000040 -#define FILTER_RES_BITRATE 0x00000080 -#define FILTER_RES_DURATION 0x00000100 -#define FILTER_RES_NRAUDIOCHANNELS 0x00000200 -#define FILTER_RES_RESOLUTION 0x00000400 -#define FILTER_RES_SAMPLEFREQUENCY 0x00000800 -#define FILTER_RES_SIZE 0x00001000 -#define FILTER_SEARCHABLE 0x00002000 -#define FILTER_UPNP_ACTOR 0x00004000 -#define FILTER_UPNP_ALBUM 0x00008000 -#define FILTER_UPNP_ALBUMARTURI 0x00010000 -#define FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID 0x00020000 -#define FILTER_UPNP_ARTIST 0x00040000 -#define FILTER_UPNP_GENRE 0x00080000 -#define FILTER_UPNP_ORIGINALTRACKNUMBER 0x00100000 -#define FILTER_UPNP_SEARCHCLASS 0x00200000 -#define FILTER_UPNP_STORAGEUSED 0x00400000 +#define FILTER_CHILDCOUNT 0x00000001 +#define FILTER_DC_CREATOR 0x00000002 +#define FILTER_DC_DATE 0x00000004 +#define FILTER_DC_DESCRIPTION 0x00000008 +#define FILTER_DLNA_NAMESPACE 0x00000010 +#define FILTER_REFID 0x00000020 +#define FILTER_RES 0x00000040 +#define FILTER_RES_BITRATE 0x00000080 +#define FILTER_RES_DURATION 0x00000100 +#define FILTER_RES_NRAUDIOCHANNELS 0x00000200 +#define FILTER_RES_RESOLUTION 0x00000400 +#define FILTER_RES_SAMPLEFREQUENCY 0x00000800 +#define FILTER_RES_SIZE 0x00001000 +#define FILTER_SEARCHABLE 0x00002000 +#define FILTER_UPNP_ACTOR 0x00004000 +#define FILTER_UPNP_ALBUM 0x00008000 +#define FILTER_UPNP_ALBUMARTURI 0x00010000 +#define FILTER_UPNP_ALBUMARTURI_DLNA_PROFILEID 0x00020000 +#define FILTER_UPNP_ARTIST 0x00040000 +#define FILTER_UPNP_GENRE 0x00080000 +#define FILTER_UPNP_ORIGINALTRACKNUMBER 0x00100000 +#define FILTER_UPNP_SEARCHCLASS 0x00200000 +#define FILTER_UPNP_STORAGEUSED 0x00400000 +/* Not normally used, so leave out of the default filter */ +#define FILTER_UPNP_PLAYBACKCOUNT 0x01000000 +#define FILTER_UPNP_LASTPLAYBACKPOSITION 0x02000000 /* Vendor-specific filter flags */ -#define FILTER_SEC_CAPTION_INFO_EX 0x01000000 -#define FILTER_SEC_DCM_INFO 0x02000000 -#define FILTER_PV_SUBTITLE_FILE_TYPE 0x04000000 -#define FILTER_PV_SUBTITLE_FILE_URI 0x08000000 -#define FILTER_PV_SUBTITLE 0x0C000000 -#define FILTER_AV_MEDIA_CLASS 0x10000000 +#define FILTER_SEC_CAPTION_INFO_EX 0x04000000 +#define FILTER_SEC_DCM_INFO 0x08000000 +#define FILTER_PV_SUBTITLE_FILE_TYPE 0x10000000 +#define FILTER_PV_SUBTITLE_FILE_URI 0x20000000 +#define FILTER_PV_SUBTITLE 0x30000000 +#define FILTER_AV_MEDIA_CLASS 0x40000000 +/* Masks */ +#define STANDARD_FILTER_MASK 0x00FFFFFF +#define FILTER_BOOKMARK_MASK (FILTER_UPNP_PLAYBACKCOUNT | \ + FILTER_UPNP_LASTPLAYBACKPOSITION | \ + FILTER_SEC_DCM_INFO) static uint32_t set_filter_flags(char *filter, struct upnphttp *h) @@ -409,7 +417,7 @@ set_filter_flags(char *filter, struct upnphttp *h) if( !filter || (strlen(filter) <= 1) ) { /* Not the full 32 bits. Skip vendor-specific stuff by default. */ - flags = 0xFFFFFF; + flags = STANDARD_FILTER_MASK; if (samsung) flags |= FILTER_SEC_CAPTION_INFO_EX | FILTER_SEC_DCM_INFO; } @@ -538,6 +546,14 @@ set_filter_flags(char *filter, struct upnphttp *h) flags |= FILTER_RES; flags |= FILTER_RES_SIZE; } + else if( strcmp(item, "upnp:playbackCount") == 0 ) + { + flags |= FILTER_UPNP_PLAYBACKCOUNT; + } + else if( strcmp(item, "upnp:lastPlaybackPosition") == 0 ) + { + flags |= FILTER_UPNP_LASTPLAYBACKPOSITION; + } else if( strcmp(item, "sec:CaptionInfoEx") == 0 ) { flags |= FILTER_SEC_CAPTION_INFO_EX; @@ -727,7 +743,7 @@ add_res(char *size, char *duration, char *bitrate, char *sampleFrequency, strcatf(args->str, "pv:subtitleFileType=\"SRT\" "); if( args->filter & FILTER_PV_SUBTITLE_FILE_URI ) strcatf(args->str, "pv:subtitleFileUri=\"http://%s:%d/Captions/%s.srt\" ", - lan_addr[args->iface].str, runtime_vars.port, detailID); + lan_addr[args->iface].str, runtime_vars.port, detailID); } } strcatf(args->str, "protocolInfo=\"http-get:*:%s:%s\">" @@ -916,7 +932,7 @@ callback(void *args, int argc, char **argv, char **azColName) dlna_flags |= DLNA_FLAG_TM_I; if( passed_args->flags & FLAG_SKIP_DLNA_PN ) - dlna_pn = NULL; + dlna_pn = NULL; if( dlna_pn ) snprintf(dlna_buf, sizeof(dlna_buf), "DLNA.ORG_PN=%s;" @@ -949,10 +965,24 @@ callback(void *args, int argc, char **argv, char **azColName) if( date && (passed_args->filter & FILTER_DC_DATE) ) { ret = strcatf(str, "<dc:date>%s</dc:date>", date); } - if( passed_args->filter & FILTER_SEC_DCM_INFO ) { + if( (passed_args->filter & FILTER_BOOKMARK_MASK) ) { /* Get bookmark */ - ret = strcatf(str, "<sec:dcmInfo>CREATIONDATE=0,FOLDER=%s,BM=%d</sec:dcmInfo>", - title, sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID)); + int sec = sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID); + if( sec > 0 && (passed_args->filter & FILTER_UPNP_LASTPLAYBACKPOSITION) ) { + /* This format is wrong according to the UPnP/AV spec. It should be in duration format, + ** so HH:MM:SS. But Kodi seems to be the only user of this tag, and it only works with a + ** raw seconds value. + ** If Kodi gets fixed, we can use duration_str(sec * 1000) here */ + ret = strcatf(str, "<upnp:lastPlaybackPosition>%d</upnp:lastPlaybackPosition>", + sec); + } + if( passed_args->filter & FILTER_SEC_DCM_INFO ) + ret = strcatf(str, "<sec:dcmInfo>CREATIONDATE=0,FOLDER=%s,BM=%d</sec:dcmInfo>", + title, sec); + if( passed_args->filter & FILTER_UPNP_PLAYBACKCOUNT ) { + ret = strcatf(str, "<upnp:playbackCount>%d</upnp:playbackCount>", + sql_get_int_field(db, "SELECT WATCH_COUNT from BOOKMARKS where ID = '%s'", detailID)); + } } if( artist ) { if( (*mime == 'v') && (passed_args->filter & FILTER_UPNP_ACTOR) ) { @@ -1369,7 +1399,7 @@ BrowseContentDirectory(struct upnphttp * h, const char * action) } sql = sqlite3_mprintf("SELECT %s, %s, %s, " COLUMNS - "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" + "from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" " where %s %s limit %d, %d;", objectid_sql, parentid_sql, refid_sql, where, THISORNUL(orderBy), StartingIndex, RequestedCount); @@ -1844,10 +1874,10 @@ static void QueryStateVariable(struct upnphttp * h, const char * action) { static const char resp[] = - "" + "" "%s" - ""; + ""; char body[512]; struct NameValueParserData data; @@ -1865,10 +1895,10 @@ QueryStateVariable(struct upnphttp * h, const char * action) SoapError(h, 402, "Invalid Args"); } else if(strcmp(var_name, "ConnectionStatus") == 0) - { + { int bodylen; bodylen = snprintf(body, sizeof(body), resp, - action, "urn:schemas-upnp-org:control-1-0", + action, "urn:schemas-upnp-org:control-1-0", "Connected", action); BuildSendAndCloseSoapResp(h, body, bodylen); } @@ -1878,7 +1908,131 @@ QueryStateVariable(struct upnphttp * h, const char * action) SoapError(h, 404, "Invalid Var"); } - ClearNameValueList(&data); + ClearNameValueList(&data); +} + +/* For some reason, Kodi does URI encoding and appends a trailing slash */ +static void _kodi_decode(char *str) +{ + while (*str) + { + switch (*str) { + case '%': + { + if (isxdigit(str[1]) && isxdigit(str[2])) + { + char x[3] = { str[1], str[2], '\0' }; + *str++ = (char)strtol(x, NULL, 16); + memmove(str, str+2, strlen(str+1)); + } + break; + } + case '/': + if (!str[1]) + *str = '\0'; + default: + str++; + break; + } + } +} + +static int duration_sec(const char *str) +{ + int hr, min, sec; + + if (sscanf(str, "%d:%d:%d", &hr, &min, &sec) == 3) + return (hr * 3600) + (min * 60) + sec; + + return atoi(str); +} + +static void UpdateObject(struct upnphttp * h, const char * action) +{ + static const char resp[] = + "" + ""; + + struct NameValueParserData data; + + ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data, 0); + + char *ObjectID = GetValueFromNameValueList(&data, "ObjectID"); + char *CurrentTagValue = GetValueFromNameValueList(&data, "CurrentTagValue"); + char *NewTagValue = GetValueFromNameValueList(&data, "NewTagValue"); + const char *rid = ObjectID; + char tag[32], current[32], new[32]; + char *item, *saveptr = NULL; + int64_t detailID; + int ret = 1; + + if (!ObjectID || !CurrentTagValue || !NewTagValue) + { + SoapError(h, 402, "Invalid Args"); + ClearNameValueList(&data); + return; + } + + _kodi_decode(ObjectID); + DPRINTF(E_DEBUG, L_HTTP, "UpdateObject %s: %s => %s\n", ObjectID, CurrentTagValue, NewTagValue); + + in_magic_container(ObjectID, 0, &rid); + detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid); + if (detailID <= 0) + { + SoapError(h, 701, "No such object"); + ClearNameValueList(&data); + return; + } + + for (item = strtok_r(CurrentTagValue, ",", &saveptr); item; item = strtok_r(NULL, ",", &saveptr)) + { + char *p; + if (sscanf(item, "<%31[^&]>%31[^&]", tag, current) != 2) + continue; + p = strstr(NewTagValue, tag); + if (!p || sscanf(p, "%*[^&]>%31[^&]", new) != 1) + continue; + + DPRINTF(E_DEBUG, L_HTTP, "Setting %s to %s\n", tag, new); + /* Kodi uses incorrect tag "upnp:playCount" instead of "upnp:playbackCount" */ + if (strcmp(tag, "upnp:playbackCount") == 0 || strcmp(tag, "upnp:playCount") == 0) + { + //ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, WATCH_COUNT)" + ret = sql_exec(db, "INSERT into BOOKMARKS (ID, WATCH_COUNT)" + " VALUES (%lld, %Q)", (long long)detailID, new); + if (atoi(new)) + ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = %Q" + " where WATCH_COUNT = %Q and ID = %lld", + new, current, (long long)detailID); + else + ret = sql_exec(db, "UPDATE BOOKMARKS set WATCH_COUNT = 0" + " where ID = %lld", (long long)detailID); + } + else if (strcmp(tag, "upnp:lastPlaybackPosition") == 0) + { + int sec = duration_sec(new); + if (sec < 30) + sec = 0; + else + sec -= 1; + ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)" + " VALUES (%lld, %d)", (long long)detailID, sec); + ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d" + " where SEC = %Q and ID = %lld", + sec, current, (long long)detailID); + } + else + DPRINTF(E_WARN, L_HTTP, "Tag %s unsupported for writing\n", tag); + } + + if (ret == SQLITE_OK) + BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1); + else + SoapError(h, 501, "Action Failed"); + + ClearNameValueList(&data); } static void @@ -1939,18 +2093,23 @@ SamsungSetBookmark(struct upnphttp * h, const char * action) ObjectID = GetValueFromNameValueList(&data, "ObjectID"); PosSecond = GetValueFromNameValueList(&data, "PosSecond"); - if ( atoi(PosSecond) < 30 ) - PosSecond = "0"; - if( ObjectID && PosSecond ) { - int ret; const char *rid = ObjectID; + int64_t detailID; + int sec = atoi(PosSecond); + int ret; in_magic_container(ObjectID, 0, &rid); - ret = sql_exec(db, "INSERT OR REPLACE into BOOKMARKS" - " VALUES " - "((select DETAIL_ID from OBJECTS where OBJECT_ID = '%q'), %q)", rid, PosSecond); + detailID = sql_get_int64_field(db, "SELECT DETAIL_ID from OBJECTS where OBJECT_ID = '%q'", rid); + + if ( sec < 30 ) + sec = 0; + ret = sql_exec(db, "INSERT OR IGNORE into BOOKMARKS (ID, SEC)" + " VALUES (%lld, %d)", (long long)detailID, sec); + ret = sql_exec(db, "UPDATE BOOKMARKS set SEC = %d" + " where ID = %lld", + sec, (long long)detailID); if( ret != SQLITE_OK ) DPRINTF(E_WARN, L_METADATA, "Error setting bookmark %s on ObjectID='%s'\n", PosSecond, rid); BuildSendAndCloseSoapResp(h, resp, sizeof(resp)-1); @@ -1958,12 +2117,12 @@ SamsungSetBookmark(struct upnphttp * h, const char * action) else SoapError(h, 402, "Invalid Args"); - ClearNameValueList(&data); + ClearNameValueList(&data); } -static const struct +static const struct { - const char * methodName; + const char * methodName; void (*methodImpl)(struct upnphttp *, const char *); } soapMethods[] = @@ -1980,6 +2139,7 @@ soapMethods[] = { "IsAuthorized", IsAuthorizedValidated}, { "IsValidated", IsAuthorizedValidated}, { "RegisterDevice", RegisterDevice}, + { "UpdateObject", UpdateObject}, { "X_GetFeatureList", SamsungGetFeatureList}, { "X_SetBookmark", SamsungSetBookmark}, { 0, 0 }