From 5bafa67ba68e6c37f7e4074262176ada205ae41d Mon Sep 17 00:00:00 2001
From: Roger Siddons <rsiddons@mythtv.org>
Date: Fri, 6 May 2016 16:12:19 +0100
Subject: [PATCH 1/3] =?UTF-8?q?Use=20lastPlayPos=20instead=20of=20bookmark?=
 =?UTF-8?q?=E2=80=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Enable 'Play from last play position'

38443b8e disabled playing recordings from last play position mark.
This patch re-enables it for recordings.

Add MConcurrent

Provides a simple version of QtConcurrent::run() that uses MThreadPool rather
than QThreadPool. Useful for starting background threads in 1 line.

Given a class method of:

  void Class::fn(arg1, arg2...)

you can run it in a different thread using:

  MConcurrent::run("thread name", &Class instance, &Class::fn, arg1, arg2...)

Refer to QtConcurrent::run for further details

Restrictions:
1. Accepts 0-5 arguments
2. Only class methods are supported (most typical in Myth)
3. Only non-const classes & methods are supported (most typical in Myth)
4. The method must have return type of void (QFuture is not easily ported to
   MThreadPool. Use signals/events instead)

Add UI progress indicator to Watch Recordings

Adds theme widget 'progresspercent' to watchrecordings window to show
percentage of recording that has been watched.

MythUI: Implement progressbar on buttonlist items

Allows buttonlists to contain a progressbar.

Show play position as a progress bar in Watch recordings

Allows recording list to show part-watched recordings using a progressbar.
Demo uses Mythcenter-wide theme

Move automatic bookmark updates to last play position.

Convert bookmarks to a user aid only. They will never be automatically updated/removed by playback.
Last play position mark is now used instead.

Settings "Clear bookmark on playback" & "Action on playback exit"/"Save position and exit" are
removed (from UI only) as they are now redundant.

Last play position is never updated within 30 secs of playback start.
Thereafter it records position on playback exit (as well as the existing periodic 30 sec update).
At end of recording it is reset.

Note: 'Automatically Mark watched' is now also actioned by end-of-playback dialog (it wasn't previously)
It also will not trigger for first 30 secs of playback.

Default recording playback from last play position

Playback of recordings now starts from:

- last play position, if present
- bookmark, if present (so user can override progstart)
- program start mark, if present
- beginning of file

Menu options allow user to start from last play position, bookmark or 'beginning'
(prog start/file start) where applicable.

Add menu options to Clear bookmark and Clear last play position for recordings

Allow user to easily reset last play position and bookmark.

Start video playback as per recordings

Video playback starts from:

- last played position, if present
- bookmark, if present
- file start

Menu options allow user to explicitly select option and reset last played position & bookmark

Plays from last played position, then bookmark, then file start
Add menu options to Play from bookmark & Play from beginning
Add menu options to Clear Bookmark & Clear last played position

Preview generator uses last played position

Previews are now generated from:

- last played position, if present
- bookmark, if present,
- prog start mark, if present
- existing offset, which us unchanged (it does not use the progstart mark.)

Add progress bar to Upcoming Recordings.

Progress of an active recording is shown by a progress bar and
'progresspercent' theme widget. Both are based on rec start/end and current time.

diff --git a/mythtv/libs/libmyth/programinfo.cpp b/mythtv/libs/libmyth/programinfo.cpp
index d845a28..866d077 100644
--- a/mythtv/libs/libmyth/programinfo.cpp
+++ b/mythtv/libs/libmyth/programinfo.cpp
@@ -230,6 +230,7 @@ ProgramInfo::ProgramInfo(void) :
 
     // everything below this line is not serialized
     availableStatus(asAvailable),
+    progressPercent(0),
     spread(-1),
     startCol(-1),
     sortTitle(),
@@ -314,6 +315,7 @@ ProgramInfo::ProgramInfo(const ProgramInfo &other) :
 
     // everything below this line is not serialized
     availableStatus(other.availableStatus),
+    progressPercent(other.progressPercent),
     spread(other.spread),
     startCol(other.startCol),
     sortTitle(other.sortTitle),
@@ -502,6 +504,7 @@ ProgramInfo::ProgramInfo(
 
     // everything below this line is not serialized
     availableStatus(asAvailable),
+    progressPercent(0),
     spread(-1),
     startCol(-1),
     sortTitle(),
@@ -620,6 +623,7 @@ ProgramInfo::ProgramInfo(
 
     // everything below this line is not serialized
     availableStatus(asAvailable),
+    progressPercent(0),
     spread(-1),
     startCol(-1),
     sortTitle(),
@@ -751,6 +755,7 @@ ProgramInfo::ProgramInfo(
 
     // everything below this line is not serialized
     availableStatus(asAvailable),
+    progressPercent(0),
     spread(-1),
     startCol(-1),
     sortTitle(),
@@ -905,6 +910,7 @@ ProgramInfo::ProgramInfo(
 
     // everything below this line is not serialized
     availableStatus(asAvailable),
+    progressPercent(0),
     spread(-1),
     startCol(-1),
     sortTitle(),
@@ -1259,6 +1265,7 @@ void ProgramInfo::clear(void)
     spread = -1;
     startCol = -1;
     availableStatus = asAvailable;
+    progressPercent = 0;
 
     // Private
     inUseForWhat.clear();
@@ -1702,6 +1709,9 @@ void ProgramInfo::ToMap(InfoMap &progMap,
         progMap["lentime"] = QObject::tr("%n hour(s)","", hours);
     }
 
+    progMap["progresspercent"] =
+            GetProgressPercent() > 0 ? QString::number(GetProgressPercent()) : "";
+
     progMap["rectypechar"] = toQChar(GetRecordingRuleType());
     progMap["rectype"] = ::toString(GetRecordingRuleType());
     QString tmp_rec = progMap["rectype"];
@@ -2665,6 +2675,31 @@ void ProgramInfo::SaveBookmark(uint64_t frame)
 
     set_flag(programflags, FL_BOOKMARK, is_valid);
 
+    UpdateMarkTimeStamp(is_valid);
+    SendUpdateEvent();
+}
+
+void ProgramInfo::SaveLastPlayPos(uint64_t frame, bool notify)
+{
+    LOG(VB_PLAYBACK, LOG_DEBUG,
+        QString("LastPlayPos frame=%1").arg(frame));
+    ClearMarkupMap(MARK_UTIL_LASTPLAYPOS);
+
+    if (frame > 0)
+    {
+        frm_dir_map_t lastPlayPosMap;
+        lastPlayPosMap[frame] = MARK_UTIL_LASTPLAYPOS;
+        SaveMarkupMap(lastPlayPosMap, MARK_UTIL_LASTPLAYPOS);
+    }
+
+    UpdateMarkTimeStamp(IsBookmarkSet());
+
+    if (notify)
+        SendUpdateEvent();
+}
+
+void ProgramInfo::UpdateMarkTimeStamp(bool bookmarked)
+{
     if (IsRecording())
     {
         MSqlQuery query(MSqlQuery::InitCon());
@@ -2672,17 +2707,13 @@ void ProgramInfo::SaveBookmark(uint64_t frame)
             "UPDATE recorded "
             "SET bookmarkupdate = CURRENT_TIMESTAMP, "
             "    bookmark       = :BOOKMARKFLAG "
-            "WHERE chanid    = :CHANID AND "
-            "      starttime = :STARTTIME");
+            "WHERE recordedid = :RECORDEDID");
 
-        query.bindValue(":BOOKMARKFLAG", is_valid);
-        query.bindValue(":CHANID",       chanid);
-        query.bindValue(":STARTTIME",    recstartts);
+        query.bindValue(":BOOKMARKFLAG", bookmarked);
+        query.bindValue(":RECORDEDID",   recordedid);
 
         if (!query.exec())
             MythDB::DBError("bookmark flag update", query);
-
-        SendUpdateEvent();
     }
 }
 
@@ -2769,6 +2800,23 @@ uint64_t ProgramInfo::QueryProgStart(void) const
     return (bookmarkmap.isEmpty()) ? 0 : bookmarkmap.begin().key();
 }
 
+uint64_t ProgramInfo::QueryStartMark(void) const
+{
+    uint64_t start = 0;
+    if ((start = QueryLastPlayPos()) > 0)
+        LOG(VB_PLAYBACK, LOG_INFO, QString("Using last position @ %1").arg(start));
+    else if ((start = QueryBookmark()) > 0)
+        LOG(VB_PLAYBACK, LOG_INFO, QString("Using bookmark @ %1").arg(start));
+    else if (HasCutlist())
+        // Disable progstart if the program has a cutlist.
+        LOG(VB_PLAYBACK, LOG_INFO, "Ignoring progstart as cutlist exists");
+    else if ((start = QueryProgStart()) > 0)
+        LOG(VB_PLAYBACK, LOG_INFO, QString("Using progstart @ %1").arg(start));
+    else 
+        LOG(VB_PLAYBACK, LOG_INFO, "Using file start");
+    return start;
+}
+
 /** \brief Gets any lastplaypos position in database,
  *         unless the ignore lastplaypos flag is set.
  *
diff --git a/mythtv/libs/libmyth/programinfo.h b/mythtv/libs/libmyth/programinfo.h
index 2a1cb20..ca7aa91 100644
--- a/mythtv/libs/libmyth/programinfo.h
+++ b/mythtv/libs/libmyth/programinfo.h
@@ -69,7 +69,7 @@ class MPUBLIC ProgramInfo
   public:
     enum CategoryType { kCategoryNone, kCategoryMovie, kCategorySeries,
                         kCategorySports, kCategoryTVShow };
-                        
+
     /// Null constructor
     ProgramInfo(void);
     /// Copy constructor
@@ -548,6 +548,9 @@ class MPUBLIC ProgramInfo
     void SetPositionMapDBReplacement(PMapDBReplacement *pmap)
         { positionMapDBReplacement = pmap; }
 
+    uint GetProgressPercent() const        { return progressPercent; }
+    void SetProgressPercent(uint progress) { progressPercent = progress; }
+
     // Slow DB gets
     QString     QueryBasename(void) const;
 //  uint64_t    QueryFilesize(void) const; // TODO Remove
@@ -556,6 +559,7 @@ class MPUBLIC ProgramInfo
     uint64_t    QueryBookmark(void) const;
     uint64_t    QueryProgStart(void) const;
     uint64_t    QueryLastPlayPos(void) const;
+    uint64_t    QueryStartMark(void) const;
     CategoryType QueryCategoryType(void) const;
     QStringList QueryDVDBookmark(const QString &serialid) const;
     QStringList QueryBDBookmark(const QString &serialid) const;
@@ -581,6 +585,7 @@ class MPUBLIC ProgramInfo
 
     // Slow DB sets
     virtual void SaveFilesize(uint64_t fsize); /// TODO Move to RecordingInfo
+    void SaveLastPlayPos(uint64_t frame, bool notify = true);
     void SaveBookmark(uint64_t frame);
     void SaveDVDBookmark(const QStringList &fields) const;
     void SaveBDBookmark(const QStringList &fields) const;
@@ -702,6 +707,8 @@ class MPUBLIC ProgramInfo
     bool FromStringList(QStringList::const_iterator &it,
                         QStringList::const_iterator  end);
 
+    void UpdateMarkTimeStamp(bool bookmarked);
+
     static void QueryMarkupMap(
         const QString &video_pathname,
         frm_dir_map_t&, MarkTypes type, bool merge = false);
@@ -782,6 +789,8 @@ class MPUBLIC ProgramInfo
 
 // everything below this line is not serialized
     uint8_t availableStatus; // only used for playbackbox.cpp
+    uint  progressPercent; // only used by UI
+
   public:
     void SetAvailableStatus(AvailableStatusType status, const QString &where);
     AvailableStatusType GetAvailableStatus(void) const
diff --git a/mythtv/libs/libmythbase/libmythbase.pro b/mythtv/libs/libmythbase/libmythbase.pro
index 228617a..3e44184 100644
--- a/mythtv/libs/libmythbase/libmythbase.pro
+++ b/mythtv/libs/libmythbase/libmythbase.pro
@@ -10,7 +10,7 @@ INSTALLS = target
 QMAKE_CLEAN += $(TARGET) $(TARGETA) $(TARGETD) $(TARGET0) $(TARGET1) $(TARGET2)
 
 # Input
-HEADERS += mthread.h mthreadpool.h
+HEADERS += mthread.h mthreadpool.h mconcurrent.h
 HEADERS += mythsocket.h mythsocket_cb.h
 HEADERS += mythbaseexp.h mythdbcon.h mythdb.h mythdbparams.h oldsettings.h
 HEADERS += verbosedefs.h mythversion.h compat.h mythconfig.h
diff --git a/mythtv/libs/libmythbase/mconcurrent.h b/mythtv/libs/libmythbase/mconcurrent.h
new file mode 100644
index 0000000..5990763
--- /dev/null
+++ b/mythtv/libs/libmythbase/mconcurrent.h
@@ -0,0 +1,176 @@
+#ifndef MCONCURRENT_H
+#define MCONCURRENT_H
+
+#include "mthreadpool.h"
+#include "logging.h"
+
+
+/// Provides a simple version of QtConcurrent::run() that uses MThreadPool rather
+/// than QThreadPool. Useful for starting background threads in 1 line.
+///
+/// Given a class method of:
+///
+///   void Class::fn(arg1, arg2...)
+///
+/// you can run it in a different thread using:
+///
+///   MConcurrent::run("thread name", &Class instance, &Class::fn, arg1, arg2...)
+///
+/// Refer to QtConcurrent::run for further details
+///
+/// Restrictions:
+/// 1. Accepts 0-5 arguments
+/// 2. Only class methods are supported (most typical in Myth)
+/// 3. Only non-const classes & methods are supported (most typical in Myth)
+/// 4. The method must have return type of void (QFuture is not easily ported to
+/// MThreadPool. Use signals/events instead)
+///
+namespace MConcurrent {
+
+class RunFunctionTask : public QRunnable
+{
+public:
+    void start(QString name)
+    {
+        MThreadPool::globalInstance()->start(this, name, /*m_priority*/ 0);
+    }
+
+    virtual void runFunctor() = 0;
+
+    void run()
+    {
+        try
+        {
+            this->runFunctor();
+        }
+        catch (...)
+        {
+            LOG(VB_GENERAL, LOG_ERR, "An exception occurred");
+        }
+    }
+};
+
+template <typename Class>
+class VoidStoredMemberFunctionPointerCall0 : public RunFunctionTask
+{
+public:
+    VoidStoredMemberFunctionPointerCall0(void (Class::*_fn)() , Class *_object)
+    : fn(_fn), object(_object) { }
+
+    void runFunctor() { (object->*fn)(); }
+private:
+    void (Class::*fn)();
+    Class *object;
+};
+
+template <typename Class, typename Param1, typename Arg1>
+class VoidStoredMemberFunctionPointerCall1 : public RunFunctionTask
+{
+public:
+    VoidStoredMemberFunctionPointerCall1(void (Class::*_fn)(Param1) , Class *_object, const Arg1 &_arg1)
+    : fn(_fn), object(_object), arg1(_arg1){ }
+
+    void runFunctor() { (object->*fn)(arg1); }
+private:
+    void (Class::*fn)(Param1);
+    Class *object;
+    Arg1 arg1;
+};
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2>
+class VoidStoredMemberFunctionPointerCall2 : public RunFunctionTask
+{
+public:
+    VoidStoredMemberFunctionPointerCall2(void (Class::*_fn)(Param1, Param2) , Class *_object, const Arg1 &_arg1, const Arg2 &_arg2)
+    : fn(_fn), object(_object), arg1(_arg1), arg2(_arg2){ }
+
+    void runFunctor() { (object->*fn)(arg1, arg2); }
+private:
+    void (Class::*fn)(Param1, Param2);
+    Class *object;
+    Arg1 arg1; Arg2 arg2;
+};
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2, typename Param3, typename Arg3>
+class VoidStoredMemberFunctionPointerCall3 : public RunFunctionTask
+{
+public:
+    VoidStoredMemberFunctionPointerCall3(void (Class::*_fn)(Param1, Param2, Param3) , Class *_object, const Arg1 &_arg1, const Arg2 &_arg2, const Arg3 &_arg3)
+    : fn(_fn), object(_object), arg1(_arg1), arg2(_arg2), arg3(_arg3){ }
+
+    void runFunctor() { (object->*fn)(arg1, arg2, arg3); }
+private:
+    void (Class::*fn)(Param1, Param2, Param3);
+    Class *object;
+    Arg1 arg1; Arg2 arg2; Arg3 arg3;
+};
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2, typename Param3, typename Arg3, typename Param4, typename Arg4>
+class VoidStoredMemberFunctionPointerCall4 : public RunFunctionTask
+{
+public:
+    VoidStoredMemberFunctionPointerCall4(void (Class::*_fn)(Param1, Param2, Param3, Param4) , Class *_object, const Arg1 &_arg1, const Arg2 &_arg2, const Arg3 &_arg3, const Arg4 &_arg4)
+    : fn(_fn), object(_object), arg1(_arg1), arg2(_arg2), arg3(_arg3), arg4(_arg4){ }
+
+    void runFunctor() { (object->*fn)(arg1, arg2, arg3, arg4); }
+private:
+    void (Class::*fn)(Param1, Param2, Param3, Param4);
+    Class *object;
+    Arg1 arg1; Arg2 arg2; Arg3 arg3; Arg4 arg4;
+};
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2, typename Param3, typename Arg3, typename Param4, typename Arg4, typename Param5, typename Arg5>
+class VoidStoredMemberFunctionPointerCall5 : public RunFunctionTask
+{
+public:
+    VoidStoredMemberFunctionPointerCall5(void (Class::*_fn)(Param1, Param2, Param3, Param4, Param5) , Class *_object, const Arg1 &_arg1, const Arg2 &_arg2, const Arg3 &_arg3, const Arg4 &_arg4, const Arg5 &_arg5)
+    : fn(_fn), object(_object), arg1(_arg1), arg2(_arg2), arg3(_arg3), arg4(_arg4), arg5(_arg5){ }
+
+    void runFunctor() { (object->*fn)(arg1, arg2, arg3, arg4, arg5); }
+private:
+    void (Class::*fn)(Param1, Param2, Param3, Param4, Param5);
+    Class *object;
+    Arg1 arg1; Arg2 arg2; Arg3 arg3; Arg4 arg4; Arg5 arg5;
+};
+
+template <typename Class>
+void run(const QString &name, Class *object, void (Class::*fn)())
+{
+    (new VoidStoredMemberFunctionPointerCall0<Class>(fn, object))->start(name);
+}
+
+template <typename Class, typename Param1, typename Arg1>
+void run(const QString &name, Class *object, void (Class::*fn)(Param1), const Arg1 &arg1)
+{
+    (new VoidStoredMemberFunctionPointerCall1<Class, Param1, Arg1>(fn, object, arg1))->start(name);
+}
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2>
+void run(const QString &name, Class *object, void (Class::*fn)(Param1, Param2), const Arg1 &arg1, const Arg2 &arg2)
+{
+    (new VoidStoredMemberFunctionPointerCall2<Class, Param1, Arg1, Param2, Arg2>(fn, object, arg1, arg2))->start(name);
+}
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2, typename Param3, typename Arg3>
+void run(const QString &name, Class *object, void (Class::*fn)(Param1, Param2, Param3), const Arg1 &arg1, const Arg2 &arg2, const Arg3 &arg3)
+{
+    (new VoidStoredMemberFunctionPointerCall3<Class, Param1, Arg1, Param2, Arg2, Param3, Arg3>(fn, object, arg1, arg2, arg3))->start(name);
+}
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2, typename Param3, typename Arg3, typename Param4, typename Arg4>
+void run(const QString &name, Class *object, void (Class::*fn)(Param1, Param2, Param3, Param4), const Arg1 &arg1, const Arg2 &arg2, const Arg3 &arg3, const Arg4 &arg4)
+{
+    (new VoidStoredMemberFunctionPointerCall4<Class, Param1, Arg1, Param2, Arg2, Param3, Arg3, Param4, Arg4>(fn, object, arg1, arg2, arg3, arg4))->start(name);
+}
+
+template <typename Class, typename Param1, typename Arg1, typename Param2, typename Arg2, typename Param3, typename Arg3, typename Param4, typename Arg4, typename Param5, typename Arg5>
+void run(const QString &name, Class *object, void (Class::*fn)(Param1, Param2, Param3, Param4, Param5), const Arg1 &arg1, const Arg2 &arg2, const Arg3 &arg3, const Arg4 &arg4, const Arg4 &arg5)
+{
+    (new VoidStoredMemberFunctionPointerCall5<Class, Param1, Arg1, Param2, Arg2, Param3, Arg3, Param4, Arg4, Param5, Arg5>(fn, object, arg1, arg2, arg3, arg4, arg5))->start(name);
+}
+
+
+} //namespace QtConcurrent
+
+#endif // MCONCURRENT_H
+
diff --git a/mythtv/libs/libmythtv/mythplayer.cpp b/mythtv/libs/libmythtv/mythplayer.cpp
index f953e11..c0bbab0 100644
--- a/mythtv/libs/libmythtv/mythplayer.cpp
+++ b/mythtv/libs/libmythtv/mythplayer.cpp
@@ -238,7 +238,6 @@ MythPlayer::MythPlayer(PlayerFlags flags)
     captionsEnabledbyDefault = gCoreContext->GetNumSetting("DefaultCCMode");
     decode_extra_audio = gCoreContext->GetNumSetting("DecodeExtraAudio", 0);
     itvEnabled         = gCoreContext->GetNumSetting("EnableMHEG", 0);
-    clearSavedPosition = gCoreContext->GetNumSetting("ClearSavedPosition", 1);
     endExitPrompt      = gCoreContext->GetNumSetting("EndOfRecordingExitPrompt");
     pip_default_loc    = (PIPLocation)gCoreContext->GetNumSetting("PIPLocation", kPIPTopLeft);
 
@@ -2901,8 +2900,6 @@ void MythPlayer::InitialSeek(void)
     if (bookmarkseek > 30)
     {
         DoJumpToFrame(bookmarkseek, kInaccuracyNone);
-        if (clearSavedPosition && !player_ctx->IsPIP())
-            SetBookmark(true);
     }
 }
 
@@ -3666,12 +3663,7 @@ uint64_t MythPlayer::GetBookmark(void)
         player_ctx->LockPlayingInfo(__FILE__, __LINE__);
         if (const ProgramInfo *pi = player_ctx->playingInfo)
         {
-            bookmark = pi->QueryBookmark();
-            // Disable progstart if the program has a cutlist.
-            if (bookmark == 0 && !pi->HasCutlist())
-                bookmark = pi->QueryProgStart();
-            if (bookmark == 0)
-                bookmark = pi->QueryLastPlayPos();
+            bookmark = pi->QueryStartMark();
         }
         player_ctx->UnlockPlayingInfo(__FILE__, __LINE__);
     }
diff --git a/mythtv/libs/libmythtv/mythplayer.h b/mythtv/libs/libmythtv/mythplayer.h
index bfdac87..667c6d4 100644
--- a/mythtv/libs/libmythtv/mythplayer.h
+++ b/mythtv/libs/libmythtv/mythplayer.h
@@ -684,7 +684,6 @@ class MTV_PUBLIC MythPlayer
 
     // Bookmark stuff
     uint64_t bookmarkseek;
-    int      clearSavedPosition;
     int      endExitPrompt;
 
     // Seek
diff --git a/mythtv/libs/libmythtv/previewgenerator.cpp b/mythtv/libs/libmythtv/previewgenerator.cpp
index c138f5e..a2ec613 100644
--- a/mythtv/libs/libmythtv/previewgenerator.cpp
+++ b/mythtv/libs/libmythtv/previewgenerator.cpp
@@ -618,8 +618,9 @@ bool PreviewGenerator::SavePreview(const QString &filename,
 bool PreviewGenerator::LocalPreviewRun(void)
 {
     m_programInfo.MarkAsInUse(true, kPreviewGeneratorInUseID);
-    m_programInfo.SetIgnoreProgStart(true);
-    m_programInfo.SetAllowLastPlayPos(false);
+    m_programInfo.SetIgnoreProgStart(false);
+    m_programInfo.SetIgnoreBookmark(false);
+    m_programInfo.SetAllowLastPlayPos(true);
 
     float aspect = 0;
     int   width, height, sz;
@@ -632,15 +633,14 @@ bool PreviewGenerator::LocalPreviewRun(void)
 
     if (captime < 0)
     {
-        captime = m_programInfo.QueryBookmark();
-        if (captime > 0)
+        uint64_t markFrame = m_programInfo.QueryStartMark();
+        LOG(VB_GENERAL, LOG_INFO,
+            QString("Preview from start mark (frame %1)").arg(markFrame));
+        if (markFrame > 0)
         {
             m_timeInSeconds = false;
-            LOG(VB_GENERAL, LOG_INFO,
-                QString("Preview from bookmark (frame %1)").arg(captime));
+            captime         = markFrame;
         }
-        else
-            captime = -1;
     }
 
     if (captime <= 0)
diff --git a/mythtv/libs/libmythtv/tv_play.cpp b/mythtv/libs/libmythtv/tv_play.cpp
index 9b9511b..dbc5d39 100644
--- a/mythtv/libs/libmythtv/tv_play.cpp
+++ b/mythtv/libs/libmythtv/tv_play.cpp
@@ -35,6 +35,7 @@ using namespace std;
 #include "compat.h"
 #include "mythdirs.h"
 #include "mythmedia.h"
+#include "mconcurrent.h"
 
 // libmyth
 #include "programinfo.h"
@@ -1013,7 +1014,6 @@ TV::TV(void)
       db_playback_exit_prompt(0),   db_autoexpire_default(0),
       db_auto_set_watched(false),   db_end_of_rec_exit_prompt(false),
       db_jump_prefer_osd(true),     db_use_gui_size_for_tv(false),
-      db_clear_saved_position(false),
       db_toggle_bookmark(false),
       db_run_jobs_on_remote(false), db_continue_embedded(false),
       db_use_fixed_size(true),      db_browse_always(false),
@@ -1038,6 +1038,7 @@ TV::TV(void)
       requestDelete(false),  allowRerecord(false),
       doSmartForward(false),
       queuedTranscode(false),
+      savePosOnExit(false),
       adjustingPicture(kAdjustingPicture_None),
       adjustingPictureAttribute(kPictureAttribute_None),
       askAllowLock(QMutex::Recursive),
@@ -1121,7 +1122,6 @@ void TV::InitFromDB(void)
     kv["EndOfRecordingExitPrompt"] = "0";
     kv["JumpToProgramOSD"]         = "1";
     kv["GuiSizeForTV"]             = "0";
-    kv["ClearSavedPosition"]       = "1";
     kv["AltClearSavedPosition"]    = "1";
     kv["JobsRunOnRecordHost"]      = "0";
     kv["ContinueEmbeddedTVPlay"]   = "0";
@@ -1172,7 +1172,6 @@ void TV::InitFromDB(void)
     db_end_of_rec_exit_prompt = kv["EndOfRecordingExitPrompt"].toInt();
     db_jump_prefer_osd     = kv["JumpToProgramOSD"].toInt();
     db_use_gui_size_for_tv = kv["GuiSizeForTV"].toInt();
-    db_clear_saved_position= kv["ClearSavedPosition"].toInt();
     db_toggle_bookmark     = kv["AltClearSavedPosition"].toInt();
     db_run_jobs_on_remote  = kv["JobsRunOnRecordHost"].toInt();
     db_continue_embedded   = kv["ContinueEmbeddedTVPlay"].toInt();
@@ -1358,6 +1357,8 @@ bool TV::Init(bool createWindow)
     speedChangeTimerId   = StartTimer(kSpeedChangeCheckFrequency, __LINE__);
     saveLastPlayPosTimerId = StartTimer(kSaveLastPlayPosTimeout, __LINE__);
 
+    savePosOnExit = false;
+
     LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "-- end");
     return true;
 }
@@ -3348,51 +3349,22 @@ void TV::PrepToSwitchToRecordedProgram(PlayerContext *ctx,
     SetExitPlayer(true, true);
 }
 
-void TV::PrepareToExitPlayer(PlayerContext *ctx, int line, BookmarkAction bookmark)
+void TV::PrepareToExitPlayer(PlayerContext *ctx, int line)
 {
-    bool bm_allowed = IsBookmarkAllowed(ctx);
     ctx->LockDeletePlayer(__FILE__, line);
-    if (ctx->player)
+    if (savePosOnExit && ctx->player && ctx->playingInfo)
     {
-        if (bm_allowed)
-        {
-            // If we're exiting in the middle of the recording, we
-            // automatically save a bookmark when "Action on playback
-            // exit" is set to "Save position and exit".
-            bool allow_set_before_end =
-                (bookmark == kBookmarkAlways ||
-                 (bookmark == kBookmarkAuto &&
-                  db_playback_exit_prompt == 2));
-            // If we're exiting at the end of the recording, we
-            // automatically clear the bookmark when "Action on
-            // playback exit" is set to "Save position and exit" and
-            // "Clear bookmark on playback" is set to true.
-            bool allow_clear_at_end =
-                (bookmark == kBookmarkAlways ||
-                 (bookmark == kBookmarkAuto &&
-                  db_playback_exit_prompt == 2 &&
-                  db_clear_saved_position));
-            // Whether to set/clear a bookmark depends on whether we're
-            // exiting at the end of a recording.
-            bool at_end = (ctx->player->IsNearEnd() || getEndOfRecording());
-            // Don't consider ourselves at the end if the recording is
-            // in-progress.
-            at_end &= !StateIsRecording(GetState(ctx));
-            bool clear_lastplaypos = false;
-            if (at_end && allow_clear_at_end)
-            {
-                SetBookmark(ctx, true);
-                // Tidy up the lastplaypos mark only when we clear the
-                // bookmark due to exiting at the end.
-                clear_lastplaypos = true;
-            }
-            else if (!at_end && allow_set_before_end)
-            {
-                SetBookmark(ctx, false);
-            }
-            if (clear_lastplaypos && ctx->playingInfo)
-                ctx->playingInfo->ClearMarkupMap(MARK_UTIL_LASTPLAYPOS);
-        }
+        // Clear last play position when we're at the end of a recording.
+        // unless the recording is in-progress.
+        bool at_end = !StateIsRecording(GetState(ctx)) &&
+                (getEndOfRecording() || ctx->player->IsNearEnd());
+
+        // Clear/Save play position without notification
+        // The change must be broadcast when file is no longer in use
+        // to update previews, ie. with the MarkNotInUse notification
+        uint64_t frame = at_end ? 0 : ctx->player->GetFramesPlayed();
+        ctx->playingInfo->SaveLastPlayPos(frame, false);
+
         if (db_auto_set_watched)
             ctx->player->SetWatched();
     }
@@ -5079,7 +5051,7 @@ bool TV::ActivePostQHandleAction(PlayerContext *ctx, const QStringList &actions)
     {
         NormalSpeed(ctx);
         StopFFRew(ctx);
-        SetBookmark(ctx);
+        PrepareToExitPlayer(ctx, __LINE__);
         ShowOSDPromptDeleteRecording(ctx, tr("Are you sure you want to delete:"));
     }
     else if (has_action(ACTION_JUMPTODVDROOTMENU, actions) && isdisc)
@@ -5301,11 +5273,7 @@ void TV::ProcessNetworkControlCommand(PlayerContext *ctx,
     }
     else if (tokens.size() == 2 && tokens[1] == "STOP")
     {
-        SetBookmark(ctx);
-        ctx->LockDeletePlayer(__FILE__, __LINE__);
-        if (ctx->player && db_auto_set_watched)
-            ctx->player->SetWatched();
-        ctx->UnlockDeletePlayer(__FILE__, __LINE__);
+        PrepareToExitPlayer(ctx, __LINE__);
         SetExitPlayer(true, true);
     }
     else if (tokens.size() >= 3 && tokens[1] == "SEEK" && ctx->HasPlayer())
@@ -9502,7 +9470,7 @@ void TV::customEvent(QEvent *e)
             for (uint i = 0; mctx && (i < player.size()); i++)
             {
                 PlayerContext *ctx = GetPlayer(mctx, i);
-                PrepareToExitPlayer(ctx, __LINE__, kBookmarkAuto);
+                PrepareToExitPlayer(ctx, __LINE__);
             }
 
             SetExitPlayer(true, true);
@@ -13255,16 +13223,8 @@ void TV::ShowOSDStopWatchingRecording(PlayerContext *ctx)
         osd->DialogShow(OSD_DLG_VIDEOEXIT,
                         tr("You are exiting %1").arg(videotype));
 
-        if (IsBookmarkAllowed(ctx))
-        {
-            osd->DialogAddButton(tr("Save this position and go to the menu"),
-                                 "DIALOG_VIDEOEXIT_SAVEPOSITIONANDEXIT_0");
-            osd->DialogAddButton(tr("Do not save, just exit to the menu"),
-                                 ACTION_STOP);
-        }
-        else
-            osd->DialogAddButton(tr("Exit %1").arg(videotype),
-                                 ACTION_STOP);
+        osd->DialogAddButton(tr("Exit %1").arg(videotype),
+                             ACTION_STOP);
 
         if (IsDeleteAllowed(ctx))
             osd->DialogAddButton(tr("Delete this recording"),
@@ -13409,7 +13369,6 @@ bool TV::HandleOSDVideoExit(PlayerContext *ctx, QString action)
 
     bool hide        = true;
     bool delete_ok   = IsDeleteAllowed(ctx);
-    bool bookmark_ok = IsBookmarkAllowed(ctx);
 
     ctx->LockDeletePlayer(__FILE__, __LINE__);
     bool near_end = ctx->player && ctx->player->IsNearEnd();
@@ -13419,11 +13378,13 @@ bool TV::HandleOSDVideoExit(PlayerContext *ctx, QString action)
     {
         allowRerecord = true;
         requestDelete = true;
+        PrepareToExitPlayer(ctx, __LINE__);
         SetExitPlayer(true, true);
     }
     else if (action == "JUSTDELETE" && delete_ok)
     {
         requestDelete = true;
+        PrepareToExitPlayer(ctx, __LINE__);
         SetExitPlayer(true, true);
     }
     else if (action == "CONFIRMDELETE")
@@ -13432,11 +13393,6 @@ bool TV::HandleOSDVideoExit(PlayerContext *ctx, QString action)
         ShowOSDPromptDeleteRecording(ctx, tr("Are you sure you want to delete:"),
                                      true);
     }
-    else if (action == "SAVEPOSITIONANDEXIT" && bookmark_ok)
-    {
-        PrepareToExitPlayer(ctx, __LINE__, kBookmarkAlways);
-        SetExitPlayer(true, true);
-    }
     else if (action == "KEEPWATCHING" && !near_end)
     {
         DoTogglePause(ctx, true);
@@ -13447,29 +13403,6 @@ bool TV::HandleOSDVideoExit(PlayerContext *ctx, QString action)
 
 void TV::HandleSaveLastPlayPosEvent(void)
 {
-    // Helper class to save the latest playback position (in a background thread
-    // to avoid playback glitches).  The ctor makes a copy of the ProgramInfo
-    // struct to avoid race conditions if playback ends and deletes objects
-    // before or while the background thread runs.
-    class PositionSaver : public QRunnable
-    {
-    public:
-        PositionSaver(const ProgramInfo &pginfo, uint64_t frame) :
-            m_pginfo(pginfo), m_frame(frame) {}
-        virtual void run(void)
-        {
-            LOG(VB_PLAYBACK, LOG_DEBUG,
-                QString("PositionSaver frame=%1").arg(m_frame));
-            frm_dir_map_t lastPlayPosMap;
-            lastPlayPosMap[m_frame] = MARK_UTIL_LASTPLAYPOS;
-            m_pginfo.ClearMarkupMap(MARK_UTIL_LASTPLAYPOS);
-            m_pginfo.SaveMarkupMap(lastPlayPosMap, MARK_UTIL_LASTPLAYPOS);
-        }
-    private:
-        const ProgramInfo m_pginfo;
-        const uint64_t m_frame;
-    };
-
     PlayerContext *mctx = GetPlayerReadLock(0, __FILE__, __LINE__);
     for (uint i = 0; mctx && i < player.size(); ++i)
     {
@@ -13478,10 +13411,11 @@ void TV::HandleSaveLastPlayPosEvent(void)
         bool playing = ctx->player && !ctx->player->IsPaused();
         if (playing) // Don't bother saving lastplaypos while paused
         {
+            // Save the latest playback position in a background thread
+            // to avoid playback glitches.
             uint64_t framesPlayed = ctx->player->GetFramesPlayed();
-            MThreadPool::globalInstance()->
-                start(new PositionSaver(*ctx->playingInfo, framesPlayed),
-                      "PositionSaver");
+            MConcurrent::run("PositionSaver", ctx->playingInfo,
+                             &ProgramInfo::SaveLastPlayPos, framesPlayed, true);
         }
         ctx->UnlockDeletePlayer(__FILE__, __LINE__);
     }
@@ -13490,6 +13424,8 @@ void TV::HandleSaveLastPlayPosEvent(void)
     QMutexLocker locker(&timerIdLock);
     KillTimer(saveLastPlayPosTimerId);
     saveLastPlayPosTimerId = StartTimer(kSaveLastPlayPosTimeout, __LINE__);
+
+    savePosOnExit = true;
 }
 
 void TV::SetLastProgram(const ProgramInfo *rcinfo)
diff --git a/mythtv/libs/libmythtv/tv_play.h b/mythtv/libs/libmythtv/tv_play.h
index 0678cbb..b026781 100644
--- a/mythtv/libs/libmythtv/tv_play.h
+++ b/mythtv/libs/libmythtv/tv_play.h
@@ -366,7 +366,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     bool Init(bool createWindow = true);
     void InitFromDB(void);
     QList<QKeyEvent> ConvertScreenPressKeyMap(const QString& keyList);
-    
+
     // Top level playback methods
     bool LiveTV(bool showDialogs, const ChannelInfoList &selection);
     int  Playback(const ProgramInfo &rcinfo);
@@ -375,7 +375,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     // Private event handling
     bool ProcessKeypressOrGesture(PlayerContext*, QEvent *e);
     bool TranslateKeyPressOrGesture(const QString &context, QEvent *e,
-                                    QStringList &actions, bool isLiveTV, 
+                                    QStringList &actions, bool isLiveTV,
                                     bool allowJumps = true);
     bool TranslateGesture(const QString &context, MythGestureEvent *e,
                           QStringList &actions, bool isLiveTV);
@@ -483,8 +483,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
         kBookmarkNever,
         kBookmarkAuto // set iff db_playback_exit_prompt==2
     };
-    void PrepareToExitPlayer(PlayerContext*, int line,
-                             BookmarkAction bookmark = kBookmarkAuto);
+    void PrepareToExitPlayer(PlayerContext*, int line);
     void SetExitPlayer(bool set_it, bool wants_to);
 
     bool RequestNextRecorder(PlayerContext *, bool,
@@ -795,7 +794,6 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     bool    db_jump_prefer_osd;
     bool    db_use_gui_size_for_tv;
     bool    db_start_in_guide;
-    bool    db_clear_saved_position;
     bool    db_toggle_bookmark;
     bool    db_run_jobs_on_remote;
     bool    db_continue_embedded;
@@ -839,6 +837,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     bool allowRerecord;  ///< User wants to rerecord the last video if deleted
     bool doSmartForward;
     bool queuedTranscode;
+    bool savePosOnExit;  ///< False until first timer event
     /// Picture attribute type to modify.
     PictureAdjustType adjustingPicture;
     /// Picture attribute to modify (on arrow left or right)
@@ -899,7 +898,7 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer
     static const int screenPressRegionCount = 12;
     QList<QKeyEvent>    screenPressKeyMapPlayback;
     QList<QKeyEvent>    screenPressKeyMapLiveTV;
-    
+
     // Channel changing timeout notification variables
     QTime   lockTimer;
     bool    lockTimerOn;
diff --git a/mythtv/libs/libmythui/mythuibuttonlist.cpp b/mythtv/libs/libmythui/mythuibuttonlist.cpp
index 9c25ff4..1e013d0 100644
--- a/mythtv/libs/libmythui/mythuibuttonlist.cpp
+++ b/mythtv/libs/libmythui/mythuibuttonlist.cpp
@@ -21,6 +21,7 @@
 #include "mythuigroup.h"
 #include "mythuiimage.h"
 #include "mythgesture.h"
+#include "mythuiprogressbar.h"
 
 #define LOC     QString("MythUIButtonList(%1): ").arg(objectName())
 
@@ -3133,6 +3134,9 @@ MythUIButtonListItem::MythUIButtonListItem(MythUIButtonList *lbtype,
     m_showArrow = showArrow;
     m_data      = 0;
     m_isVisible = false;
+    m_progress  = 0;
+    m_progressStart = 0;
+    m_progressTotal = 0;
 
     if (state >= NotChecked)
         m_checkable = true;
@@ -3158,6 +3162,9 @@ MythUIButtonListItem::MythUIButtonListItem(MythUIButtonList *lbtype,
     m_state     = CantCheck;
     m_showArrow = false;
     m_isVisible = false;
+    m_progress  = 0;
+    m_progressStart = 0;
+    m_progressTotal = 0;
 
     if (m_parent)
         m_parent->InsertItem(this, listPosition);
@@ -3413,6 +3420,16 @@ QString MythUIButtonListItem::GetImageFilename(const QString &name) const
     return QString();
 }
 
+void MythUIButtonListItem::SetProgress(int start, int total, int used)
+{
+    m_progress      = used;
+    m_progressStart = start;
+    m_progressTotal = total;
+
+    if (m_parent && m_isVisible)
+        m_parent->Update();
+}
+
 void MythUIButtonListItem::DisplayState(const QString &state,
                                         const QString &name)
 {
@@ -3707,6 +3724,11 @@ void MythUIButtonListItem::SetToRealButton(MythUIStateType *button, bool selecte
     // There is no need to check the return value here, since we already
     // checked that the state exists with GetState() earlier
     button->DisplayState(state);
+
+    MythUIProgressBar *progress = dynamic_cast<MythUIProgressBar *>
+                                  (buttonstate->GetChild("buttonprogress"));
+    if (progress)
+        progress->Set(m_progressStart, m_progressTotal, m_progress);
 }
 
 //---------------------------------------------------------
diff --git a/mythtv/libs/libmythui/mythuibuttonlist.h b/mythtv/libs/libmythui/mythuibuttonlist.h
index 3838fc6..2efc533 100644
--- a/mythtv/libs/libmythui/mythuibuttonlist.h
+++ b/mythtv/libs/libmythui/mythuibuttonlist.h
@@ -81,6 +81,8 @@ class MUI_PUBLIC MythUIButtonListItem
     void SetImageFromMap(const InfoMap &imageMap);
     QString GetImageFilename(const QString &name="") const;
 
+    void SetProgress(int start, int total, int used);
+    
     void DisplayState(const QString &state, const QString &name);
     void SetStatesFromMap(const InfoMap &stateMap);
 
@@ -113,6 +115,9 @@ class MUI_PUBLIC MythUIButtonListItem
     QVariant        m_data;
     bool            m_showArrow;
     bool            m_isVisible;
+    int             m_progress;
+    int             m_progressStart;
+    int             m_progressTotal;
 
     QMap<QString, TextProperties> m_strings;
     QMap<QString, MythImage*> m_images;
diff --git a/mythtv/libs/libmythui/mythuiprogressbar.cpp b/mythtv/libs/libmythui/mythuiprogressbar.cpp
index 4a7fdf4..2942c47 100644
--- a/mythtv/libs/libmythui/mythuiprogressbar.cpp
+++ b/mythtv/libs/libmythui/mythuiprogressbar.cpp
@@ -58,6 +58,16 @@ bool MythUIProgressBar::ParseElement(
     return true;
 }
 
+void MythUIProgressBar::Set(int start, int total, int used)
+{
+    if (used != m_current || start != m_start || total != m_total)
+    {
+        m_start = start;
+        m_total = total;
+        SetUsed(used);
+    }
+}
+
 void MythUIProgressBar::SetStart(int value)
 {
     m_start = value;
diff --git a/mythtv/libs/libmythui/mythuiprogressbar.h b/mythtv/libs/libmythui/mythuiprogressbar.h
index fea6eb5..dfa4ac7 100644
--- a/mythtv/libs/libmythui/mythuiprogressbar.h
+++ b/mythtv/libs/libmythui/mythuiprogressbar.h
@@ -20,6 +20,7 @@ class MUI_PUBLIC MythUIProgressBar : public MythUIType
     enum LayoutType { LayoutVertical, LayoutHorizontal };
     enum EffectType { EffectReveal, EffectSlide, EffectAnimate };
 
+    void Set(int start, int total, int used);
     void SetStart(int);
     void SetUsed(int);
     void SetTotal(int);
diff --git a/mythtv/programs/mythfrontend/globalsettings.cpp b/mythtv/programs/mythfrontend/globalsettings.cpp
index f07e4fe..4a8a5b3 100644
--- a/mythtv/programs/mythfrontend/globalsettings.cpp
+++ b/mythtv/programs/mythfrontend/globalsettings.cpp
@@ -1656,23 +1656,6 @@ static HostCheckBox *BrowseAllTuners()
     return gc;
 }
 
-static HostCheckBox *ClearSavedPosition()
-{
-    HostCheckBox *gc = new HostCheckBox("ClearSavedPosition");
-
-    gc->setLabel(PlaybackSettings::tr("Clear bookmark on playback"));
-
-    gc->setValue(true);
-
-    gc->setHelpText(PlaybackSettings::tr("If enabled, automatically clear the "
-                                         "bookmark on a recording when the "
-                                         "recording is played back. If "
-                                         "disabled, you can mark the "
-                                         "beginning with rewind then save "
-                                         "position."));
-    return gc;
-}
-
 static HostCheckBox *AltClearSavedPosition()
 {
     HostCheckBox *gc = new HostCheckBox("AltClearSavedPosition");
@@ -1697,7 +1680,6 @@ static HostComboBox *PlaybackExitPrompt()
     gc->setLabel(PlaybackSettings::tr("Action on playback exit"));
 
     gc->addSelection(PlaybackSettings::tr("Just exit"), "0");
-    gc->addSelection(PlaybackSettings::tr("Save position and exit"), "2");
     gc->addSelection(PlaybackSettings::tr("Always prompt (excluding Live TV)"),
                      "1");
     gc->addSelection(PlaybackSettings::tr("Always prompt (including Live TV)"),
@@ -1707,9 +1689,8 @@ static HostComboBox *PlaybackExitPrompt()
     gc->setHelpText(PlaybackSettings::tr("If set to prompt, a menu will be "
                                          "displayed when you exit playback "
                                          "mode. The options available will "
-                                         "allow you to save your position, "
-                                         "delete the recording, or continue "
-                                         "watching."));
+                                         "allow you to delete the recording, "
+                                         "continue watching or exit."));
     return gc;
 }
 
@@ -3924,7 +3905,6 @@ PlaybackSettings::PlaybackSettings()
 
     VerticalConfigurationGroup *column2 =
         new VerticalConfigurationGroup(false, false, true, true);
-    column2->addChild(ClearSavedPosition());
     column2->addChild(AltClearSavedPosition());
     column2->addChild(AutomaticSetWatched());
     column2->addChild(ContinueEmbeddedTVPlay());
diff --git a/mythtv/programs/mythfrontend/main.cpp b/mythtv/programs/mythfrontend/main.cpp
index 62ba4b2..a6f6f15 100644
--- a/mythtv/programs/mythfrontend/main.cpp
+++ b/mythtv/programs/mythfrontend/main.cpp
@@ -194,17 +194,23 @@ namespace
         Q_DECLARE_TR_FUNCTIONS(BookmarkDialog)
 
       public:
-        BookmarkDialog(ProgramInfo *pginfo, MythScreenStack *parent) :
+        BookmarkDialog(ProgramInfo *pginfo, MythScreenStack *parent,
+                       bool bookmarkPresent, bool lastPlayPresent) :
                 MythScreenType(parent, "bookmarkdialog"),
-                pgi(pginfo)
+                pgi(pginfo),
+                bookmarked(bookmarkPresent),
+                lastPlayed(lastPlayPresent),
+                btnPlayBookmark(tr("Play from bookmark")),
+                btnClearBookmark(tr("Clear bookmark")),
+                btnPlayBegin(tr("Play from beginning")),
+                btnPlayLast(tr("Play from last played position")),
+                btnClearLastPlay(tr("Clear last played position"))
         {
         }
 
         bool Create()
         {
             QString msg = tr("DVD/Video contains a bookmark");
-            QString btn0msg = tr("Play from bookmark");
-            QString btn1msg = tr("Play from beginning");
 
             MythDialogBox *popup = new MythDialogBox(msg, GetScreenStack(), "bookmarkdialog");
             if (!popup->Create())
@@ -216,8 +222,19 @@ namespace
             GetScreenStack()->AddScreen(popup);
 
             popup->SetReturnEvent(this, "bookmarkdialog");
-            popup->AddButton(btn0msg);
-            popup->AddButton(btn1msg);
+            if (bookmarked)
+            {
+                popup->AddButton(btnPlayBookmark);
+                popup->AddButton(btnClearBookmark);
+            }
+
+            popup->AddButton(btnPlayBegin);
+
+            if (lastPlayed)
+            {
+                popup->AddButton(btnPlayLast);
+                popup->AddButton(btnClearLastPlay);
+            }
             return true;
         }
 
@@ -226,22 +243,30 @@ namespace
             if (event->type() == DialogCompletionEvent::kEventType)
             {
                 DialogCompletionEvent *dce = (DialogCompletionEvent*)(event);
-                int buttonnum = dce->GetResult();
+                QString buttonText = dce->GetResultText();
 
                 if (dce->GetId() == "bookmarkdialog")
                 {
-                    uint flags = kStartTVNoFlags;
-                    if (buttonnum == 1)
+                    if (buttonText == btnPlayBookmark)
                     {
-                        flags |= kStartTVIgnoreBookmark;
+                        TV::StartTV(pgi, kStartTVNoFlags );
                     }
-                    else if (buttonnum != 0)
+                    else if (buttonText == btnPlayBegin)
                     {
-                        delete pgi;
-                        return;
+                        TV::StartTV(pgi, kStartTVNoFlags | kStartTVIgnoreBookmark);
+                    }
+                    else if (buttonText == btnPlayLast)
+                    {
+                        TV::StartTV(pgi, kStartTVNoFlags | kStartTVAllowLastPlayPos);
+                    }
+                    else if (buttonText == btnClearBookmark)
+                    {
+                        pgi->SaveBookmark(0);
+                    }
+                    else if (buttonText == btnClearLastPlay)
+                    {
+                        pgi->SaveLastPlayPos(0);
                     }
-
-                    TV::StartTV(pgi, flags);
 
                     delete pgi;
                 }
@@ -250,6 +275,10 @@ namespace
 
       private:
         ProgramInfo* pgi;
+        bool bookmarked, lastPlayed;
+        QString btnPlayBookmark, btnClearBookmark;
+        QString btnPlayBegin;
+        QString btnPlayLast, btnClearLastPlay;
     };
 
     void cleanup()
@@ -1174,6 +1203,7 @@ static int internal_play_media(const QString &mrl, const QString &plot,
     pginfo->SetProgramInfoType(pginfo->DiscoverProgramInfoType());
 
     bool bookmarkPresent = false;
+    bool lastPlayPresent = false;
 
     if (pginfo->IsVideoDVD())
     {
@@ -1224,13 +1254,20 @@ static int internal_play_media(const QString &mrl, const QString &plot,
             return res;
         }
     }
-    else if (pginfo->IsVideo())
-        bookmarkPresent = (pginfo->QueryBookmark() > 0);
+    else if (useBookmark && pginfo->IsVideo())
+    {
+        pginfo->SetAllowLastPlayPos(true);
+        pginfo->SetIgnoreBookmark(false);
+        bookmarkPresent = pginfo->QueryBookmark() > 0;
+        lastPlayPresent = pginfo->QueryLastPlayPos() > 0;
+    }
 
-    if (useBookmark && bookmarkPresent)
+    if (useBookmark && (bookmarkPresent || lastPlayPresent))
     {
         MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack();
-        BookmarkDialog *bookmarkdialog = new BookmarkDialog(pginfo, mainStack);
+        BookmarkDialog *bookmarkdialog = new BookmarkDialog(pginfo, mainStack,
+                                                            bookmarkPresent,
+                                                            lastPlayPresent);
         if (!bookmarkdialog->Create())
         {
             delete bookmarkdialog;
@@ -1655,7 +1692,7 @@ static bool WasAutomaticStart(void)
 }
 
 // from https://www.raspberrypi.org/forums/viewtopic.php?f=33&t=16897
-// The old way of revoking root with setuid(getuid()) 
+// The old way of revoking root with setuid(getuid())
 // causes system hang in certain cases on raspberry pi
 
 static int revokeRoot (void)
diff --git a/mythtv/programs/mythfrontend/playbackbox.cpp b/mythtv/programs/mythfrontend/playbackbox.cpp
index 34815cc..fe71685 100644
--- a/mythtv/programs/mythfrontend/playbackbox.cpp
+++ b/mythtv/programs/mythfrontend/playbackbox.cpp
@@ -40,6 +40,7 @@
 #include "mythdb.h"
 #include "mythdate.h"
 #include "tv.h"
+#include "mconcurrent.h"
 
 #ifdef _MSC_VER
 #  include "compat.h"                   // for random
@@ -562,7 +563,7 @@ bool PlaybackBox::Create()
     connect(m_recordingList, SIGNAL(itemSelected(MythUIButtonListItem*)),
             SLOT(ItemSelected(MythUIButtonListItem*)));
     connect(m_recordingList, SIGNAL(itemClicked(MythUIButtonListItem*)),
-            SLOT(PlayFromBookmarkOrProgStart(MythUIButtonListItem*)));
+            SLOT(PlayFromAnyMark(MythUIButtonListItem*)));
     connect(m_recordingList, SIGNAL(itemVisible(MythUIButtonListItem*)),
             SLOT(ItemVisible(MythUIButtonListItem*)));
     connect(m_recordingList, SIGNAL(itemLoaded(MythUIButtonListItem*)),
@@ -1011,6 +1012,8 @@ void PlaybackBox::ItemVisible(MythUIButtonListItem *item)
     // Flagging status (queued, running, no, yes)
     item->DisplayState(extract_commflag_state(*pginfo), "commflagged");
 
+    item->SetProgress(0, 100, pginfo->GetProgressPercent());
+
     MythUIButtonListItem *sel_item = item->parent()->GetItemCurrent();
     if ((item != sel_item) && item->GetImageFilename("preview").isEmpty() &&
         (asAvailable == pginfo->GetAvailableStatus()))
@@ -2222,7 +2225,7 @@ void PlaybackBox::playSelectedPlaylist(bool _random)
         this, new MythEvent("PLAY_PLAYLIST"));
 }
 
-void PlaybackBox::PlayFromBookmarkOrProgStart(MythUIButtonListItem *item)
+void PlaybackBox::PlayFromAnyMark(MythUIButtonListItem *item)
 {
     if (!item)
         item = m_recordingList->GetItemCurrent();
@@ -2234,7 +2237,7 @@ void PlaybackBox::PlayFromBookmarkOrProgStart(MythUIButtonListItem *item)
 
     const bool ignoreBookmark = false;
     const bool ignoreProgStart = false;
-    const bool ignoreLastPlayPos = true;
+    const bool ignoreLastPlayPos = false;
     const bool underNetworkControl = false;
     if (pginfo)
         PlayX(*pginfo, ignoreBookmark, ignoreProgStart, ignoreLastPlayPos,
@@ -2323,6 +2326,20 @@ void PlaybackBox::PlayX(const ProgramInfo &pginfo,
     Close();
 }
 
+void PlaybackBox::ClearBookmark()
+{
+    ProgramInfo *pginfo = GetCurrentProgram();
+    if (pginfo)
+        pginfo->SaveBookmark(0);
+}
+
+void PlaybackBox::ClearLastPlayPos()
+{
+    ProgramInfo *pginfo = GetCurrentProgram();
+    if (pginfo)
+        pginfo->SaveLastPlayPos(0);
+}
+
 void PlaybackBox::StopSelected(void)
 {
     ProgramInfo *pginfo = GetCurrentProgram();
@@ -2401,7 +2418,7 @@ void PlaybackBox::selected(MythUIButtonListItem *item)
     if (!item)
         return;
 
-    PlayFromBookmarkOrProgStart(item);
+    PlayFromAnyMark(item);
 }
 
 void PlaybackBox::popupClosed(QString which, int result)
@@ -2536,14 +2553,6 @@ bool PlaybackBox::Play(
         QCoreApplication::postEvent(
             this, new MythEvent("PLAY_PLAYLIST"));
     }
-    else
-    {
-        // User may have saved or deleted a bookmark
-        // requiring update of bookmark icon..
-        ProgramInfo *pginfo = m_programInfoCache.GetRecordingInfo(tvrec.GetRecordingID());
-        if (pginfo)
-            UpdateUIListItem(pginfo, true);
-    }
 
     if (m_needUpdate)
         ScheduleUpdateUIList();
@@ -2993,11 +3002,18 @@ MythMenu* PlaybackBox::createPlayFromMenu()
     MythMenu *menu = new MythMenu(title, this, "slotmenu");
 
     if (pginfo->IsBookmarkSet())
+    {
         menu->AddItem(tr("Play from bookmark"), SLOT(PlayFromBookmark()));
+        menu->AddItem(tr("Clear bookmark"), SLOT(ClearBookmark()));
+    }
     menu->AddItem(tr("Play from beginning"), SLOT(PlayFromBeginning()));
     if (pginfo->QueryLastPlayPos())
+    {
         menu->AddItem(tr("Play from last played position"),
                       SLOT(PlayFromLastPlayPos()));
+        menu->AddItem(tr("Clear last played position"),
+                      SLOT(ClearLastPlayPos()));
+    }
 
     return menu;
 }
@@ -3237,7 +3253,7 @@ void PlaybackBox::ShowActionPopup(const ProgramInfo &pginfo)
             m_popupMenu->AddItem(tr("Play from..."), NULL, createPlayFromMenu());
         else
             m_popupMenu->AddItem(tr("Play"),
-                                 SLOT(PlayFromBookmarkOrProgStart()));
+                                 SLOT(PlayFromAnyMark()));
     }
 
     if (!m_player)
@@ -3966,7 +3982,7 @@ bool PlaybackBox::keyPressEvent(QKeyEvent *event)
             if (action == "DELETE")
                 deleteSelected(m_recordingList->GetItemCurrent());
             else if (action == ACTION_PLAYBACK)
-                PlayFromBookmarkOrProgStart();
+                PlayFromAnyMark();
             else if (action == "DETAILS" || action == "INFO")
                 ShowDetails();
             else if (action == "CUSTOMEDIT")
@@ -4022,7 +4038,11 @@ void PlaybackBox::customEvent(QEvent *event)
             {
                 ProgramInfo evinfo(me->ExtraDataList());
                 if (evinfo.HasPathname() || evinfo.GetChanID())
-                    HandleUpdateProgramInfoEvent(evinfo);
+                {
+                    uint32_t flags = m_programInfoCache.Update(evinfo);
+                    if (flags != PIC_NO_ACTION)
+                        HandleUpdateItemEvent(evinfo.GetRecordingID(), flags);
+                }
             }
             else if (recordingID && (tokens[1] == "ADD"))
             {
@@ -4073,18 +4093,18 @@ void PlaybackBox::customEvent(QEvent *event)
         else if (message.startsWith("UPDATE_FILE_SIZE"))
         {
             QStringList tokens = message.simplified().split(" ");
-            bool ok = false;
-            uint recordingID = 0;
-            uint64_t filesize = 0ULL;
             if (tokens.size() >= 3)
             {
-                recordingID = tokens[1].toUInt();
-                filesize   = tokens[2].toLongLong(&ok);
-            }
-            if (recordingID && ok)
-            {
-
-                HandleUpdateProgramInfoFileSizeEvent(recordingID, filesize);
+                bool ok = false;
+                uint recordingID  = tokens[1].toUInt();
+                uint64_t filesize = tokens[2].toLongLong(&ok);
+                if (ok)
+                {
+                    // Delegate to background thread
+                    MConcurrent::run("UpdateFileSize", &m_programInfoCache,
+                                     &ProgramInfoCache::UpdateFileSize,
+                                     recordingID, filesize, PIC_NONE);
+                }
             }
         }
         else if (message == "UPDATE_UI_LIST")
@@ -4097,6 +4117,18 @@ void PlaybackBox::customEvent(QEvent *event)
                 m_helper.ForceFreeSpaceUpdate();
             }
         }
+        else if (message.startsWith("UPDATE_UI_ITEM"))
+        {
+            QStringList tokens = message.simplified().split(" ");
+            if (tokens.size() < 3)
+                return;
+
+            uint recordingID  = tokens[1].toUInt();
+            UpdateState flags = static_cast<UpdateState>(tokens[2].toUInt());
+
+            if (flags != PIC_NO_ACTION)
+                HandleUpdateItemEvent(recordingID, flags);
+        }
         else if (message == "UPDATE_USAGE_UI")
         {
             UpdateUsageUI();
@@ -4415,35 +4447,22 @@ void PlaybackBox::HandleRecordingAddEvent(const ProgramInfo &evinfo)
     ScheduleUpdateUIList();
 }
 
-void PlaybackBox::HandleUpdateProgramInfoEvent(const ProgramInfo &evinfo)
+void PlaybackBox::HandleUpdateItemEvent(uint recordingId, uint flags)
 {
-    QString old_recgroup = m_programInfoCache.GetRecGroup(
-        evinfo.GetRecordingID());
-
-    if (!m_programInfoCache.Update(evinfo))
-        return;
-
-    // If the recording group has changed, reload lists from the recently
-    // updated cache; if not, only update UI for the updated item
-    if (evinfo.GetRecordingGroup() == old_recgroup)
+    // Changing recording group full reload
+    if (flags & PIC_RECGROUP_CHANGED)
     {
-        ProgramInfo *dst = FindProgramInUILists(evinfo);
-        if (dst)
-            UpdateUIListItem(dst, true);
-        return;
+        ScheduleUpdateUIList();
+    }
+    else
+    {
+        ProgramInfo *pginfo = FindProgramInUILists(recordingId);
+        if (pginfo)
+        {
+            bool genPreview = (flags & PIC_MARK_CHANGED);
+            UpdateUIListItem(pginfo, genPreview);
+        }
     }
-
-    ScheduleUpdateUIList();
-}
-
-void PlaybackBox::HandleUpdateProgramInfoFileSizeEvent(uint recordingID,
-                                                       uint64_t filesize)
-{
-    m_programInfoCache.UpdateFileSize(recordingID, filesize);
-
-    ProgramInfo *dst = FindProgramInUILists(recordingID);
-    if (dst)
-        UpdateUIListItem(dst, false);
 }
 
 void PlaybackBox::ScheduleUpdateUIList(void)
diff --git a/mythtv/programs/mythfrontend/playbackbox.h b/mythtv/programs/mythfrontend/playbackbox.h
index 6b7e144..280a516 100644
--- a/mythtv/programs/mythfrontend/playbackbox.h
+++ b/mythtv/programs/mythfrontend/playbackbox.h
@@ -141,12 +141,13 @@ class PlaybackBox : public ScheduleCommon
     void ItemVisible(MythUIButtonListItem *item);
     void ItemLoaded(MythUIButtonListItem *item);
     void selected(MythUIButtonListItem *item);
-    void PlayFromBookmarkOrProgStart(MythUIButtonListItem *item = NULL);
+    void PlayFromAnyMark(MythUIButtonListItem *item = NULL);
     void PlayFromBookmark(MythUIButtonListItem *item = NULL);
     void PlayFromBeginning(MythUIButtonListItem *item = NULL);
     void PlayFromLastPlayPos(MythUIButtonListItem *item = NULL);
     void deleteSelected(MythUIButtonListItem *item);
-
+    void ClearBookmark();
+    void ClearLastPlayPos();
     void SwitchList(void);
 
     void ShowGroupPopup(void);
@@ -321,8 +322,7 @@ class PlaybackBox : public ScheduleCommon
     void HandlePreviewEvent(const QStringList &list);
     void HandleRecordingRemoveEvent(uint recordingID);
     void HandleRecordingAddEvent(const ProgramInfo &evinfo);
-    void HandleUpdateProgramInfoEvent(const ProgramInfo &evinfo);
-    void HandleUpdateProgramInfoFileSizeEvent(uint recordingID, uint64_t filesize);
+    void HandleUpdateItemEvent(uint recordingId, uint flags);
 
     void ScheduleUpdateUIList(void);
     void ShowMenu(void);
diff --git a/mythtv/programs/mythfrontend/programinfocache.cpp b/mythtv/programs/mythfrontend/programinfocache.cpp
index 920190f..5a8c59f 100644
--- a/mythtv/programs/mythfrontend/programinfocache.cpp
+++ b/mythtv/programs/mythfrontend/programinfocache.cpp
@@ -10,6 +10,8 @@
 #include "programinfo.h"
 #include "remoteutil.h"
 #include "mythevent.h"
+#include "mythdb.h"
+#include "mconcurrent.h"
 
 #include <QCoreApplication>
 #include <QRunnable>
@@ -73,6 +75,35 @@ void ProgramInfoCache::ScheduleLoad(const bool updateUI)
     }
 }
 
+void ProgramInfoCache::CalculateProgress(ProgramInfo &pg, int pos)
+{
+    uint lastPlayPercent = 0;
+    if (pos > 0)
+    {
+        int total = 0;
+
+        switch (pg.GetRecordingStatus())
+        {
+        case RecStatus::Recorded:
+            total = pg.QueryTotalFrames();
+            break;
+        case RecStatus::Recording:
+            // Active recordings won't have total frames set yet.
+            total = pg.QueryLastFrameInPosMap();
+            break;
+        default:
+            break;
+        }
+
+        lastPlayPercent = (total > pos) ? (100 * pos) / total : 0;
+
+        LOG(VB_GUI, LOG_DEBUG, QString("%1 %2  %3/%4 = %5%")
+            .arg(pg.GetRecordingID()).arg(pg.GetTitle())
+            .arg(pos).arg(total).arg(lastPlayPercent));
+    }
+    pg.SetProgressPercent(lastPlayPercent);
+}
+
 void ProgramInfoCache::Load(const bool updateUI)
 {
     QMutexLocker locker(&m_lock);
@@ -84,6 +115,45 @@ void ProgramInfoCache::Load(const bool updateUI)
     // we sort the list later anyway.
     vector<ProgramInfo*> *tmp = RemoteGetRecordedList(0);
     /**/
+
+    // Calculate play positions for UI
+    if (tmp)
+    {
+        // Played progress
+        typedef QPair<uint, QDateTime> ProgId;
+        QHash<ProgId, uint> lastPlayFrames;
+
+        // Get all lastplaypos marks in a single lookup
+        MSqlQuery query(MSqlQuery::InitCon());
+        query.prepare("SELECT chanid, starttime, mark "
+                      "FROM recordedmarkup "
+                      "WHERE type = :TYPE ");
+        query.bindValue(":TYPE", MARK_UTIL_LASTPLAYPOS);
+
+        if (query.exec())
+        {
+            while (query.next())
+            {
+                ProgId id = qMakePair(query.value(0).toUInt(),
+                                      MythDate::as_utc(query.value(1).toDateTime()));
+                lastPlayFrames[id] = query.value(2).toUInt();
+            }
+
+            // Determine progress of each prog
+            foreach (ProgramInfo* pg, *tmp)
+            {
+                // Enable last play pos for all recordings
+                pg->SetAllowLastPlayPos(true);
+
+                ProgId id = qMakePair(pg->GetChanID(),
+                                      pg->GetRecordingStartTime());
+                CalculateProgress(*pg, lastPlayFrames.value(id));
+            }
+        }
+        else
+            MythDB::DBError("Watched progress", query);
+    }
+
     locker.relock();
 
     free_vec(m_next_cache);
@@ -157,54 +227,80 @@ void ProgramInfoCache::Refresh(void)
 
 /** \brief Updates a ProgramInfo in the cache.
  *  \note This must only be called from the UI thread.
- *  \return True iff the ProgramInfo was in the cache and was updated.
+ *  \return Flags indicating the result of the update
  */
-bool ProgramInfoCache::Update(const ProgramInfo &pginfo)
+uint32_t ProgramInfoCache::Update(const ProgramInfo& pginfo)
 {
     QMutexLocker locker(&m_lock);
 
-    Cache::iterator it = m_cache.find(pginfo.GetRecordingID());
+    uint recordingId = pginfo.GetRecordingID();
+    Cache::iterator it = m_cache.find(recordingId);
 
-    if (it != m_cache.end())
-        (*it)->clone(pginfo, true);
+    if (it == m_cache.end())
+        return PIC_NO_ACTION;
 
-    return it != m_cache.end();
-}
+    ProgramInfo& pg = **it;
+    uint32_t flags = PIC_NONE;
 
-/** \brief Updates a ProgramInfo in the cache.
- *  \note This must only be called from the UI thread.
- *  \return True iff the ProgramInfo was in the cache and was updated.
- */
-bool ProgramInfoCache::UpdateFileSize(uint recordingID, uint64_t filesize)
-{
-    QMutexLocker locker(&m_lock);
+    if (pginfo.GetBookmarkUpdate() != pg.GetBookmarkUpdate())
+        flags |= PIC_MARK_CHANGED;
 
-    Cache::iterator it = m_cache.find(recordingID);
+    if (pginfo.GetRecordingGroup() != pg.GetRecordingGroup())
+        flags |= PIC_RECGROUP_CHANGED;
 
-    if (it != m_cache.end())
+    pg.clone(pginfo, true);
+    pg.SetAllowLastPlayPos(true);
+
+    if (flags & PIC_MARK_CHANGED)
     {
-        (*it)->SetFilesize(filesize);
-        if (filesize)
-            (*it)->SetAvailableStatus(asAvailable, "PIC::UpdateFileSize");
+        // Delegate this update to a background task
+        MConcurrent::run("UpdateProg", this, &ProgramInfoCache::UpdateFileSize,
+                         recordingId, 0, flags);
+        // Ignore this update
+        flags = PIC_NO_ACTION;
     }
 
-    return it != m_cache.end();
+    LOG(VB_GUI, LOG_DEBUG, QString("Pg %1 %2 update state %3")
+        .arg(recordingId).arg(pg.GetTitle()).arg(flags));
+    return flags;
 }
 
-/** \brief Returns the ProgramInfo::recgroup or an empty string if not found.
- *  \note This must only be called from the UI thread.
+/** \brief Updates file size calculations of a ProgramInfo in the cache.
+ *  \note This should only be run by a non-UI thread as it contains multiple
+ *   Db queries
+ *  \return True iff the ProgramInfo was in the cache and was updated.
  */
-QString ProgramInfoCache::GetRecGroup(uint recordingID) const
+void ProgramInfoCache::UpdateFileSize(uint recordingId, uint64_t filesize,
+                                      uint32_t flags)
 {
     QMutexLocker locker(&m_lock);
 
-    Cache::const_iterator it = m_cache.find(recordingID);
+    Cache::iterator it = m_cache.find(recordingId);
+    if (it == m_cache.end())
+        return;
 
-    QString recgroup;
-    if (it != m_cache.end())
-        recgroup = (*it)->GetRecordingGroup();
+    ProgramInfo& pg = **it;
+
+    CalculateProgress(pg, pg.QueryLastPlayPos());
 
-    return recgroup;
+    if (filesize > 0)
+    {
+        // Filesize update
+        pg.SetFilesize(filesize);
+        pg.SetAvailableStatus(asAvailable, "PIC::UpdateFileSize");
+    }
+    else // Info update
+    {
+        // Don't keep regenerating previews of files being played
+        QString byWhom;
+        if (pg.QueryIsInUse(byWhom) && byWhom.contains(QObject::tr("Playing")))
+            flags &= ~PIC_MARK_CHANGED;
+    }
+
+    QString mesg = QString("UPDATE_UI_ITEM %1 %2").arg(recordingId).arg(flags);
+    QCoreApplication::postEvent(m_listener, new MythEvent(mesg));
+
+    LOG(VB_GUI, LOG_DEBUG, mesg);
 }
 
 /** \brief Adds a ProgramInfo to the cache.
@@ -212,10 +308,14 @@ QString ProgramInfoCache::GetRecGroup(uint recordingID) const
  */
 void ProgramInfoCache::Add(const ProgramInfo &pginfo)
 {
-    if (!pginfo.GetRecordingID() || Update(pginfo))
+    if (!pginfo.GetRecordingID() || Update(pginfo) != PIC_NO_ACTION)
         return;
 
-    m_cache[pginfo.GetRecordingID()] = new ProgramInfo(pginfo);
+    QMutexLocker locker(&m_lock);
+
+    ProgramInfo* pg = new ProgramInfo(pginfo);
+    pg->SetAllowLastPlayPos(true);
+    m_cache[pginfo.GetRecordingID()] = pg;
 }
 
 /** \brief Marks a ProgramInfo in the cache for deletion on the next
diff --git a/mythtv/programs/mythfrontend/programinfocache.h b/mythtv/programs/mythfrontend/programinfocache.h
index 3b95a74..4c8d5a4 100644
--- a/mythtv/programs/mythfrontend/programinfocache.h
+++ b/mythtv/programs/mythfrontend/programinfocache.h
@@ -20,6 +20,13 @@ class ProgramInfoLoader;
 class ProgramInfo;
 class QObject;
 
+typedef enum {
+    PIC_NONE              = 0x00,
+    PIC_MARK_CHANGED      = 0x01,
+    PIC_RECGROUP_CHANGED  = 0x02,
+    PIC_NO_ACTION         = 0x80,
+} UpdateState;
+
 class ProgramInfoCache
 {
     friend class ProgramInfoLoader;
@@ -35,9 +42,8 @@ class ProgramInfoCache
     void Refresh(void);
     void Add(const ProgramInfo&);
     bool Remove(uint recordingID);
-    bool Update(const ProgramInfo&);
-    bool UpdateFileSize(uint recordingID, uint64_t filesize);
-    QString GetRecGroup(uint recordingID) const;
+    uint32_t Update(const ProgramInfo& pginfo);
+    void UpdateFileSize(uint recordingID, uint64_t filesize, uint32_t flags);
     void GetOrdered(vector<ProgramInfo*> &list, bool newest_first = false);
     /// \note This must only be called from the UI thread.
     bool empty(void) const { return m_cache.empty(); }
@@ -46,6 +52,8 @@ class ProgramInfoCache
   private:
     void Load(const bool updateUI = true);
     void Clear(void);
+    void CalculateProgress(ProgramInfo &pg, int playPos);
+    void LoadProgressMarks();
 
   private:
     // NOTE: Hash would be faster for lookups and updates, but we need a sorted
diff --git a/mythtv/programs/mythfrontend/viewscheduled.cpp b/mythtv/programs/mythfrontend/viewscheduled.cpp
index 63f7881..81109fe 100644
--- a/mythtv/programs/mythfrontend/viewscheduled.cpp
+++ b/mythtv/programs/mythfrontend/viewscheduled.cpp
@@ -282,13 +282,16 @@ void ViewScheduled::LoadList(bool useExistingData)
     if (!useExistingData)
         LoadFromScheduler(m_recList, m_conflictBool);
 
-    ProgramList::iterator pit = m_recList.begin();
-    QString currentDate;
     m_recgroupList[m_defaultGroup] = ProgramList(false);
     m_recgroupList[m_defaultGroup].setAutoDelete(false);
+
+    ProgramList::iterator pit = m_recList.begin();
     while (pit != m_recList.end())
     {
         ProgramInfo *pginfo = *pit;
+
+        CalcRecordedPercent(*pginfo);
+
         const RecStatus::Type recstatus = pginfo->GetRecordingStatus();
         if ((pginfo->GetRecordingEndTime() >= now ||
              pginfo->GetScheduledEndTime() >= now ||
@@ -392,6 +395,57 @@ void ViewScheduled::ChangeGroup(MythUIButtonListItem* item)
         FillList();
 }
 
+void ViewScheduled::UpdateUIListItem(MythUIButtonListItem* item,
+                                     const ProgramInfo &pginfo)
+{
+    QString state;
+
+    const RecStatus::Type recstatus = pginfo.GetRecordingStatus();
+    if (recstatus == RecStatus::Recording      ||
+        recstatus == RecStatus::Tuning)
+        state = "running";
+    else if (recstatus == RecStatus::Conflict  ||
+             recstatus == RecStatus::Offline   ||
+             recstatus == RecStatus::TunerBusy ||
+             recstatus == RecStatus::Failed    ||
+             recstatus == RecStatus::Failing   ||
+             recstatus == RecStatus::Aborted   ||
+             recstatus == RecStatus::Missed)
+        state = "error";
+    else if (recstatus == RecStatus::WillRecord)
+    {
+        if (m_curinput == 0 || pginfo.GetInputID() == m_curinput)
+        {
+            if (pginfo.GetRecordingPriority2() < 0)
+                state = "warning";
+            else
+                state = "normal";
+        }
+    }
+    else if (recstatus == RecStatus::Repeat ||
+             recstatus == RecStatus::NeverRecord ||
+             recstatus == RecStatus::DontRecord ||
+             (recstatus != RecStatus::DontRecord &&
+              recstatus <= RecStatus::EarlierShowing))
+        state = "disabled";
+    else
+        state = "warning";
+
+    InfoMap infoMap;
+    pginfo.ToMap(infoMap);
+    
+    infoMap["progresspercent"] = ProgressString(pginfo);
+
+    item->SetTextFromMap(infoMap, state);
+
+    if (!infoMap["progresspercent"].isEmpty())
+        item->SetProgress(0, 100, pginfo.GetProgressPercent());
+    
+    QString rating = QString::number(pginfo.GetStars(10));
+    item->DisplayState(rating, "ratingstate");
+    item->DisplayState(state, "status");    
+}
+
 void ViewScheduled::FillList()
 {
     m_schedulesList->Reset();
@@ -415,58 +469,14 @@ void ViewScheduled::FillList()
     ProgramList::iterator pit = plist.begin();
     while (pit != plist.end())
     {
-        ProgramInfo *pginfo = *pit;
-        if (!pginfo)
+        ProgramInfo* pginfo = *pit;
+        if (pginfo)
         {
-            ++pit;
-            continue;
+            MythUIButtonListItem *item =
+                    new MythUIButtonListItem(m_schedulesList,"",
+                                             qVariantFromValue(pginfo));
+            UpdateUIListItem(item, *pginfo);
         }
-
-        QString state;
-
-        const RecStatus::Type recstatus = pginfo->GetRecordingStatus();
-        if (recstatus == RecStatus::Recording      ||
-            recstatus == RecStatus::Tuning)
-            state = "running";
-        else if (recstatus == RecStatus::Conflict  ||
-                 recstatus == RecStatus::Offline   ||
-                 recstatus == RecStatus::TunerBusy ||
-                 recstatus == RecStatus::Failed    ||
-                 recstatus == RecStatus::Failing   ||
-                 recstatus == RecStatus::Aborted   ||
-                 recstatus == RecStatus::Missed)
-            state = "error";
-        else if (recstatus == RecStatus::WillRecord)
-        {
-            if (m_curinput == 0 || pginfo->GetInputID() == m_curinput)
-            {
-                if (pginfo->GetRecordingPriority2() < 0)
-                    state = "warning";
-                else
-                    state = "normal";
-            }
-        }
-        else if (recstatus == RecStatus::Repeat ||
-                 recstatus == RecStatus::NeverRecord ||
-                 recstatus == RecStatus::DontRecord ||
-                 (recstatus != RecStatus::DontRecord &&
-                  recstatus <= RecStatus::EarlierShowing))
-            state = "disabled";
-        else
-            state = "warning";
-
-        MythUIButtonListItem *item =
-                                new MythUIButtonListItem(m_schedulesList,"",
-                                                    qVariantFromValue(pginfo));
-
-        InfoMap infoMap;
-        pginfo->ToMap(infoMap);
-        item->SetTextFromMap(infoMap, state);
-
-        QString rating = QString::number(pginfo->GetStars(10));
-        item->DisplayState(rating, "ratingstate");
-        item->DisplayState(state, "status");
-
         ++pit;
     }
 
@@ -510,6 +520,13 @@ void ViewScheduled::FillList()
     }
 }
 
+QString ViewScheduled::ProgressString(const ProgramInfo &pg)
+{
+    // Only show progress value for active recordings
+    return pg.GetRecordingStatus() == RecStatus::Recording
+            ? QString::number(pg.GetProgressPercent()) : "";
+}
+
 void ViewScheduled::updateInfo(MythUIButtonListItem *item)
 {
     if (!item)
@@ -520,6 +537,9 @@ void ViewScheduled::updateInfo(MythUIButtonListItem *item)
     {
         InfoMap infoMap;
         pginfo->ToMap(infoMap);
+
+        infoMap["progresspercent"] = ProgressString(*pginfo);
+
         SetTextFromMap(infoMap);
 
         MythUIStateType *ratingState = dynamic_cast<MythUIStateType*>
@@ -600,19 +620,62 @@ void ViewScheduled::customEvent(QEvent *event)
         MythEvent *me = (MythEvent *)event;
         QString message = me->Message();
 
-        if (message != "SCHEDULE_CHANGE")
-            return;
+        if (message == "SCHEDULE_CHANGE")
+        {
+            m_needFill = true;
 
-        m_needFill = true;
+            if (m_inEvent)
+                return;
 
-        if (m_inEvent)
-            return;
+            m_inEvent = true;
 
-        m_inEvent = true;
+            LoadList();
 
-        LoadList();
+            m_inEvent = false;
+        }
+        else if (message.startsWith("UPDATE_FILE_SIZE"))
+        {
+            QStringList tokens = message.simplified().split(" ");
+            if (tokens.size() < 3)
+                return;
 
-        m_inEvent = false;
+            bool ok;
+            uint recordingID  = tokens[1].toUInt();
+            uint64_t filesize = tokens[2].toLongLong(&ok);
+
+            // Locate program
+            ProgramList::iterator pit = m_recList.begin();
+            while (pit != m_recList.end())
+            {
+                ProgramInfo* pginfo = *pit;
+                if (pginfo && pginfo->GetRecordingID() == recordingID)
+                {
+                    // Update size & progress
+                    pginfo->SetFilesize(filesize);
+                    uint current = pginfo->GetProgressPercent();
+                    CalcRecordedPercent(*pginfo);
+                    if (pginfo->GetProgressPercent() != current)
+                    {
+                        // Update display, if it's shown
+                        MythUIButtonListItem *item =
+                                m_schedulesList->
+                                GetItemByData(qVariantFromValue(pginfo));
+                        if (item)
+                        {
+                            UpdateUIListItem(item, *pginfo);
+
+                            // Update selected item if necessary
+                            MythUIButtonListItem *selected =
+                                    m_schedulesList->GetItemCurrent();
+                            if (item == selected)
+                                updateInfo(selected);
+                        }
+                    }
+                    break;
+                }
+                ++pit;
+            }
+        }
     }
     else if (event->type() == DialogCompletionEvent::kEventType)
     {
@@ -694,6 +757,23 @@ void ViewScheduled::customEvent(QEvent *event)
     }
 }
 
+void ViewScheduled::CalcRecordedPercent(ProgramInfo &pg)
+{
+    QDateTime start = pg.GetRecordingStartTime();
+    int current = start.secsTo(MythDate::current());
+    uint recordedPercent = 0;
+    int duration = 0;
+    if (current > 0)
+    {
+        // Recording stops at end of the final minute
+        duration        = start.secsTo(pg.GetRecordingEndTime()) + 60;
+        recordedPercent = duration > current ? current * 100 / duration : 100;
+    }
+    pg.SetProgressPercent(recordedPercent);
+    LOG(VB_GUI, LOG_DEBUG, QString("%1  %2/%3 = %4%")
+        .arg(pg.GetTitle()).arg(current).arg(duration).arg(recordedPercent));
+}
+
 ProgramInfo *ViewScheduled::GetCurrentProgram(void) const
 {
     MythUIButtonListItem *item = m_schedulesList->GetItemCurrent();
diff --git a/mythtv/programs/mythfrontend/viewscheduled.h b/mythtv/programs/mythfrontend/viewscheduled.h
index 5d2e6a2..ce2fa6c 100644
--- a/mythtv/programs/mythfrontend/viewscheduled.h
+++ b/mythtv/programs/mythfrontend/viewscheduled.h
@@ -59,6 +59,11 @@ class ViewScheduled : public ScheduleCommon
 
     void EmbedTVWindow(void);
 
+    void CalcRecordedPercent(ProgramInfo &pg);
+    void UpdateUIListItem(MythUIButtonListItem* item,
+                          const ProgramInfo &pginfo);
+    QString ProgressString(const ProgramInfo &pg);
+
     bool m_conflictBool;
     QDate m_conflictDate;
 
diff --git a/mythtv/themes/MythCenter-wide/recordings-ui.xml b/mythtv/themes/MythCenter-wide/recordings-ui.xml
index f5729a6..569e204 100644
--- a/mythtv/themes/MythCenter-wide/recordings-ui.xml
+++ b/mythtv/themes/MythCenter-wide/recordings-ui.xml
@@ -180,6 +180,16 @@
             <statetype name="buttonitem">
                 <state name="active">
                 <area>0,0,880,30</area>
+                <progressbar name="buttonprogress">
+                    <area>0,0,100%,100%</area>
+                    <layout>horizontal</layout>
+                    <style>reveal</style>
+                    <shape name="progressimage">
+                        <area>0,1,100%,100%-1</area>
+                        <type>box</type>
+                        <fill color="#000000" alpha="128"/>
+                    </shape>
+                </progressbar>
                 <statetype name="status">
                     <position>3,2</position>
                     <state name="disabled">
@@ -212,8 +222,9 @@
                     <textarea name="titlesubtitle" from="buttontext">
                         <area>32,2,656,28</area>
                         <align>vcenter</align>
+                        <template>%titlesubtitle%% (|progresspercent|%)%</template>
                     </textarea>
-                    <textarea name="shortstartdate" from="titlesubtitle">
+                    <textarea name="shortstartdate" from="buttontext">
                         <area>634,2,120,28</area>
                         <align>right,vcenter</align>
                     </textarea>
@@ -249,8 +260,7 @@
                     <shape name="selectbar">
                         <area>0,0,100%,30</area>
                     </shape>
-                    <textarea name="titlesubtitle" from="buttontext">
-                        <area>32,2,656,28</area>
+                    <textarea name="fonts" from="buttontext">
                         <font>basesmall_normal_selected</font>
                         <font state="disabled">basesmall_disabled_selected</font>
                         <font state="error">basesmall_error_selected</font>
@@ -259,13 +269,16 @@
                         <font state="running">basesmall_running_selected</font>
                         <align>vcenter</align>
                     </textarea>
-                    <textarea name="shortstartdate" from="titlesubtitle">
+                    <textarea name="titlesubtitle" from="fonts">
+                        <area>32,2,656,28</area>
+                        <template>%titlesubtitle%% (|progresspercent|%)%</template>
+                    </textarea>
+                    <textarea name="shortstartdate" from="fonts">
                         <area>634,2,120,28</area>
                         <align>right,vcenter</align>
                     </textarea>
                     <textarea name="starttime" from="shortstartdate">
                         <area>760,2,114,28</area>
-                        <align>right,vcenter</align>
                     </textarea>
                 </state>
             </statetype>
@@ -382,6 +395,7 @@
             <font>baselarge</font>
             <cutdown>yes</cutdown>
             <align>vcenter</align>
+            <template>%title%% (|progresspercent|%)%</template>
         </textarea>
 
         <textarea name="channel" from="basetextarea">
diff --git a/mythtv/themes/MythCenter-wide/schedule-ui.xml b/mythtv/themes/MythCenter-wide/schedule-ui.xml
index 9b1ff20..a6c00c6 100644
--- a/mythtv/themes/MythCenter-wide/schedule-ui.xml
+++ b/mythtv/themes/MythCenter-wide/schedule-ui.xml
@@ -513,6 +513,16 @@
             <statetype name="buttonitem">
                 <area>0,0,1200,24</area>
                 <state name="active">
+                    <progressbar name="buttonprogress">
+                        <area>10,2,1200,24</area>
+                        <layout>horizontal</layout>
+                        <style>reveal</style>
+                        <shape name="progressimage">
+                            <area>0,0,100%,100%</area>
+                            <type>box</type>
+                            <fill color="#000000" alpha="128"/>
+                        </shape>
+                    </progressbar>
                     <textarea name="shortstarttimedate" from="buttontext">
                         <area>10,2,250,24</area>
                     </textarea>
@@ -521,6 +531,7 @@
                     </textarea>
                     <textarea name="title" from="shortstarttimedate">
                         <area>480,2,655,24</area>
+                        <template>%title%% (|progresspercent|%)%</template>
                     </textarea>
                     <textarea name="card" from="shortstarttimedate">
                         <area>1145,2,40,24</area>
@@ -542,6 +553,7 @@
                     </textarea>
                     <textarea name="title" from="shortstarttimedate">
                         <area>480,2,655,24</area>
+                        <template>%title%% (|progresspercent|%)%</template>
                     </textarea>
                     <textarea name="card" from="shortstarttimedate">
                         <area>1145,2,40,24</area>
@@ -562,10 +574,16 @@
         </buttonlist>
 
         <textarea name="title" from="basetextarea">
-            <area>30,454,1200,50</area>
+            <area>30,454,1000,50</area>
             <font>baselarge</font>
         </textarea>
 
+        <textarea name="progresspercent" from="basetextarea">
+            <area>1180,454,70,50</area>
+            <align>right,vcenter</align>
+            <template>%progresspercent|%%</template>
+        </textarea>
+
         <textarea name="channel" from="basetextarea">
             <area>30,494,360,30</area>
         </textarea>
-- 
2.7.4

