upnpsoap: Add additonal bookmark support

Add support for upnp:playbackCount and upnp:lastPlaybackPosition tags.
These are used by Kodi to keep track of bookmark information as well as
determining whether to show the checkmark to indicate that the video
has been played.

Also add support for the UpdateObject command, which Kodi uses to
update the playbackCount and lastPlaybackPosition information.

This change requires a DB schema update, which should be done
automatically on the first run.

Inspired by SF user Karsten's patch #167.
This commit is contained in:
Justin Maggard 2017-05-17 12:01:04 -07:00
parent 2b3bdb8373
commit 4f926639b2
9 changed files with 286 additions and 100 deletions

View File

@ -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",

View File

@ -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
};

View File

@ -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.
*

View File

@ -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,7 +55,8 @@ 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, "
@ -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"
");";

14
sql.c
View File

@ -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.
*
@ -94,7 +94,7 @@ sql_get_int_field(sqlite3 *db, const char *fmt, ...)
((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 */
* SQLITE_LOCKED does not, so sleep */
if (result == SQLITE_LOCKED)
sleep(1);
}
@ -153,7 +153,7 @@ sql_get_int64_field(sqlite3 *db, const char *fmt, ...)
((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 */
* SQLITE_LOCKED does not, so sleep */
if (result == SQLITE_LOCKED)
sleep(1);
}
@ -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;

View File

@ -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},

View File

@ -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)

View File

@ -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.
*
@ -392,13 +392,21 @@ GetCurrentConnectionInfo(struct upnphttp * h, const char * action)
#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;
@ -949,10 +965,24 @@ callback(void *args, int argc, char **argv, char **azColName)
if( date && (passed_args->filter & FILTER_DC_DATE) ) {
ret = strcatf(str, "&lt;dc:date&gt;%s&lt;/dc:date&gt;", date);
}
if( passed_args->filter & FILTER_SEC_DCM_INFO ) {
if( (passed_args->filter & FILTER_BOOKMARK_MASK) ) {
/* Get bookmark */
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, "&lt;upnp:lastPlaybackPosition&gt;%d&lt;/upnp:lastPlaybackPosition&gt;",
sec);
}
if( passed_args->filter & FILTER_SEC_DCM_INFO )
ret = strcatf(str, "&lt;sec:dcmInfo&gt;CREATIONDATE=0,FOLDER=%s,BM=%d&lt;/sec:dcmInfo&gt;",
title, sql_get_int_field(db, "SELECT SEC from BOOKMARKS where ID = '%s'", detailID));
title, sec);
if( passed_args->filter & FILTER_UPNP_PLAYBACKCOUNT ) {
ret = strcatf(str, "&lt;upnp:playbackCount&gt;%d&lt;/upnp:playbackCount&gt;",
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) ) {
@ -1881,6 +1911,130 @@ QueryStateVariable(struct upnphttp * h, const char * action)
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[] =
"<u:UpdateObjectResponse"
" xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
"</u:UpdateObjecResponse>";
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, "&lt;%31[^&]&gt;%31[^&]", tag, current) != 2)
continue;
p = strstr(NewTagValue, tag);
if (!p || sscanf(p, "%*[^&]&gt;%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
SamsungGetFeatureList(struct upnphttp * h, const char * action)
{
@ -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);
@ -1980,6 +2139,7 @@ soapMethods[] =
{ "IsAuthorized", IsAuthorizedValidated},
{ "IsValidated", IsAuthorizedValidated},
{ "RegisterDevice", RegisterDevice},
{ "UpdateObject", UpdateObject},
{ "X_GetFeatureList", SamsungGetFeatureList},
{ "X_SetBookmark", SamsungSetBookmark},
{ 0, 0 }