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:
parent
2b3bdb8373
commit
4f926639b2
@ -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",
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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);
|
||||
|
||||
|
18
scanner.c
18
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);
|
||||
|
@ -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"
|
||||
");";
|
||||
|
||||
|
||||
|
30
sql.c
30
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;
|
||||
|
@ -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},
|
||||
|
@ -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)
|
||||
|
288
upnpsoap.c
288
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[] =
|
||||
"<s:Envelope "
|
||||
"xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
|
||||
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
@ -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[] =
|
||||
"<u:%sResponse "
|
||||
"xmlns:u=\"%s\">"
|
||||
"<u:%sResponse "
|
||||
"xmlns:u=\"%s\">"
|
||||
"<return>%s</return>"
|
||||
"</u:%sResponse>";
|
||||
"</u:%sResponse>";
|
||||
|
||||
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[] =
|
||||
"<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, "<%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 }
|
||||
|
Loading…
x
Reference in New Issue
Block a user