Index: mythplugins/mythweb/modules/tv/classes/Program.php
===================================================================
--- mythplugins/mythweb/modules/tv/classes/Program.php	(revision 26104)
+++ mythplugins/mythweb/modules/tv/classes/Program.php	(working copy)
@@ -456,8 +456,8 @@
  * Generate a new preview pixmap for this recording.
 /**/
     public function generate_pixmap() {
-        $ret = MythBackend::find()->sendCommand(array('QUERY_GENPIXMAP', $this->backend_row()));
-        if ($ret == 'BAD') {
+        $ret = MythBackend::find()->sendCommand(array('QUERY_GENPIXMAP2', "do_not_care", $this->backend_row()));
+        if ($ret == 'ERROR') {
             return 0;
         }
         return 1;
Index: mythplugins/mythweb/classes/MythBackend.php
===================================================================
--- mythplugins/mythweb/classes/MythBackend.php	(revision 26104)
+++ mythplugins/mythweb/classes/MythBackend.php	(working copy)
@@ -15,7 +15,7 @@
 
 // MYTH_PROTO_VERSION is defined in libmyth in mythtv/libs/libmyth/mythcontext.h
 // and should be the current MythTV protocol version.
-    static $protocol_version        = '60';
+    static $protocol_version        = '61';
 
 // The character string used by the backend to separate records
     static $backend_separator       = '[]:[]';
Index: mythtv/libs/libmythtv/jobqueue.cpp
===================================================================
--- mythtv/libs/libmythtv/jobqueue.cpp	(revision 26104)
+++ mythtv/libs/libmythtv/jobqueue.cpp	(working copy)
@@ -2169,7 +2169,7 @@
         if (program_info->IsLocal())
         {
             PreviewGenerator *pg = new PreviewGenerator(
-                program_info, PreviewGenerator::kLocal);
+                program_info, QString(), PreviewGenerator::kLocal);
             pg->Run();
             pg->deleteLater();
         }
Index: mythtv/libs/libmythtv/libmythtv.pro
===================================================================
--- mythtv/libs/libmythtv/libmythtv.pro	(revision 26104)
+++ mythtv/libs/libmythtv/libmythtv.pro	(working copy)
@@ -156,7 +156,8 @@
 HEADERS += scheduledrecording.h
 HEADERS += signalmonitorvalue.h     signalmonitorlistener.h
 HEADERS += livetvchain.h            playgroup.h
-HEADERS += channelsettings.h        previewgenerator.h
+HEADERS += channelsettings.h
+HEADERS += previewgenerator.h       previewgeneratorqueue.h
 HEADERS += transporteditor.h        listingsources.h
 HEADERS += myth_imgconvert.h
 HEADERS += channelgroup.h           channelgroupsettings.h
@@ -179,7 +180,8 @@
 SOURCES += scheduledrecording.cpp
 SOURCES += signalmonitorvalue.cpp
 SOURCES += livetvchain.cpp          playgroup.cpp
-SOURCES += channelsettings.cpp      previewgenerator.cpp
+SOURCES += channelsettings.cpp
+SOURCES += previewgenerator.cpp     previewgeneratorqueue.cpp
 SOURCES += transporteditor.cpp
 SOURCES += channelgroup.cpp         channelgroupsettings.cpp
 SOURCES += myth_imgconvert.cpp
Index: mythtv/libs/libmythtv/tvremoteutil.h
===================================================================
--- mythtv/libs/libmythtv/tvremoteutil.h	(revision 26104)
+++ mythtv/libs/libmythtv/tvremoteutil.h	(working copy)
@@ -45,7 +45,6 @@
 MPUBLIC vector<InputInfo> RemoteRequestFreeInputList(
     uint cardid, const vector<uint> &excluded_cardids);
 MPUBLIC InputInfo RemoteRequestBusyInputID(uint cardid);
-MPUBLIC void RemoteGeneratePreviewPixmap(ProgramInfo *pginfo);
 MPUBLIC bool RemoteIsBusy(uint cardid, TunedInputInfo &busy_input);
 
 MPUBLIC bool RemoteGetRecordingStatus(
Index: mythtv/libs/libmythtv/tvremoteutil.cpp
===================================================================
--- mythtv/libs/libmythtv/tvremoteutil.cpp	(revision 26104)
+++ mythtv/libs/libmythtv/tvremoteutil.cpp	(working copy)
@@ -264,14 +264,6 @@
     return blank;
 }
 
-void RemoteGeneratePreviewPixmap(ProgramInfo *pginfo)
-{
-    QStringList strlist( "QUERY_GENPIXMAP" );
-    pginfo->ToStringList(strlist);
-
-    gCoreContext->SendReceiveStringList(strlist);
-}
-
 bool RemoteIsBusy(uint cardid, TunedInputInfo &busy_input)
 {
     //VERBOSE(VB_IMPORTANT, QString("RemoteIsBusy(%1) %2")
Index: mythtv/libs/libmythtv/previewgeneratorqueue.h
===================================================================
--- mythtv/libs/libmythtv/previewgeneratorqueue.h	(revision 26104)
+++ mythtv/libs/libmythtv/previewgeneratorqueue.h	(working copy)
@@ -0,0 +1,96 @@
+// -*- Mode: c++ -*-
+#ifndef _PREVIEW_GENERATOR_QUEUE_H_
+#define _PREVIEW_GENERATOR_QUEUE_H_
+
+#include <QStringList>
+#include <QDateTime>
+#include <QThread>
+#include <QMutex>
+#include <QMap>
+#include <QSet>
+
+#include "previewgenerator.h"
+#include "mythexp.h"
+
+class ProgramInfo;
+class QSize;
+
+class PreviewGenState
+{
+  public:
+    PreviewGenState() :
+        gen(NULL), genStarted(false),
+        attempts(0), lastBlockTime(0) {}
+    PreviewGenerator *gen;
+    bool              genStarted;
+    uint              attempts;
+    uint              lastBlockTime;
+    QDateTime         blockRetryUntil;
+    QSet<QString>     tokens;
+};
+typedef QMap<QString,PreviewGenState> PreviewMap;
+
+class MPUBLIC PreviewGeneratorQueue : public QThread
+{
+    Q_OBJECT
+
+  public:
+    static void CreatePreviewGeneratorQueue(
+        PreviewGenerator::Mode mode,
+        uint maxAttempts, uint minBlockSeconds);
+    static void TeardownPreviewGeneratorQueue();
+
+    static void GetPreviewImage(const ProgramInfo &pginfo, QString token)
+    {
+        GetPreviewImage(pginfo, QSize(0,0), "", -1, true, token);
+    }
+    static void GetPreviewImage(const ProgramInfo&, const QSize&,
+                                const QString &outputfile,
+                                long long time, bool in_seconds,
+                                QString token);
+    static void AddListener(QObject*);
+    static void RemoveListener(QObject*);
+
+  private:
+    PreviewGeneratorQueue(PreviewGenerator::Mode mode,
+                          uint maxAttempts, uint minBlockSeconds);
+    ~PreviewGeneratorQueue();
+
+    QString GeneratePreviewImage(ProgramInfo &pginfo, const QSize&,
+                                 const QString &outputfile,
+                                 long long time, bool in_seconds,
+                                 QString token);
+
+    void GetInfo(const QString &key, uint &queue_depth, uint &preview_tokens);
+    void SetPreviewGenerator(const QString &key, PreviewGenerator *g);
+    void IncPreviewGeneratorPriority(const QString &key, QString token);
+    void UpdatePreviewGeneratorThreads(void);
+    bool IsGeneratingPreview(const QString &key) const;
+    uint IncPreviewGeneratorAttempts(const QString &key);
+    void ClearPreviewGeneratorAttempts(const QString &key);
+
+    virtual bool event(QEvent *e); // QObject
+
+    void SendEvent(const ProgramInfo &pginfo,
+                   const QString     &eventname,
+                   const QString     &fn,
+                   const QString     &token,
+                   const QString     &msg,
+                   const QDateTime   &dt);
+
+  private:
+    static PreviewGeneratorQueue *s_pgq;
+    QSet<QObject*> m_listeners;
+
+    mutable QMutex         m_lock;
+    PreviewGenerator::Mode m_mode;
+    PreviewMap             m_previewMap;
+    QMap<QString,QString>  m_tokenToKeyMap;
+    QStringList            m_queue;
+    uint                   m_running;
+    uint                   m_maxThreads;
+    uint                   m_maxAttempts;
+    uint                   m_minBlockSeconds;
+};
+
+#endif // _PREVIEW_GENERATOR_QUEUE_H_
Index: mythtv/libs/libmythtv/previewgenerator.cpp
===================================================================
--- mythtv/libs/libmythtv/previewgenerator.cpp	(revision 26104)
+++ mythtv/libs/libmythtv/previewgenerator.cpp	(working copy)
@@ -2,15 +2,20 @@
 #include <cmath>
 
 // POSIX headers
+#include <sys/types.h> // for utime
 #include <sys/time.h>
 #include <fcntl.h>
+#include <utime.h>     // for utime
 
 // Qt headers
+#include <QCoreApplication>
+#include <QTemporaryFile>
 #include <QFileInfo>
+#include <QMetaType>
+#include <QThread>
 #include <QImage>
-#include <QMetaType>
+#include <QDir>
 #include <QUrl>
-#include <QDir>
 
 // MythTV headers
 #include "mythconfig.h"
@@ -27,6 +32,7 @@
 #include "playercontext.h"
 #include "mythdirs.h"
 #include "mythverbose.h"
+#include "remoteutil.h"
 
 #define LOC QString("Preview: ")
 #define LOC_ERR QString("Preview Error: ")
@@ -37,17 +43,14 @@
  *
  *   The usage is simple: First, pass a ProgramInfo whose pathname points
  *   to a local or remote recording to the constructor. Then call either
- *   Start(void) or Run(void) to generate the preview.
+ *   start(void) or Run(void) to generate the preview.
  *
- *   Start(void) will create a thread that processes the request,
- *   creating a sockets the the backend if the recording is not local.
+ *   start(void) will create a thread that processes the request.
  *
- *   Run(void) will process the request in the current thread, and it
- *   uses the MythContext's server and event sockets if the recording
- *   is not local.
+ *   Run(void) will block until the preview completes.
  *
- *   The PreviewGenerator will send Qt signals when the preview is ready
- *   and when the preview thread finishes running if Start(void) was called.
+ *   The PreviewGenerator will send a PREVIEW_SUCCESS or a
+ *   PREVIEW_FAILED event when the preview completes or fails.
  */
 
 /**
@@ -64,11 +67,12 @@
  *                    if the file is local.
  */
 PreviewGenerator::PreviewGenerator(const ProgramInfo *pginfo,
+                                   const QString     &_token,
                                    PreviewGenerator::Mode _mode)
-    : programInfo(*pginfo), mode(_mode), isConnected(false),
-      createSockets(false), serverSock(NULL), pathname(pginfo->GetPathname()),
+    : programInfo(*pginfo), mode(_mode), listener(NULL),
+      pathname(pginfo->GetPathname()),
       timeInSeconds(true),  captureTime(-1),  outFileName(QString::null),
-      outSize(0,0)
+      outSize(0,0), token(_token), gotReply(false), pixmapOk(false)
 {
 }
 
@@ -84,25 +88,8 @@
 
 void PreviewGenerator::TeardownAll(void)
 {
-    if (!isConnected)
-        return;
-
-    const QString filename = programInfo.GetPathname() + ".png";
-
-    MythTimer t;
-    t.start();
-    for (bool done = false; !done;)
-    {
-        previewLock.lock();
-        if (isConnected)
-            emit previewThreadDone(filename, done);
-        else
-            done = true;
-        previewLock.unlock();
-        usleep(5000);
-    }
-    VERBOSE(VB_PLAYBACK, LOC + "previewThreadDone took "<<t.elapsed()<<"ms");
-    disconnectSafe();
+    previewWaitCondition.wakeAll();
+    listener = NULL;
 }
 
 void PreviewGenerator::deleteLater()
@@ -114,51 +101,43 @@
 void PreviewGenerator::AttachSignals(QObject *obj)
 {
     QMutexLocker locker(&previewLock);
-    qRegisterMetaType<bool>("bool &");
-    connect(this, SIGNAL(previewThreadDone(const QString&,bool&)),
-            obj,  SLOT(  previewThreadDone(const QString&,bool&)),
-            Qt::DirectConnection);
-    connect(this, SIGNAL(previewReady(const ProgramInfo*)),
-            obj,  SLOT(  previewReady(const ProgramInfo*)),
-            Qt::DirectConnection);
-    isConnected = true;
+    listener = obj;
 }
 
-/** \fn PreviewGenerator::disconnectSafe(void)
- *  \brief disconnects signals while holding previewLock, ensuring that
- *         no one will receive a signal from this class after this call.
- */
-void PreviewGenerator::disconnectSafe(void)
-{
-    QMutexLocker locker(&previewLock);
-    QObject::disconnect(this, NULL, NULL, NULL);
-    isConnected = false;
-}
-
-/** \fn PreviewGenerator::Start(void)
- *  \brief This call starts a thread that will create a preview.
- */
-void PreviewGenerator::Start(void)
-{
-    pthread_create(&previewThread, NULL, PreviewRun, this);
-    // detach, so we don't have to join thread to free thread local mem.
-    pthread_detach(previewThread);
-}
-
 /** \fn PreviewGenerator::RunReal(void)
  *  \brief This call creates a preview without starting a new thread.
  */
 bool PreviewGenerator::RunReal(void)
 {
+    QString msg;
+    QTime tm = QTime::currentTime();
     bool ok = false;
     bool is_local = IsLocal();
-    if (is_local && (mode && kLocal) && LocalPreviewRun())
+
+    if (!is_local && !!(mode & kRemote))
     {
+        VERBOSE(VB_IMPORTANT, LOC_ERR +
+                QString("Run() file not local: '%1'")
+                .arg(pathname));
+    }
+    else if (!(mode & kLocal) && !(mode & kRemote))
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR +
+                QString("Run() Preview of '%1' failed "
+                        "because mode was invalid 0x%2")
+                .arg(pathname).arg((int)mode,0,16));
+    }
+    else if (is_local && !!(mode & kLocal) && LocalPreviewRun())
+    {
         ok = true;
+        msg = QString("Generated on %1 in %2 seconds, starting at %3")
+            .arg(gCoreContext->GetHostName())
+            .arg(tm.elapsed()*0.001)
+            .arg(tm.toString(Qt::ISODate));
     }
-    else if (mode & kRemote)
+    else if (!!(mode & kRemote))
     {
-        if (is_local)
+        if (is_local && (mode & kLocal))
         {
             VERBOSE(VB_IMPORTANT, LOC_WARN + "Failed to save preview."
                     "\n\t\t\tYou may need to check user and group ownership on"
@@ -166,33 +145,77 @@
                     "\n\t\t\tAttempting to regenerate preview on backend.\n");
         }
         ok = RemotePreviewRun();
+        if (ok)
+        {
+            msg = QString("Generated remotely in %1 seconds, starting at %2")
+                .arg(tm.elapsed()*0.001)
+                .arg(tm.toString(Qt::ISODate));
+        }
+        else
+        {
+            msg = "Remote preview failed";
+        }
     }
     else
     {
-        VERBOSE(VB_IMPORTANT, LOC_ERR + QString("Run() file not local: '%1'")
-                .arg(pathname));
+        msg = "Could not access recording";
     }
 
+    QMutexLocker locker(&previewLock);
+    if (listener)
+    {
+        QString output_fn = outFileName.isEmpty() ?
+            (programInfo.GetPathname()+".png") : outFileName;
+
+        QDateTime dt;
+        if (ok)
+        {
+            QFileInfo fi(output_fn);
+            if (fi.exists())
+                dt = fi.lastModified();
+        }
+
+        QString message = (ok) ? "PREVIEW_SUCCESS" : "PREVIEW_FAILED";
+        QStringList list;
+        list.push_back(programInfo.MakeUniqueKey());
+        list.push_back(output_fn);
+        list.push_back(msg);
+        list.push_back(dt.isValid()?dt.toString(Qt::ISODate):"");
+        list.push_back(token);
+        QCoreApplication::postEvent(listener, new MythEvent(message, list));
+    }
+
     return ok;
 }
 
 bool PreviewGenerator::Run(void)
 {
+    QString msg;
+    QDateTime dtm = QDateTime::currentDateTime();
+    QTime tm = QTime::currentTime();
     bool ok = false;
     QString command = GetInstallPrefix() + "/bin/mythpreviewgen";
-    bool local_ok = (IsLocal() && (mode & kLocal) &&
+    bool local_ok = (IsLocal() && !!(mode & kLocal) &&
                      QFileInfo(command).isExecutable());
     if (!local_ok)
     {
-        if (mode & kRemote)
+        if (!!(mode & kRemote))
         {
             ok = RemotePreviewRun();
+            if (ok)
+            {
+                msg =
+                    QString("Generated remotely in %1 seconds, starting at %2")
+                    .arg(tm.elapsed()*0.001)
+                    .arg(tm.toString(Qt::ISODate));
+            }
         }
         else
         {
             VERBOSE(VB_IMPORTANT, LOC_ERR +
                     QString("Run() can not generate preview locally for: '%1'")
                     .arg(pathname));
+            msg = "Failed, local preview requested for remote file.";
         }
     }
     else
@@ -215,13 +238,15 @@
         if (!outFileName.isEmpty())
             command += QString("--outfile \"%1\" ").arg(outFileName);
 
+        command += " > /dev/null";
+
         int ret = myth_system(command, MYTH_SYSTEM_DONT_BLOCK_LIRC |
                                        MYTH_SYSTEM_DONT_BLOCK_JOYSTICK_MENU |
                                        MYTH_SYSTEM_DONT_BLOCK_PARENT);
         if (ret)
         {
-            VERBOSE(VB_IMPORTANT, LOC_ERR + "Encountered problems running " +
-                    QString("'%1'").arg(command));
+            msg = QString("Encountered problems running '%1'").arg(command);
+            VERBOSE(VB_IMPORTANT, LOC_ERR + msg);
         }
         else
         {
@@ -240,7 +265,13 @@
             QFileInfo fi(outname);
             ok = (fi.exists() && fi.isReadable() && fi.size());
             if (ok)
+            {
                 VERBOSE(VB_PLAYBACK, LOC + "Preview process ran ok.");
+                msg = QString("Generated on %1 in %2 seconds, starting at %3")
+                    .arg(gCoreContext->GetHostName())
+                    .arg(tm.elapsed()*0.001)
+                    .arg(tm.toString(Qt::ISODate));
+            }
             else
             {
                 VERBOSE(VB_IMPORTANT, LOC_ERR + "Preview process not ok." +
@@ -251,45 +282,60 @@
                 VERBOSE(VB_IMPORTANT, LOC_ERR +
                         QString("Despite command '%1' returning success")
                         .arg(command));
+                msg = QString("Failed to read preview image despite "
+                              "preview process returning success.");
             }
         }
     }
 
+    QMutexLocker locker(&previewLock);
+
+    // Backdate file to start of preview time in case a bookmark was made
+    // while we were generating the preview.
+    QString output_fn = outFileName.isEmpty() ?
+        (programInfo.GetPathname()+".png") : outFileName;
+
+    QDateTime dt;
     if (ok)
     {
-        QMutexLocker locker(&previewLock);
-        emit previewReady(&programInfo);
+        QFileInfo fi(output_fn);
+        if (fi.exists())
+            dt = fi.lastModified();
     }
 
+    QString message = (ok) ? "PREVIEW_SUCCESS" : "PREVIEW_FAILED";
+    if (listener)
+    {
+        QStringList list;
+        list.push_back(programInfo.MakeUniqueKey());
+        list.push_back(outFileName.isEmpty() ?
+                       (programInfo.GetPathname()+".png") : outFileName);
+        list.push_back(msg);
+        list.push_back(dt.isValid()?dt.toString(Qt::ISODate):"");
+        list.push_back(token);
+        QCoreApplication::postEvent(listener, new MythEvent(message, list));
+    }
+
     return ok;
 }
 
-void *PreviewGenerator::PreviewRun(void *param)
+void PreviewGenerator::run(void)
 {
-    // Lower scheduling priority, to avoid problems with recordings.
-    if (setpriority(PRIO_PROCESS, 0, 9))
-        VERBOSE(VB_IMPORTANT, LOC + "Setting priority failed." + ENO);
-    PreviewGenerator *gen = (PreviewGenerator*) param;
-    gen->createSockets = true;
-    gen->Run();
-    gen->deleteLater();
-    return NULL;
+    setPriority(QThread::LowPriority);
+    Run();
+    connect(this, SIGNAL(finished()),
+            this, SLOT(deleteLater()));
 }
 
-bool PreviewGenerator::RemotePreviewSetup(void)
-{
-    QString server = gCoreContext->GetSetting("MasterServerIP", "localhost");
-    int     port   = gCoreContext->GetNumSetting("MasterServerPort", 6543);
-    QString ann    = QString("ANN Monitor %2 %3")
-        .arg(gCoreContext->GetHostName()).arg(false);
-
-    serverSock = gCoreContext->ConnectCommandSocket(server, port, ann);
-    return serverSock;
-}
-
 bool PreviewGenerator::RemotePreviewRun(void)
 {
-    QStringList strlist( "QUERY_GENPIXMAP" );
+    QStringList strlist( "QUERY_GENPIXMAP2" );
+    if (token.isEmpty())
+    {
+        token = QString("%1:%2")
+            .arg(programInfo.MakeUniqueKey()).arg(rand());
+    }
+    strlist.push_back(token);
     programInfo.ToStringList(strlist);
     strlist.push_back(timeInSeconds ? "s" : "f");
     encodeLongLong(strlist, captureTime);
@@ -305,29 +351,10 @@
     strlist.push_back(QString::number(outSize.width()));
     strlist.push_back(QString::number(outSize.height()));
 
-    bool ok = false;
+    gCoreContext->addListener(this);
+    pixmapOk = false;
 
-    if (createSockets)
-    {
-        if (!RemotePreviewSetup())
-        {
-            VERBOSE(VB_IMPORTANT, LOC_ERR + "Failed to open sockets.");
-            return false;
-        }
-
-        if (serverSock)
-        {
-            serverSock->writeStringList(strlist);
-            ok = serverSock->readStringList(strlist, false);
-        }
-
-        RemotePreviewTeardown();
-    }
-    else
-    {
-        ok = gCoreContext->SendReceiveStringList(strlist);
-    }
-
+    bool ok = gCoreContext->SendReceiveStringList(strlist);
     if (!ok || strlist.empty() || (strlist[0] != "OK"))
     {
         if (!ok)
@@ -340,17 +367,103 @@
             VERBOSE(VB_IMPORTANT, LOC_ERR +
                     "Remote Preview failed, reason given: " <<strlist[1]);
         }
-        else
-        {
-            VERBOSE(VB_IMPORTANT, LOC_ERR +
-                    "Remote Preview failed due to an unknown error.");
-        }
+
+        gCoreContext->removeListener(this);
+
         return false;
     }
 
+    QMutexLocker locker(&previewLock);
+
+    // wait up to 30 seconds for the preview to complete
+    if (!gotReply)
+        previewWaitCondition.wait(&previewLock, 30 * 1000);
+
+    if (!gotReply)
+        VERBOSE(VB_IMPORTANT, LOC + "RemotePreviewRun() -- no reply..");
+
+    gCoreContext->removeListener(this);
+
+    return pixmapOk;
+}
+
+bool PreviewGenerator::event(QEvent *e)
+{
+    if (e->type() != (QEvent::Type) MythEvent::MythEventMessage)
+        return QObject::event(e);
+
+    MythEvent *me = (MythEvent*)e;
+    if (me->Message() != "GENERATED_PIXMAP" || me->ExtraDataCount() < 3)
+        return QObject::event(e);
+            
+    bool ok = me->ExtraData(0) == "OK";
+    bool ours = false;
+    uint i = ok ? 4 : 3;
+    for (; i < (uint) me->ExtraDataCount() && !ours; i++)
+        ours |= me->ExtraData(i) == token;
+    if (!ours)
+        return false;
+
+    QString pginfokey = me->ExtraData(1);
+
+    QMutexLocker locker(&previewLock);
+    gotReply = true;
+    pixmapOk = ok;
+    if (!ok)
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR + pginfokey + ": " + me->ExtraData(2));
+        previewWaitCondition.wakeAll();
+        return true;
+    }
+
+    if (me->ExtraDataCount() < 5)
+    {
+        pixmapOk = false;
+        previewWaitCondition.wakeAll();
+        return true; // could only happen with very broken client...
+    }
+
+    QDateTime datetime = QDateTime::fromString(me->ExtraData(3), Qt::ISODate);
+    if (!datetime.isValid())
+    {
+        pixmapOk = false;
+        VERBOSE(VB_IMPORTANT, LOC_ERR + pginfokey + "Got invalid date");
+        previewWaitCondition.wakeAll();
+        return false;
+    }
+
+    size_t     length     = me->ExtraData(4).toULongLong();
+    quint16    checksum16 = me->ExtraData(5).toUInt();
+    QByteArray data       = QByteArray::fromBase64(me->ExtraData(6).toAscii());
+    if ((size_t) data.size() < length)
+    {   // (note data.size() may be up to 3
+        //  bytes longer after decoding
+        VERBOSE(VB_IMPORTANT, LOC_ERR +
+                QString("Preview size check failed %1 < %2")
+                .arg(data.size()).arg(length));
+        data.clear();
+    }
+    data.resize(length);
+
+    if (checksum16 != qChecksum(data.constData(), data.size()))
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR + "Preview checksum failed");
+        data.clear();
+    }
+
+    pixmapOk = (data.isEmpty()) ? false : SaveOutFile(data, datetime);
+
+    previewWaitCondition.wakeAll();
+
+    return true;
+}
+
+bool PreviewGenerator::SaveOutFile(const QByteArray &data, const QDateTime &dt)
+{
     if (outFileName.isEmpty())
     {
-        QString remotecachedirname = QString("%1/remotecache").arg(GetConfDir());
+        QString remotecachedirname =
+            QString("%1/remotecache").arg(GetConfDir());
         QDir remotecachedir(remotecachedirname);
 
         if (!remotecachedir.exists())
@@ -368,73 +481,46 @@
         outFileName = QString("%1/%2").arg(remotecachedirname).arg(filename);
     }
 
-    // find file, copy/move to output file name & location...
-
-    QString url = QString::null;
-    QString fn = QFileInfo(outFileName).fileName();
-    QByteArray data;
-    ok = false;
-
-    QStringList fileNames;
-    fileNames.push_back(
-        CreateAccessibleFilename(programInfo.GetPathname(), fn));
-    fileNames.push_back(
-        CreateAccessibleFilename(programInfo.GetPathname(), ""));
-
-    QStringList::const_iterator it = fileNames.begin();
-    for ( ; it != fileNames.end() && (!ok || data.isEmpty()); ++it)
+    QFile file(outFileName);
+    bool ok = file.open(QIODevice::Unbuffered|QIODevice::WriteOnly);
+    if (!ok)
     {
-        data.resize(0);
-        url = *it;
-        RemoteFile *rf = new RemoteFile(url, false, false, 0);
-        ok = rf->SaveAs(data);
-        delete rf;
+        VERBOSE(VB_IMPORTANT, LOC_ERR + QString("Failed to open: '%1'")
+                .arg(outFileName));
     }
 
-    if (ok && data.size())
+    off_t offset = 0;
+    size_t remaining = data.size();
+    uint failure_cnt = 0;
+    while ((remaining > 0) && (failure_cnt < 5))
     {
-        QFile file(outFileName);
-        ok = file.open(QIODevice::Unbuffered|QIODevice::WriteOnly);
-        if (!ok)
+        ssize_t written = file.write(data.data() + offset, remaining);
+        if (written < 0)
         {
-            VERBOSE(VB_IMPORTANT, QString("Failed to open: '%1'")
-                    .arg(outFileName));
+            failure_cnt++;
+            usleep(50000);
+            continue;
         }
 
-        off_t offset = 0;
-        size_t remaining = (ok) ? data.size() : 0;
-        uint failure_cnt = 0;
-        while ((remaining > 0) && (failure_cnt < 5))
-        {
-            ssize_t written = file.write(data.data() + offset, remaining);
-            if (written < 0)
-            {
-                failure_cnt++;
-                usleep(50000);
-                continue;
-            }
-
-            failure_cnt  = 0;
-            offset      += written;
-            remaining   -= written;
-        }
-        if (ok && !remaining)
-        {
-            VERBOSE(VB_PLAYBACK, QString("Saved: '%1'")
-                    .arg(outFileName));
-        }
+        failure_cnt  = 0;
+        offset      += written;
+        remaining   -= written;
     }
 
-    return ok && data.size();
-}
-
-void PreviewGenerator::RemotePreviewTeardown(void)
-{
-    if (serverSock)
+    if (ok && !remaining)
     {
-        serverSock->DownRef();
-        serverSock = NULL;
+        file.close();
+        struct utimbuf times;
+        times.actime = times.modtime = dt.toTime_t();
+        utime(outFileName.toLocal8Bit().constData(), &times);
+        VERBOSE(VB_FILE, LOC + QString("Saved: '%1'").arg(outFileName));
     }
+    else
+    {
+        file.remove();
+    }
+
+    return ok;
 }
 
 bool PreviewGenerator::SavePreview(QString filename,
@@ -476,35 +562,24 @@
     QImage small_img = img.scaled((int) ppw, (int) pph,
         Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
 
-    QByteArray fname = filename.toAscii();
-    if (small_img.save(fname.constData(), "PNG"))
+    QTemporaryFile f(QFileInfo(filename).absoluteFilePath()+".XXXXXX");
+    f.setAutoRemove(false);
+    if (f.open() && small_img.save(&f, "PNG"))
     {
-        makeFileAccessible(fname.constData()); // Let anybody update it
-
-        VERBOSE(VB_PLAYBACK, LOC +
-                QString("Saved preview '%0' %1x%2")
-                .arg(filename).arg((int) ppw).arg((int) pph));
-
-        return true;
+        // Let anybody update it
+        makeFileAccessible(f.fileName().toLocal8Bit().constData());
+        QFile of(filename);
+        of.remove();
+        if (f.rename(filename))
+        {
+            VERBOSE(VB_PLAYBACK, LOC +
+                    QString("Saved preview '%0' %1x%2")
+                    .arg(filename).arg((int) ppw).arg((int) pph));
+            return true;
+        }
+        f.remove();
     }
 
-    // Save failed; if file exists, try saving to .new and moving over
-    QString newfile = filename + ".new";
-    QByteArray newfilea = newfile.toAscii();
-    if (QFileInfo(fname.constData()).exists() &&
-        small_img.save(newfilea.constData(), "PNG"))
-    {
-        makeFileAccessible(newfilea.constData());
-        rename(newfilea.constData(), fname.constData());
-
-        VERBOSE(VB_PLAYBACK, LOC +
-                QString("Saved preview '%0' %1x%2")
-                .arg(filename).arg((int) ppw).arg((int) pph));
-
-        return true;
-    }
-
-    // Couldn't save, nothing else I can do?
     return false;
 }
 
@@ -515,8 +590,23 @@
     float aspect = 0;
     int   width, height, sz;
     long long captime = captureTime;
+
+    QDateTime dt = QDateTime::currentDateTime();
+
+    if (captime > 0)
+        VERBOSE(VB_IMPORTANT, "Preview from time spec");
+
     if (captime < 0)
     {
+        captime = programInfo.QueryBookmark();
+        if (captime > 0)
+            timeInSeconds = false;
+        else
+            captime = -1;
+    }
+
+    if (captime < 0)
+    {
         timeInSeconds = true;
         int startEarly = 0;
         int programDuration = 0;
@@ -559,6 +649,15 @@
 
     bool ok = SavePreview(outname, data, width, height, aspect, dw, dh);
 
+    if (ok)
+    {
+        // Backdate file to start of preview time in case a bookmark was made
+        // while we were generating the preview.
+        struct utimbuf times;
+        times.actime = times.modtime = dt.toTime_t();
+        utime(outname.toLocal8Bit().constData(), &times);
+    }
+
     delete[] data;
 
     programInfo.MarkAsInUse(false, kPreviewGeneratorInUseID);
@@ -604,10 +703,21 @@
     if (tmppathname.left(4) == "dvd:")
         tmppathname = tmppathname.section(":", 1, 1);
 
+    if (!QFileInfo(tmppathname).isReadable())
+        return false;
+
+    tmppathname = outFileName.isEmpty() ? tmppathname : outFileName;
     QString pathdir = QFileInfo(tmppathname).path();
 
-    return (QFileInfo(tmppathname).isReadable() &&
-            QFileInfo(pathdir).isWritable());
+    if (!QFileInfo(pathdir).isWritable())
+    {
+        VERBOSE(VB_IMPORTANT, LOC_WARN +
+                QString("Output path '%1' is not writeable")
+                .arg(pathdir));
+        return false;
+    }
+
+    return true;
 }
 
 /**
@@ -640,7 +750,7 @@
     (void) video_height;
     char *retbuf = NULL;
     bufferlen = 0;
-#ifdef USING_FRONTEND
+
     if (!MSqlQuery::testDBConnection())
     {
         VERBOSE(VB_IMPORTANT, LOC_ERR + "Previewer could not connect to DB.");
@@ -686,11 +796,6 @@
 
     delete ctx;
 
-#else // USING_FRONTEND
-    QString msg = "Backend compiled without USING_FRONTEND !!!!";
-    VERBOSE(VB_IMPORTANT, LOC_ERR + msg);
-#endif // USING_FRONTEND
-
     if (retbuf)
     {
         VERBOSE(VB_GENERAL, LOC +
Index: mythtv/libs/libmythtv/tv_play.cpp
===================================================================
--- mythtv/libs/libmythtv/tv_play.cpp	(revision 26104)
+++ mythtv/libs/libmythtv/tv_play.cpp	(working copy)
@@ -57,6 +57,7 @@
 #include "mythsystemevent.h"
 #include "videometadatautil.h"
 #include "mythdialogbox.h"
+#include "mythdirs.h"
 
 #if ! HAVE_ROUND
 #define round(x) ((int) ((x) + 0.5))
@@ -11470,6 +11471,15 @@
  */
 bool TV::ScreenShot(PlayerContext *ctx, long long frameNumber)
 {
+    QDir d;
+    QString confdir = GetConfDir();
+    if (!d.mkpath(confdir))
+    {
+        QString msg = tr("Screen Shot") + " " + tr("Error");
+        SetOSDMessage(ctx, msg);
+        return false;
+    }
+
     ctx->LockPlayingInfo(__FILE__, __LINE__);
     if (!ctx->playingInfo)
     {
@@ -11479,16 +11489,14 @@
         return false;
     }
 
-    // TODO FIXME .mythtv isn't guaranteed to exist, and may
-    // very well belong to another frontend.
     QString outFile =
-        QString("%1/.mythtv/%2_%3_%4.png")
-        .arg(QDir::homePath()).arg(ctx->playingInfo->GetChanID())
+        QString("%1/%2_%3_%4.png")
+        .arg(confdir).arg(ctx->playingInfo->GetChanID())
         .arg(ctx->playingInfo->GetRecordingStartTime(MythDate))
         .arg(frameNumber);
 
     PreviewGenerator *previewgen = new PreviewGenerator(
-        ctx->playingInfo, PreviewGenerator::kLocalAndRemote);
+        ctx->playingInfo, QString(), PreviewGenerator::kLocalAndRemote);
     ctx->UnlockPlayingInfo(__FILE__, __LINE__);
 
     previewgen->SetPreviewTimeAsFrameNumber(frameNumber);
Index: mythtv/libs/libmythtv/previewgeneratorqueue.cpp
===================================================================
--- mythtv/libs/libmythtv/previewgeneratorqueue.cpp	(revision 26104)
+++ mythtv/libs/libmythtv/previewgeneratorqueue.cpp	(working copy)
@@ -0,0 +1,523 @@
+#include <QCoreApplication>
+#include <QFileInfo>
+#include <QThread>
+
+#include "previewgeneratorqueue.h"
+#include "previewgenerator.h"
+#include "mythcorecontext.h"
+#include "mythcontext.h"
+#include "remoteutil.h"
+#include "mythdirs.h"
+
+#define LOC QString("PreviewQueue: ")
+#define LOC_ERR QString("PreviewQueue Error: ")
+#define LOC_WARN QString("PreviewQueue Warning: ")
+
+PreviewGeneratorQueue *PreviewGeneratorQueue::s_pgq = NULL;
+
+void PreviewGeneratorQueue::CreatePreviewGeneratorQueue(
+    PreviewGenerator::Mode mode,
+    uint maxAttempts, uint minBlockSeconds)
+{
+    s_pgq = new PreviewGeneratorQueue(mode, maxAttempts, minBlockSeconds);
+}
+
+void PreviewGeneratorQueue::TeardownPreviewGeneratorQueue()
+{
+    s_pgq->exit(0);
+    s_pgq->wait();
+    delete s_pgq;
+}
+
+PreviewGeneratorQueue::PreviewGeneratorQueue(
+    PreviewGenerator::Mode mode,
+    uint maxAttempts, uint minBlockSeconds) :
+    m_mode(mode),
+    m_running(0), m_maxThreads(2),
+    m_maxAttempts(maxAttempts), m_minBlockSeconds(minBlockSeconds)
+{
+    if (PreviewGenerator::kLocal & mode)
+    {
+        int idealThreads = QThread::idealThreadCount();
+        m_maxThreads = (idealThreads >= 1) ? idealThreads * 2 : 2;
+    }
+
+    moveToThread(this);
+    start();
+}
+
+PreviewGeneratorQueue::~PreviewGeneratorQueue()
+{
+    // disconnect preview generators
+    QMutexLocker locker(&m_lock);
+    PreviewMap::iterator it = m_previewMap.begin();
+    for (;it != m_previewMap.end(); ++it)
+    {
+        if ((*it).gen)
+            (*it).gen->deleteLater();
+    }
+}
+
+void PreviewGeneratorQueue::GetPreviewImage(
+    const ProgramInfo &pginfo,
+    const QSize &outputsize,
+    const QString &outputfile,
+    long long time, bool in_seconds,
+    QString token)
+{
+    if (!s_pgq)
+        return;
+
+    if (pginfo.GetPathname().isEmpty() ||
+        pginfo.GetBasename() == pginfo.GetPathname())
+    {
+        return;
+    }
+
+    QStringList extra;
+    pginfo.ToStringList(extra);
+    extra += token;
+    extra += QString::number(outputsize.width());
+    extra += QString::number(outputsize.height());
+    extra += outputfile;
+    extra += QString::number(time);
+    extra += (in_seconds ? "1" : "0");
+    MythEvent *e = new MythEvent("GET_PREVIEW", extra);
+    QCoreApplication::postEvent(s_pgq, e);
+}
+
+void PreviewGeneratorQueue::AddListener(QObject *listener)
+{
+    if (!s_pgq)
+        return;
+
+    QMutexLocker locker(&s_pgq->m_lock);
+    s_pgq->m_listeners.insert(listener);
+}
+
+void PreviewGeneratorQueue::RemoveListener(QObject *listener)
+{
+    if (!s_pgq)
+        return;
+
+    QMutexLocker locker(&s_pgq->m_lock);
+    s_pgq->m_listeners.remove(listener);
+}
+
+bool PreviewGeneratorQueue::event(QEvent *e)
+{
+    if (e->type() != (QEvent::Type) MythEvent::MythEventMessage)
+        return QObject::event(e);
+
+    MythEvent *me = (MythEvent*)e;
+    if (me->Message() == "GET_PREVIEW")
+    {
+        const QStringList list = me->ExtraDataList();
+        QStringList::const_iterator it = list.begin();
+        ProgramInfo evinfo(it, list.end());
+        QString token;
+        QSize outputsize;
+        QString outputfile;
+        long long time;
+        bool time_fmt_sec;
+        if (it != list.end())
+            token = (*it++);
+        if (it != list.end())
+            outputsize.setWidth((*it++).toInt());
+        if (it != list.end())
+            outputsize.setHeight((*it++).toInt());
+        if (it != list.end())
+            outputfile = (*it++);
+        if (it != list.end())
+            time = (*it++).toLongLong();
+        QString fn;
+        if (it != list.end())
+        {
+            time_fmt_sec = (*it++).toInt() != 0;
+            fn = GeneratePreviewImage(evinfo, outputsize, outputfile,
+                                      time, time_fmt_sec, token);
+        }
+        return true;
+    }
+    else if (me->Message() == "PREVIEW_SUCCESS" ||
+             me->Message() == "PREVIEW_FAILED")
+    {
+        QString pginfokey = me->ExtraData(0); // pginfo->MakeUniqueKey()
+        QString filename  = me->ExtraData(1); // outFileName
+        QString msg       = me->ExtraData(2);
+        QString datetime  = me->ExtraData(3);
+        QString token     = me->ExtraData(4);
+
+        {
+            QMutexLocker locker(&m_lock);
+            QMap<QString,QString>::iterator kit = m_tokenToKeyMap.find(token);
+            if (kit == m_tokenToKeyMap.end())
+            {
+                VERBOSE(VB_IMPORTANT, LOC_ERR +
+                        QString("Failed to find token %1 in map.").arg(token));
+                return true;
+            }
+            PreviewMap::iterator it = m_previewMap.find(*kit);
+            if (it == m_previewMap.end())
+            {
+                VERBOSE(VB_IMPORTANT, LOC_ERR +
+                        QString("Failed to find key %1 in map.").arg(*kit));
+                return true;
+            }
+
+            (*it).gen           = NULL;
+            (*it).genStarted    = false;
+            if (me->Message() == "PREVIEW_SUCCESS")
+            {
+                (*it).attempts      = 0;
+                (*it).lastBlockTime = 0;
+                (*it).blockRetryUntil = QDateTime();
+            }
+            else
+            {
+                (*it).lastBlockTime =
+                    max(m_minBlockSeconds, (*it).lastBlockTime * 2);
+                (*it).blockRetryUntil =
+                    QDateTime::currentDateTime().addSecs((*it).lastBlockTime);
+            }
+
+            QStringList list;
+            list.push_back(pginfokey);
+            list.push_back(filename);
+            list.push_back(msg);
+            list.push_back(datetime);
+            QSet<QString>::const_iterator tit = (*it).tokens.begin();
+            for (; tit != (*it).tokens.end(); ++tit)
+            {
+                kit = m_tokenToKeyMap.find(*tit);
+                if (kit != m_tokenToKeyMap.end())
+                    m_tokenToKeyMap.erase(kit);
+                list.push_back(*tit);
+            }
+
+            QSet<QObject*>::iterator sit = m_listeners.begin();
+            for (; sit != m_listeners.end(); ++sit)
+            {
+                MythEvent *e = new MythEvent(me->Message(), list);
+                QCoreApplication::postEvent(*sit, e);
+            }
+            (*it).tokens.clear();
+
+            m_running--;
+        }
+
+        UpdatePreviewGeneratorThreads();
+
+        return true;
+    }
+    return false;
+}
+
+void PreviewGeneratorQueue::SendEvent(
+    const ProgramInfo &pginfo,
+    const QString &eventname,
+    const QString &fn, const QString &token, const QString &msg,
+    const QDateTime &dt)
+{
+    QStringList list;
+    list.push_back(pginfo.MakeUniqueKey());
+    list.push_back(fn);
+    list.push_back(msg);
+    list.push_back(dt.toString(Qt::ISODate));
+    list.push_back(token);
+
+    QMutexLocker locker(&m_lock);
+    QSet<QObject*>::iterator it = m_listeners.begin();
+    for (; it != m_listeners.end(); ++it)
+    {
+        MythEvent *e = new MythEvent(eventname, list);
+        QCoreApplication::postEvent(*it, e);
+    }
+}
+
+QString PreviewGeneratorQueue::GeneratePreviewImage(
+    ProgramInfo &pginfo,
+    const QSize &size,
+    const QString &outputfile,
+    long long time, bool in_seconds,
+    QString token)
+{
+    QString key = QString("%1_%2x%3_%4%5")
+        .arg(pginfo.GetBasename()).arg(size.width()).arg(size.height())
+        .arg(time).arg(in_seconds?"s":"f");
+
+    if (pginfo.GetAvailableStatus() == asPendingDelete)
+    {
+        SendEvent(pginfo, "PREVIEW_FAILED", key, token,
+                  "Pending Delete", QDateTime());
+        return QString();
+    }
+
+    QString filename = (outputfile.isEmpty()) ?
+        pginfo.GetPathname() + ".png" : outputfile;
+    QString ret_file = filename;
+    QString ret;
+
+    bool is_special = !outputfile.isEmpty() || time >= 0 ||
+        size.width() || size.height();
+
+    bool needs_gen = true;
+    if (!is_special)
+    {
+        QDateTime previewLastModified;
+        bool streaming = filename.left(1) != "/";
+        bool locally_accessible = false;
+        bool bookmark_updated = false;
+
+        QDateTime bookmark_ts = pginfo.QueryBookmarkTimeStamp();
+        QDateTime cmp_ts = bookmark_ts.isValid() ?
+            bookmark_ts : pginfo.GetLastModifiedTime();
+
+        if (streaming)
+        {
+            ret_file = QString("%1/remotecache/%2")
+                .arg(GetConfDir()).arg(filename.section('/', -1));
+
+            QFileInfo finfo(ret_file);
+            if (finfo.isReadable() && finfo.lastModified() >= cmp_ts)
+            {
+                // This is just an optimization to avoid
+                // hitting the backend if our cached copy
+                // is newer than the bookmark, or if we have
+                // a preview and do not update it when the
+                // bookmark changes.
+                previewLastModified = finfo.lastModified();
+            }
+            else if (!IsGeneratingPreview(key))
+            {
+                previewLastModified =
+                    RemoteGetPreviewIfModified(pginfo, ret_file);
+            }
+        }
+        else
+        {
+            QFileInfo fi(filename);
+            if ((locally_accessible = fi.isReadable()))
+                previewLastModified = fi.lastModified();
+        }
+
+        bookmark_updated =
+            (!previewLastModified.isValid() || (previewLastModified <= cmp_ts));
+
+        if (bookmark_updated && bookmark_ts.isValid() &&
+            previewLastModified.isValid())
+        {
+            ClearPreviewGeneratorAttempts(key);
+        }
+
+        bool preview_exists = previewLastModified.isValid();
+
+        if (0)
+        {
+            QString alttext = (bookmark_ts.isValid()) ? QString() :
+                QString("\n\t\t\tcmp_ts:               %1")
+                .arg(cmp_ts.toString(Qt::ISODate));
+            VERBOSE(VB_IMPORTANT, QString(
+                        "previewLastModified:  %1\n\t\t\t"
+                        "bookmark_ts:          %2%3\n\t\t\t"
+                        "pginfo.lastmodified:  %4")
+                    .arg(previewLastModified.toString(Qt::ISODate))
+                    .arg(bookmark_ts.toString(Qt::ISODate))
+                    .arg(alttext)
+                    .arg(pginfo.GetLastModifiedTime(ISODate)) +
+                    QString("Title: %1\n\t\t\t")
+                    .arg(pginfo.toString(ProgramInfo::kTitleSubtitle)) +
+                    QString("File  '%1' \n\t\t\tCache '%2'")
+                    .arg(filename).arg(ret_file) +
+                    QString("\n\t\t\tPreview Exists: %1, "
+                            "Bookmark Updated: %2, "
+                            "Need Preview: %3")
+                    .arg(preview_exists).arg(bookmark_updated)
+                    .arg((bookmark_updated || !preview_exists)));
+        }
+
+        needs_gen = bookmark_updated || !preview_exists;
+
+        if (!needs_gen)
+        {
+            if (locally_accessible)
+                ret = filename;
+            else if (preview_exists && QFileInfo(ret_file).isReadable())
+                ret = ret_file;
+        }
+    }
+
+    if (needs_gen && !IsGeneratingPreview(key))
+    {
+        uint attempts = IncPreviewGeneratorAttempts(key);
+        if (attempts < m_maxAttempts)
+        {
+            VERBOSE(VB_PLAYBACK, LOC +
+                    QString("Requesting preview for '%1'")
+                    .arg(key));
+            PreviewGenerator *pg = new PreviewGenerator(
+                &pginfo, token, m_mode);
+            if (!outputfile.isEmpty() || time >= 0 ||
+                size.width() || size.height())
+            {
+                pg->SetPreviewTime(time, in_seconds);
+                pg->SetOutputFilename(outputfile);
+                pg->SetOutputSize(size);
+            }
+
+            SetPreviewGenerator(key, pg);
+
+            VERBOSE(VB_PLAYBACK, LOC +
+                    QString("Requested preview for '%1'").arg(key));
+        }
+        else if (attempts >= m_maxAttempts)
+        {
+            VERBOSE(VB_IMPORTANT, LOC_ERR +
+                    QString("Attempted to generate preview for '%1' "
+                            "%2 times; >= max(%3)")
+                    .arg(key).arg(attempts).arg(m_maxAttempts));
+        }
+    }
+    else if (needs_gen)
+    {
+        VERBOSE(VB_PLAYBACK, LOC +
+                "Not requesting preview as it "
+                "is already being generated");
+        IncPreviewGeneratorPriority(key, token);
+    }
+
+    UpdatePreviewGeneratorThreads();
+
+    if (!ret.isEmpty())
+    {
+        QString msg = "On Disk";
+        QDateTime dt = QFileInfo(ret).lastModified();
+        SendEvent(pginfo, "PREVIEW_SUCCESS", ret, token, msg, dt);
+    }
+    else
+    {
+        uint queue_depth, token_cnt;
+        GetInfo(key, queue_depth, token_cnt);
+        QString msg = QString("Queue depth %1, our tokens %2")
+            .arg(queue_depth).arg(token_cnt);
+        SendEvent(pginfo, "PREVIEW_QUEUED", ret, token, msg, QDateTime());
+    }
+
+    return ret;
+}
+
+void PreviewGeneratorQueue::GetInfo(
+    const QString &key, uint &queue_depth, uint &token_cnt)
+{
+    QMutexLocker locker(&m_lock);
+    queue_depth = m_queue.size();
+    PreviewMap::iterator pit = m_previewMap.find(key);
+    token_cnt = (pit == m_previewMap.end()) ? 0 : (*pit).tokens.size();
+}
+
+void PreviewGeneratorQueue::IncPreviewGeneratorPriority(
+    const QString &key, QString token)
+{
+    QMutexLocker locker(&m_lock);
+    m_queue.removeAll(key);
+
+    PreviewMap::iterator pit = m_previewMap.find(key);
+    if (pit == m_previewMap.end())
+        return;
+
+    if ((*pit).gen && !(*pit).genStarted)
+        m_queue.push_back(key);
+
+    if (!token.isEmpty())
+    {
+        m_tokenToKeyMap[token] = key;
+        (*pit).tokens.insert(token);
+    }
+}
+
+void PreviewGeneratorQueue::UpdatePreviewGeneratorThreads(void)
+{
+    QMutexLocker locker(&m_lock);
+    QStringList &q = m_queue;
+    if (!q.empty() && (m_running < m_maxThreads))
+    {
+        QString fn = q.back();
+        q.pop_back();
+        PreviewMap::iterator it = m_previewMap.find(fn);
+        if (it != m_previewMap.end() && (*it).gen && !(*it).genStarted)
+        {
+            m_running++;
+            (*it).gen->start();
+            (*it).genStarted = true;
+        }
+    }
+}
+
+/** \brief Sets the PreviewGenerator for a specific file.
+ *  \return true iff call succeeded.
+ */
+void PreviewGeneratorQueue::SetPreviewGenerator(
+    const QString &key, PreviewGenerator *g)
+{
+    {
+        QMutexLocker locker(&m_lock);
+        m_tokenToKeyMap[g->GetToken()] = key;
+        PreviewGenState &state = m_previewMap[key];
+        if (state.gen)
+        {
+            if (!g->GetToken().isEmpty())
+                state.tokens.insert(g->GetToken());
+            g->deleteLater();
+        }
+        else
+        {
+            g->AttachSignals(this);
+            state.gen = g;
+            state.genStarted = false;
+            if (!g->GetToken().isEmpty())
+                state.tokens.insert(g->GetToken());
+        }
+    }
+
+    IncPreviewGeneratorPriority(key, "");
+}
+
+/** \brief Returns true if we have already started a
+ *         PreviewGenerator to create this file.
+ */
+bool PreviewGeneratorQueue::IsGeneratingPreview(const QString &key) const
+{
+    PreviewMap::const_iterator it;
+    QMutexLocker locker(&m_lock);
+
+    if ((it = m_previewMap.find(key)) == m_previewMap.end())
+        return false;
+
+    if ((*it).blockRetryUntil.isValid())
+        return QDateTime::currentDateTime() < (*it).blockRetryUntil;
+
+    return (*it).gen;
+}
+
+/** \fn PreviewGeneratorQueue::IncPreviewGeneratorAttempts(const QString&)
+ *  \brief Increments and returns number of times we have
+ *         started a PreviewGenerator to create this file.
+ */
+uint PreviewGeneratorQueue::IncPreviewGeneratorAttempts(const QString &key)
+{
+    QMutexLocker locker(&m_lock);
+    return m_previewMap[key].attempts++;
+}
+
+/** \fn PreviewGeneratorQueue::ClearPreviewGeneratorAttempts(const QString&)
+ *  \brief Clears the number of times we have
+ *         started a PreviewGenerator to create this file.
+ */
+void PreviewGeneratorQueue::ClearPreviewGeneratorAttempts(const QString &key)
+{
+    QMutexLocker locker(&m_lock);
+    m_previewMap[key].attempts = 0;
+    m_previewMap[key].lastBlockTime = 0;
+    m_previewMap[key].blockRetryUntil =
+        QDateTime::currentDateTime().addSecs(-60);
+}
Index: mythtv/libs/libmythtv/tv_rec.cpp
===================================================================
--- mythtv/libs/libmythtv/tv_rec.cpp	(revision 26104)
+++ mythtv/libs/libmythtv/tv_rec.cpp	(working copy)
@@ -11,6 +11,7 @@
 using namespace std;
 
 // MythTV headers
+#include "previewgeneratorqueue.h"
 #include "mythconfig.h"
 #include "tv_rec.h"
 #include "osd.h"
@@ -25,7 +26,6 @@
 #include "recordingrule.h"
 #include "eitscanner.h"
 #include "RingBuffer.h"
-#include "previewgenerator.h"
 #include "storagegroup.h"
 #include "remoteutil.h"
 #include "tvremoteutil.h"
@@ -1143,10 +1143,7 @@
         if (!killFile)
         {
             if (curRecording->IsLocal())
-            {
-                (new PreviewGenerator(
-                    curRecording, PreviewGenerator::kLocal))->Start();
-            }
+                PreviewGeneratorQueue::GetPreviewImage(*curRecording, "");
 
             if (!tvchain)
             {
@@ -4528,10 +4525,7 @@
             if (!oldinfo->IsLocal())
                 oldinfo->SetPathname(oldinfo->GetPlaybackURL(false,true));
             if (oldinfo->IsLocal())
-            {
-                (new PreviewGenerator(
-                    oldinfo, PreviewGenerator::kLocal))->Start();
-            }
+                PreviewGeneratorQueue::GetPreviewImage(*oldinfo, "");
         }
         delete oldinfo;
     }
Index: mythtv/libs/libmythtv/previewgenerator.h
===================================================================
--- mythtv/libs/libmythtv/previewgenerator.h	(revision 26104)
+++ mythtv/libs/libmythtv/previewgenerator.h	(working copy)
@@ -2,18 +2,27 @@
 #ifndef PREVIEW_GENERATOR_H_
 #define PREVIEW_GENERATOR_H_
 
-#include <pthread.h>
-
+#include <QWaitCondition>
+#include <QDateTime>
 #include <QString>
+#include <QThread>
 #include <QMutex>
 #include <QSize>
+#include <QMap>
+#include <QSet>
 
 #include "programinfo.h"
 #include "util.h"
 
+class PreviewGenerator;
+class QByteArray;
 class MythSocket;
+class QObject;
+class QEvent;
 
-class MPUBLIC PreviewGenerator : public QObject
+typedef QMap<QString,QDateTime> FileTimeStampMap;
+
+class MPUBLIC PreviewGenerator : public QThread
 {
     friend int preview_helper(const QString &chanid,
                               const QString &starttime,
@@ -36,7 +45,9 @@
     } Mode;
 
   public:
-    PreviewGenerator(const ProgramInfo *pginfo, Mode mode = kLocal);
+    PreviewGenerator(const ProgramInfo *pginfo,
+                     const QString     &token,
+                     Mode               mode = kLocal);
 
     void SetPreviewTime(long long time, bool in_seconds)
         { captureTime = time; timeInSeconds = in_seconds; }
@@ -47,16 +58,13 @@
     void SetOutputFilename(const QString&);
     void SetOutputSize(const QSize &size) { outSize = size; }
 
-    void Start(void);
+    QString GetToken(void) const { return token; }
+
+    void run(void); // QThread
     bool Run(void);
 
     void AttachSignals(QObject*);
-    void disconnectSafe(void);
 
-  signals:
-    void previewThreadDone(const QString&, bool&);
-    void previewReady(const ProgramInfo*);
-
   public slots:
     void deleteLater();
 
@@ -64,17 +72,12 @@
     virtual ~PreviewGenerator();
     void TeardownAll(void);
 
-    bool RemotePreviewSetup(void);
     bool RemotePreviewRun(void);
-    void RemotePreviewTeardown(void);
-
     bool LocalPreviewRun(void);
     bool IsLocal(void) const;
 
     bool RunReal(void);
 
-    static void *PreviewRun(void*);
-
     static char *GetScreenGrab(const ProgramInfo &pginfo,
                                const QString     &filename,
                                long long          seektime,
@@ -93,15 +96,16 @@
     static QString CreateAccessibleFilename(
         const QString &pathname, const QString &outFileName);
 
+    virtual bool event(QEvent *e); // QObject
+    bool SaveOutFile(const QByteArray &data, const QDateTime &dt);
+
   protected:
+    QWaitCondition     previewWaitCondition;
     QMutex             previewLock;
-    pthread_t          previewThread;
     ProgramInfo        programInfo;
 
     Mode               mode;
-    bool               isConnected;
-    bool               createSockets;
-    MythSocket        *serverSock;
+    QObject           *listener;
     QString            pathname;
 
     /// tells us whether to use time as seconds or frame number
@@ -110,6 +114,10 @@
     long long          captureTime;
     QString            outFileName;
     QSize              outSize;
+
+    QString            token;
+    bool               gotReply;
+    bool               pixmapOk;
 };
 
 #endif // PREVIEW_GENERATOR_H_
Index: mythtv/libs/libmythui/mythuiimage.cpp
===================================================================
--- mythtv/libs/libmythui/mythuiimage.cpp	(revision 26104)
+++ mythtv/libs/libmythui/mythuiimage.cpp	(working copy)
@@ -73,10 +73,10 @@
   public:
     ImageLoadThread(MythUIImage *parent, const QString &basefile,
                     const QString &filename, int number,
-                    QSize forceSize) :
+                    QSize forceSize, ImageCacheMode mode) :
         m_parent(parent), m_basefile(basefile),
         m_filename(filename), m_number(number),
-        m_ForceSize(forceSize)
+        m_ForceSize(forceSize), m_cacheMode(mode)
     {
         m_basefile.detach();
         m_filename.detach();
@@ -95,13 +95,13 @@
 
         if (imageReader.supportsAnimation())
         {
-            m_parent->LoadAnimatedImage(imageReader, m_filename, m_ForceSize);
+            m_parent->LoadAnimatedImage(
+                imageReader, m_filename, m_ForceSize, m_cacheMode);
         }
         else
         {
-            MythImage *image =
-                m_parent->LoadImage(imageReader, m_filename, m_ForceSize);
-
+            MythImage *image = m_parent->LoadImage(
+                imageReader, m_filename, m_ForceSize, m_cacheMode);
             ImageLoadEvent *le = new ImageLoadEvent(m_parent, image, m_basefile,
                                                     m_filename, m_number);
             QCoreApplication::postEvent(m_parent, le);
@@ -114,6 +114,7 @@
     QString      m_filename;
     int          m_number;
     QSize        m_ForceSize;
+    ImageCacheMode m_cacheMode;
 };
 
 /////////////////////////////////////////////////////////////////
@@ -542,7 +543,7 @@
 /**
  *  \brief Load the image(s), wraps LoadImage()
  */
-bool MythUIImage::Load(bool allowLoadInBackground)
+bool MythUIImage::Load(bool allowLoadInBackground, bool forceStat)
 {
     d->m_UpdateLock.lockForRead();
 
@@ -621,7 +622,6 @@
     }
 
     QString imagelabel;
-    ImageCacheMode cacheMode = kCacheCheckMemoryOnly;
 
     int j = 0;
     for (int i = m_LowNum; i <= m_HighNum && !m_animatedImage; i++)
@@ -633,20 +633,25 @@
 
         // Only load in the background if allowed and the image is
         // not already in our mem cache
+        ImageCacheMode cacheMode = kCacheCheckMemoryOnly;
         if (m_gradient)
             cacheMode = kCacheIgnoreDisk;
-        else
-            cacheMode = kCacheCheckMemoryOnly;
+        else if (forceStat)
+            cacheMode = (ImageCacheMode)
+                ((int)kCacheCheckMemoryOnly | (int)kCacheForceStat);
+        
+        ImageCacheMode cacheMode2 = (!forceStat) ? kCacheNormal :
+            (ImageCacheMode) ((int)kCacheNormal | (int)kCacheForceStat);
 
+
         if ((allowLoadInBackground) &&
             (!GetMythUI()->LoadCacheImage(filename, imagelabel, cacheMode)) &&
             (!getenv("DISABLETHREADEDMYTHUIIMAGE")))
         {
             VERBOSE(VB_GUI|VB_FILE|VB_EXTRA, LOC + QString(
                         "Load(), spawning thread to load '%1'").arg(filename));
-            ImageLoadThread *bImgThread =
-                new ImageLoadThread(this, bFilename, filename, i,
-                                    bForceSize);
+            ImageLoadThread *bImgThread = new ImageLoadThread(
+                this, bFilename, filename, i, bForceSize, cacheMode2);
             GetMythUI()->GetImageThreadPool()->start(bImgThread);
         }
         else
@@ -665,11 +670,13 @@
 
             if (imageReader.supportsAnimation())
             {
-                LoadAnimatedImage(imageReader, filename, bForceSize);
+                LoadAnimatedImage(
+                    imageReader, filename, bForceSize, cacheMode2);
             }
             else
             {
-                MythImage *image = LoadImage(imageReader, filename, bForceSize);
+                MythImage *image = LoadImage(
+                    imageReader, filename, bForceSize, cacheMode2);
                 if (image)
                 {
                     if (bForceSize.isNull())
@@ -701,8 +708,9 @@
 /**
 *  \brief Load an image
 */
-MythImage *MythUIImage::LoadImage(MythImageReader &imageReader,
-                                  const QString &imFile, QSize bForceSize)
+MythImage *MythUIImage::LoadImage(
+    MythImageReader &imageReader, const QString &imFile,
+    QSize bForceSize, int cacheMode)
 {
     QString filename = imFile;
 
@@ -754,7 +762,10 @@
     imagelabel = GenImageLabel(filename, w, h);
 
     if (!imageReader.supportsAnimation())
-        image = GetMythUI()->LoadCacheImage(filename, imagelabel);
+    {
+        image = GetMythUI()->LoadCacheImage(
+            filename, imagelabel, (ImageCacheMode) cacheMode);
+    }
 
     if (image)
     {
@@ -882,8 +893,9 @@
 /**
 *  \brief Load an animated image
 */
-bool MythUIImage::LoadAnimatedImage(MythImageReader &imageReader,
-                                    const QString &imFile, QSize bForceSize)
+bool MythUIImage::LoadAnimatedImage(
+    MythImageReader &imageReader, const QString &imFile,
+    QSize bForceSize, int cacheMode)
 {
     bool result = false;
     m_loadingImagesLock.lock();
@@ -930,7 +942,9 @@
     {
         frameFilename = filename.arg(imageCount);
         imageLabel = GenImageLabel(frameFilename, w, h);
-        MythImage *im = LoadImage(imageReader, frameFilename, bForceSize);
+        MythImage *im = LoadImage(
+            imageReader, frameFilename,
+            bForceSize, (ImageCacheMode) cacheMode);
 
         if (!im)
             break;
Index: mythtv/libs/libmythui/mythuihelper.h
===================================================================
--- mythtv/libs/libmythui/mythuihelper.h	(revision 26104)
+++ mythtv/libs/libmythui/mythuihelper.h	(working copy)
@@ -23,8 +23,9 @@
 typedef enum ImageCacheMode
 {
     kCacheNormal          = 0x0,
-    kCacheIgnoreDisk,
-    kCacheCheckMemoryOnly
+    kCacheIgnoreDisk      = 0x1,
+    kCacheCheckMemoryOnly = 0x2,
+    kCacheForceStat       = 0x4,
 } ImageCacheMode;
 
 struct MPUBLIC MythUIMenuCallbacks
Index: mythtv/libs/libmythui/mythuihelper.cpp
===================================================================
--- mythtv/libs/libmythui/mythuihelper.cpp	(revision 26104)
+++ mythtv/libs/libmythui/mythuihelper.cpp	(working copy)
@@ -1393,18 +1393,21 @@
     if (srcfile.isEmpty() || label.isEmpty())
         return NULL;
 
-    // Some screens include certain images dozens or even hundreds of
-    // times.  Even if the image is in the cache, there is still a
-    // stat system call on the original file to see if it has changed.
-    // This code relaxes the original-file check so that the check
-    // isn't repeated if it was already done within kImageCacheTimeout
-    // seconds.
-    const uint kImageCacheTimeout = 5;
-    uint now = QDateTime::currentDateTime().toTime_t();
-    if (d->imageCache.contains(label) &&
-        d->CacheTrack[label] + kImageCacheTimeout > now)
+    if (!(kCacheForceStat & cacheMode))
     {
-        return d->imageCache[label];
+        // Some screens include certain images dozens or even hundreds of
+        // times.  Even if the image is in the cache, there is still a
+        // stat system call on the original file to see if it has changed.
+        // This code relaxes the original-file check so that the check
+        // isn't repeated if it was already done within kImageCacheTimeout
+        // seconds.
+        const uint kImageCacheTimeout = 5;
+        uint now = QDateTime::currentDateTime().toTime_t();
+        if (d->imageCache.contains(label) &&
+            d->CacheTrack[label] + kImageCacheTimeout > now)
+        {
+            return d->imageCache[label];
+        }
     }
 
     QString cachefilepath = GetThemeCacheDir() + '/' + label;
@@ -1412,10 +1415,10 @@
 
     MythImage *ret = NULL;
 
-    if ((cacheMode == kCacheIgnoreDisk) || fi.exists())
+    if (!!(cacheMode & kCacheIgnoreDisk) || fi.exists())
     {
         // Now compare the time on the source versus our cached copy
-        if (cacheMode != kCacheIgnoreDisk)
+        if (!(cacheMode & kCacheIgnoreDisk))
             FindThemeFile(srcfile);
 
         QDateTime srcLastModified;
@@ -1433,12 +1436,12 @@
         else if (original.exists())
             srcLastModified = original.lastModified();
 
-        if ((cacheMode == kCacheIgnoreDisk) ||
+        if (!!(cacheMode & kCacheIgnoreDisk) ||
             (fi.lastModified() > srcLastModified))
         {
             // Check Memory Cache
             ret = GetImageFromCache(label);
-            if (!ret && (cacheMode == kCacheNormal))
+            if (!ret && (!!(kCacheNormal & cacheMode)))
             {
                 // Load file from disk cache to memory cache
                 ret = GetMythPainter()->GetFormatImage();
Index: mythtv/libs/libmythui/mythuiimage.h
===================================================================
--- mythtv/libs/libmythui/mythuiimage.h	(revision 26104)
+++ mythtv/libs/libmythui/mythuiimage.h	(working copy)
@@ -51,7 +51,7 @@
     void SetDelays(QVector<int> delays);
 
     void Reset(void);
-    bool Load(bool allowLoadInBackground = true);
+    bool Load(bool allowLoadInBackground = true, bool forceStat = false);
 
     bool IsGradient(void) const { return m_gradient; }
 
@@ -66,9 +66,9 @@
     void Init(void);
     void Clear(void);
     MythImage* LoadImage(MythImageReader &imageReader, const QString &imFile,
-                         QSize bForceSize);
+                         QSize bForceSize, int cacheMode);
     bool LoadAnimatedImage(MythImageReader &imageReader, const QString &imFile,
-                           QSize bForceSize);
+                           QSize bForceSize, int mode);
     void customEvent(QEvent *event);
 
     virtual bool ParseElement(
Index: mythtv/libs/libmyth/remoteutil.h
===================================================================
--- mythtv/libs/libmyth/remoteutil.h	(revision 26104)
+++ mythtv/libs/libmyth/remoteutil.h	(working copy)
@@ -53,7 +53,6 @@
 MPUBLIC void RemoteSendMessage(const QString &message);
 MPUBLIC void RemoteSendEvent(const MythEvent &event);
 MPUBLIC vector<uint> RemoteRequestFreeRecorderList(void);
-MPUBLIC void RemoteGeneratePreviewPixmap(const ProgramInfo *pginfo);
 MPUBLIC QDateTime RemoteGetPreviewLastModified(const ProgramInfo *pginfo);
 MPUBLIC QDateTime RemoteGetPreviewIfModified(
     const ProgramInfo &pginfo, const QString &cachefile);
Index: mythtv/libs/libmyth/remoteutil.cpp
===================================================================
--- mythtv/libs/libmyth/remoteutil.cpp	(revision 26104)
+++ mythtv/libs/libmyth/remoteutil.cpp	(working copy)
@@ -278,14 +278,6 @@
     gCoreContext->SendReceiveStringList(strlist);
 }
 
-void RemoteGeneratePreviewPixmap(const ProgramInfo *pginfo)
-{
-    QStringList strlist( "QUERY_GENPIXMAP" );
-    pginfo->ToStringList(strlist);
-
-    gCoreContext->SendReceiveStringList(strlist);
-}
-    
 QDateTime RemoteGetPreviewLastModified(const ProgramInfo *pginfo)
 {
     QDateTime retdatetime;
@@ -354,7 +346,7 @@
         return retdatetime;
     }
 
-    size_t  length     = strlist[1].toLongLong();
+    size_t  length     = strlist[1].toULongLong();
     quint16 checksum16 = strlist[2].toUInt();
     QByteArray data = QByteArray::fromBase64(strlist[3].toAscii());
     if ((size_t) data.size() < length)
@@ -364,6 +356,7 @@
                 .arg(data.size()).arg(length));
         return QDateTime();
     }
+    data.resize(length);
 
     if (checksum16 != qChecksum(data.constData(), data.size()))
     {
Index: mythtv/libs/libmythdb/mythversion.h
===================================================================
--- mythtv/libs/libmythdb/mythversion.h	(revision 26104)
+++ mythtv/libs/libmythdb/mythversion.h	(working copy)
@@ -11,7 +11,7 @@
 /// Update this whenever the plug-in API changes.
 /// Including changes in the libmythdb, libmyth, libmythtv, libmythav* and
 /// libmythui class methods used by plug-ins.
-#define MYTH_BINARY_VERSION "0.23.20100903-1"
+#define MYTH_BINARY_VERSION "0.23.20100903-2"
 
 /** \brief Increment this whenever the MythTV network protocol changes.
  *
@@ -30,7 +30,7 @@
  *       mythtv/bindings/python/MythTV/static.py (version number)
  *       mythtv/bindings/python/MythTV/mythproto.py (layout)
  */
-#define MYTH_PROTO_VERSION "60"
+#define MYTH_PROTO_VERSION "61"
 
 MPUBLIC const char *GetMythSourceVersion();
 
Index: mythtv/programs/mythfrontend/playbackbox.cpp
===================================================================
--- mythtv/programs/mythfrontend/playbackbox.cpp	(revision 26104)
+++ mythtv/programs/mythfrontend/playbackbox.cpp	(working copy)
@@ -8,43 +8,38 @@
 #include <QTimer>
 #include <QMap>
 
-// libmythdb
-#include "oldsettings.h"
-#include "mythdb.h"
-#include "mythdbcon.h"
-#include "mythverbose.h"
-#include "mythdirs.h"
-
-// libmythtv
-#include "tv.h"
-#include "mythplayer.h"
+// MythTV
+#include "previewgeneratorqueue.h"
+#include "mythuiprogressbar.h"
+#include "mythuibuttonlist.h"
+#include "mythcorecontext.h"
+#include "mythsystemevent.h"
+#include "mythuistatetype.h"
+#include "mythuicheckbox.h"
+#include "mythuitextedit.h"
+#include "mythdialogbox.h"
 #include "recordinginfo.h"
-#include "playgroup.h"
-#include "mythsystemevent.h"
-
-// libmyth
-#include "mythcorecontext.h"
-#include "util.h"
+#include "mythuihelper.h"
 #include "storagegroup.h"
+#include "mythuibutton.h"
+#include "mythverbose.h"
+#include "mythuiimage.h"
 #include "programinfo.h"
-
-// libmythui
-#include "mythuihelper.h"
+#include "oldsettings.h"
+#include "mythplayer.h"
 #include "mythuitext.h"
-#include "mythuibutton.h"
-#include "mythuibuttonlist.h"
-#include "mythuistatetype.h"
-#include "mythdialogbox.h"
-#include "mythuitextedit.h"
-#include "mythuiimage.h"
-#include "mythuicheckbox.h"
-#include "mythuiprogressbar.h"
-
 #include "remoteutil.h"
+#include "mythdbcon.h"
+#include "playgroup.h"
+#include "mythdirs.h"
+#include "mythdb.h"
+#include "util.h"
+#include "tv.h"
 
 //  Mythfrontend
 #include "playbackboxlistitem.h"
 #include "customedit.h"
+#include "proglist.h"
 
 #define LOC      QString("PlaybackBox: ")
 #define LOC_WARN QString("PlaybackBox Warning: ")
@@ -444,6 +439,7 @@
 PlaybackBox::~PlaybackBox(void)
 {
     gCoreContext->removeListener(this);
+    PreviewGeneratorQueue::RemoveListener(this);
 
     for (uint i = 0; i < sizeof(m_artImage) / sizeof(MythUIImage*); i++)
     {
@@ -514,6 +510,7 @@
 void PlaybackBox::Load(void)
 {
     m_programInfoCache.WaitForLoadToComplete();
+    PreviewGeneratorQueue::AddListener(this);
 }
 
 void PlaybackBox::Init()
@@ -787,7 +784,32 @@
 
     QString oldimgfile = item->GetImage("preview");
     if (oldimgfile.isEmpty() || force_preview_reload)
-        m_helper.GetPreviewImage(*pginfo);
+        m_preview_tokens.insert(m_helper.GetPreviewImage(*pginfo));
+    MythUIButtonList *p = m_recordingList; //item->parent();
+    int top = p->GetTopItemPos();
+    VERBOSE(VB_IMPORTANT,
+            QString("top: %1, visible: %2, count %3, cur %4")
+            .arg(p->GetTopItemPos())
+            .arg(p->GetVisibleCount())
+            .arg(p->GetCount())
+            .arg(p->GetCurrentPos()));
+    for (int i = top; i>=0 && i < top+p->GetVisibleCount();)
+    {
+        MythUIButtonListItem *cur = p->GetItemAt(i);
+        VERBOSE(VB_IMPORTANT, QString("Checking %1 ptr 0x%2")
+                .arg(i).arg((intptr_t)cur,0,16));
+        if (cur != item)
+        {
+            ProgramInfo *curpi = qVariantValue<ProgramInfo *>(cur->GetData());
+            if (curpi && cur->GetImage("preview").isEmpty())
+                m_preview_tokens.insert(m_helper.GetPreviewImage(*curpi));
+        }
+        i = (i + 1) % p->GetCount();
+        if (i==top)
+            break;
+    }
+    if (oldimgfile.isEmpty() || force_preview_reload)
+        m_preview_tokens.insert(m_helper.GetPreviewImage(*pginfo));
 
     if ((GetFocusWidget() == m_recordingList) && is_sel)
     {
@@ -811,7 +833,7 @@
         if (m_previewImage)
         {
             m_previewImage->SetFilename(oldimgfile);
-            m_previewImage->Load();
+            m_previewImage->Load(true, true);
         }
 
         // Handle artwork
@@ -853,11 +875,53 @@
  *  a preview image UI item in the theme that it's filename property
  *  gets updated as well.
  */
-void PlaybackBox::HandlePreviewEvent(
-    const QString &piKey, const QString &previewFile)
+void PlaybackBox::HandlePreviewEvent(const QStringList &list)
 {
+    if (list.size() < 5)
+    {
+        VERBOSE(VB_IMPORTANT, "HandlePreviewEvent() -- too few args");
+        for (uint i = 0; i < (uint) list.size(); i++)
+        {
+            VERBOSE(VB_IMPORTANT, QString("%1: %2")
+                    .arg(i).arg(list[i]));
+        }
+        return;
+    }
+
+    const QString piKey       = list[0];
+    const QString previewFile = list[1];
+    const QString message     = list[2];
+
+    VERBOSE(VB_IMPORTANT, "HandlePreviewEvent()");
+
+    bool found = false;
+    for (uint i = 4; i < (uint) list.size(); i++)
+    {
+        QString token = list[i];
+        QSet<QString>::iterator it = m_preview_tokens.find(token);
+        if (it != m_preview_tokens.end())
+        {
+            found = true;
+            m_preview_tokens.erase(it);
+        }
+    }
+
+    if (!found)
+    {
+        QString tokens("\n\t\t\ttokens: ");
+        for (uint i = 4; i < (uint) list.size(); i++)
+            tokens += list[i] + ", ";
+        VERBOSE(VB_IMPORTANT, LOC +
+                "Ignoring PREVIEW_SUCCESS, no matcing token" + tokens);
+        return;
+    }
+
     if (previewFile.isEmpty())
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR +
+                "Ignoring PREVIEW_SUCCESS, no preview file.");
         return;
+    }
 
     ProgramInfo *info = m_programInfoCache.GetProgramInfo(piKey);
     MythUIButtonListItem *item = NULL;
@@ -865,8 +929,18 @@
     if (info)
         item = m_recordingList->GetItemByData(qVariantFromValue(info));
 
+    if (!item)
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR +
+                "Ignoring PREVIEW_SUCCESS, item no longer on screen.");
+    }
+
     if (item)
     {
+        VERBOSE(VB_GUI, LOC +
+                QString("Loading preview %1,\n\t\t\tmsg %2")
+                .arg(previewFile).arg(message));
+
         item->SetImage(previewFile, "preview", true);
 
         if ((GetFocusWidget() == m_recordingList) &&
@@ -874,7 +948,7 @@
             m_previewImage)
         {
             m_previewImage->SetFilename(previewFile);
-            m_previewImage->Load();
+            m_previewImage->Load(true, true);
         }
     }
 }
@@ -2249,6 +2323,15 @@
         QCoreApplication::postEvent(
             this, new MythEvent("PLAY_PLAYLIST"));
     }
+    else
+    {
+        // User may have saved or deleted a bookmark,
+        // requiring update of preview..
+        ProgramInfo *pginfo = m_programInfoCache.GetProgramInfo(
+            tvrec.GetChanID(), tvrec.GetRecordingStartTime());
+        if (pginfo)
+            UpdateUIListItem(pginfo, true);
+    }
 
     return playCompleted;
 }
@@ -3793,10 +3876,20 @@
             // asPendingDelete, we need to put them back now..
             ScheduleUpdateUIList();
         }
-        else if (message == "PREVIEW_READY" && me->ExtraDataCount() == 2)
+        else if (message == "PREVIEW_SUCCESS")
         {
-            HandlePreviewEvent(me->ExtraData(0), me->ExtraData(1));
+            HandlePreviewEvent(me->ExtraDataList());
         }
+        else if (message == "PREVIEW_FAILED" && me->ExtraDataCount() >= 5)
+        {
+            for (uint i = 4; i < (uint) me->ExtraDataCount(); i++)
+            {
+                QString token = me->ExtraData(i);
+                QSet<QString>::iterator it = m_preview_tokens.find(token);
+                if (it != m_preview_tokens.end())
+                    m_preview_tokens.erase(it);
+            }
+        }
         else if (message == "AVAILABILITY" && me->ExtraDataCount() == 8)
         {
             const uint kMaxUIWaitTime = 100; // ms
@@ -4013,7 +4106,7 @@
     {
         ProgramInfo *dst = FindProgramInUILists(evinfo);
         if (dst)
-            UpdateUIListItem(dst, false);
+            UpdateUIListItem(dst, true);
         return;
     }
 
Index: mythtv/programs/mythfrontend/playbackboxhelper.h
===================================================================
--- mythtv/programs/mythfrontend/playbackboxhelper.h	(revision 26104)
+++ mythtv/programs/mythfrontend/playbackboxhelper.h	(working copy)
@@ -17,25 +17,6 @@
 class QObject;
 class QTimer;
 
-class PreviewGenState
-{
-  public:
-    PreviewGenState() :
-        gen(NULL), genStarted(false), ready(false),
-        attempts(0), lastBlockTime(0) {}
-    PreviewGenerator *gen;
-    bool              genStarted;
-    bool              ready;
-    uint              attempts;
-    uint              lastBlockTime;
-    QDateTime         blockRetryUntil;
-
-    static const uint maxAttempts;
-    static const uint minBlockSeconds;
-};
-typedef QMap<QString,PreviewGenState> PreviewMap;
-typedef QMap<QString,QDateTime>       FileTimeStampMap;
-
 typedef enum CheckAvailabilityType {
     kCheckForCache,
     kCheckForMenuAction,
@@ -72,7 +53,7 @@
     void UndeleteRecording(uint chanid, const QDateTime &recstartts);
     void CheckAvailability(const ProgramInfo&,
                            CheckAvailabilityType cat = kCheckForCache);
-    void GetPreviewImage(const ProgramInfo&);
+    QString GetPreviewImage(const ProgramInfo&);
 
     QString LocateArtwork(const QString &seriesid, const QString &title,
                           ArtworkType, const QString &host,
@@ -83,21 +64,9 @@
     uint64_t GetFreeSpaceTotalMB(void) const;
     uint64_t GetFreeSpaceUsedMB(void) const;
 
-  private slots:
-    void previewThreadDone(const QString &fn, bool &success);
-    void previewReady(const ProgramInfo *pginfo);
-
   private:
     void UpdateFreeSpace(void);
 
-    QString GeneratePreviewImage(ProgramInfo &pginfo);
-    bool SetPreviewGenerator(const QString &fn, PreviewGenerator *g);
-    void IncPreviewGeneratorPriority(const QString &fn);
-    void UpdatePreviewGeneratorThreads(void);
-    bool IsGeneratingPreview(const QString &fn, bool really = false) const;
-    uint IncPreviewGeneratorAttempts(const QString &fn);
-    void ClearPreviewGeneratorAttempts(const QString &fn);
-
   private:
     QObject            *m_listener;
     PBHEventHandler    *m_eventHandler;
@@ -107,16 +76,6 @@
     uint64_t            m_freeSpaceTotalMB;
     uint64_t            m_freeSpaceUsedMB;
 
-    // Preview Pixmap Variables ///////////////////////////////////////////////
-    mutable QMutex      m_previewGeneratorLock;
-    uint                m_previewGeneratorMode;
-    FileTimeStampMap    m_previewFileTS;
-    bool                m_previewSuspend;
-    PreviewMap          m_previewGenerator;
-    QStringList         m_previewGeneratorQueue;
-    uint                m_previewGeneratorRunning;
-    uint                m_previewGeneratorMaxThreads;
-
     // Artwork Variables //////////////////////////////////////////////////////
     QHash<QString, QString>    m_artworkFilenameCache;
 };
Index: mythtv/programs/mythfrontend/playbackboxlistitem.cpp
===================================================================
--- mythtv/programs/mythfrontend/playbackboxlistitem.cpp	(revision 26104)
+++ mythtv/programs/mythfrontend/playbackboxlistitem.cpp	(working copy)
@@ -9,7 +9,7 @@
     pbbox(parent), needs_update(true)
 {
 }
-
+/*
 void PlaybackBoxListItem::SetToRealButton(
     MythUIStateType *button, bool selected)
 {
@@ -20,3 +20,4 @@
     }
     MythUIButtonListItem::SetToRealButton(button, selected);
 }
+*/
Index: mythtv/programs/mythfrontend/playbackboxhelper.cpp
===================================================================
--- mythtv/programs/mythfrontend/playbackboxhelper.cpp	(revision 26104)
+++ mythtv/programs/mythfrontend/playbackboxhelper.cpp	(working copy)
@@ -7,8 +7,8 @@
 #include <QFileInfo>
 #include <QDir>
 
+#include "previewgeneratorqueue.h"
 #include "playbackboxhelper.h"
-#include "previewgenerator.h"
 #include "mythcorecontext.h"
 #include "tvremoteutil.h"
 #include "storagegroup.h"
@@ -277,25 +277,21 @@
         }
         else if (me->Message() == "GET_PREVIEW")
         {
-            ProgramInfo evinfo(me->ExtraDataList());
+            QString token = me->ExtraData(0);
+            QStringList list = me->ExtraDataList();
+            QStringList::const_iterator it = list.begin()+1;
+            ProgramInfo evinfo(it, list.end());
             if (!evinfo.HasPathname())
                 return true;
 
-            QStringList list;
+            list.clear();
             evinfo.ToStringList(list);
             list += QString::number(kCheckForCache);
             if (asAvailable != CheckAvailability(list))
                 return true;
 
-            QString fn = m_pbh.GeneratePreviewImage(evinfo);
-            if (!fn.isEmpty())
-            {
-                QStringList list;
-                list.push_back(evinfo.MakeUniqueKey());
-                list.push_back(fn);
-                MythEvent *e = new MythEvent("PREVIEW_READY", list);
-                QCoreApplication::postEvent(m_pbh.m_listener, e);
-            }
+            // Now we can actually request the preview...
+            PreviewGeneratorQueue::GetPreviewImage(evinfo, token);
 
             return true;
         }
@@ -458,22 +454,11 @@
 
 //////////////////////////////////////////////////////////////////////
 
-const uint PreviewGenState::maxAttempts     = 5;
-const uint PreviewGenState::minBlockSeconds = 60;
-
 PlaybackBoxHelper::PlaybackBoxHelper(QObject *listener) :
     m_listener(listener), m_eventHandler(NULL),
     // Free Space Tracking Variables
-    m_freeSpaceTotalMB(0ULL), m_freeSpaceUsedMB(0ULL),
-    // Preview Image Variables
-    m_previewGeneratorRunning(0), m_previewGeneratorMaxThreads(2)
+    m_freeSpaceTotalMB(0ULL), m_freeSpaceUsedMB(0ULL)
 {
-    m_previewGeneratorMode = PreviewGenerator::kRemote;
-
-    int idealThreads = QThread::idealThreadCount();
-    if (idealThreads >= 1)
-        m_previewGeneratorMaxThreads = idealThreads * 2;
-
     start();
 }
 
@@ -482,15 +467,6 @@
     exit();
     wait();
 
-    // disconnect preview generators
-    QMutexLocker locker(&m_previewGeneratorLock);
-    PreviewMap::iterator it = m_previewGenerator.begin();
-    for (;it != m_previewGenerator.end(); ++it)
-    {
-        if ((*it).gen)
-            (*it).gen->disconnectSafe();
-    }
-
     // delete the event handler
     delete m_eventHandler;
     m_eventHandler = NULL;
@@ -620,305 +596,18 @@
     return QString();
 }
 
-void PlaybackBoxHelper::GetPreviewImage(const ProgramInfo &pginfo)
+QString PlaybackBoxHelper::GetPreviewImage(const ProgramInfo &pginfo)
 {
-    QStringList extra;
-    pginfo.ToStringList(extra);
-    MythEvent *e = new MythEvent("GET_PREVIEW", extra);
-    QCoreApplication::postEvent(m_eventHandler, e);
-}
-
-QString PlaybackBoxHelper::GeneratePreviewImage(ProgramInfo &pginfo)
-{
     if (pginfo.GetAvailableStatus() == asPendingDelete)
         return QString();
 
-    QString filename = pginfo.GetPathname() + ".png";
+    QString token = QString("%1:%2")
+        .arg(pginfo.MakeUniqueKey()).arg(rand());
 
-    // If someone is asking for this preview it must be on screen
-    // and hence higher priority than anything else we may have
-    // queued up recently....
-    IncPreviewGeneratorPriority(filename);
+    QStringList extra(token);
+    pginfo.ToStringList(extra);
+    MythEvent *e = new MythEvent("GET_PREVIEW", extra);
+    QCoreApplication::postEvent(m_eventHandler, e);
 
-    QDateTime previewLastModified;
-    QString ret_file = filename;
-    bool streaming = filename.left(1) != "/";
-    bool locally_accessible = false;
-    bool bookmark_updated = false;
-
-    QDateTime bookmark_ts = pginfo.QueryBookmarkTimeStamp();
-    QDateTime cmp_ts = bookmark_ts.isValid() ?
-        bookmark_ts : pginfo.GetLastModifiedTime();
-
-    if (streaming)
-    {
-        ret_file = QString("%1/remotecache/%2")
-            .arg(GetConfDir()).arg(filename.section('/', -1));
-
-        QFileInfo finfo(ret_file);
-        if (finfo.isReadable() && finfo.lastModified() >= cmp_ts)
-        {
-            // This is just an optimization to avoid
-            // hitting the backend if our cached copy
-            // is newer than the bookmark, or if we have
-            // a preview and do not update it when the
-            // bookmark changes.
-            previewLastModified = finfo.lastModified();
-        }
-        else if (!IsGeneratingPreview(filename))
-        {
-            previewLastModified =
-                RemoteGetPreviewIfModified(pginfo, ret_file);
-        }
-    }
-    else
-    {
-        QFileInfo fi(filename);
-        if ((locally_accessible = fi.isReadable()))
-            previewLastModified = fi.lastModified();
-    }
-
-    bookmark_updated =
-        (!previewLastModified.isValid() || (previewLastModified < cmp_ts));
-
-    if (bookmark_updated && bookmark_ts.isValid() &&
-        previewLastModified.isValid())
-    {
-        ClearPreviewGeneratorAttempts(filename);
-    }
-
-    if (0)
-    {
-        VERBOSE(VB_IMPORTANT, QString(
-                    "previewLastModified:  %1\n\t\t\t"
-                    "bookmark_ts:          %2\n\t\t\t"
-                    "pginfo.lastmodified: %3")
-                .arg(previewLastModified.toString(Qt::ISODate))
-                .arg(bookmark_ts.toString(Qt::ISODate))
-                .arg(pginfo.GetLastModifiedTime(ISODate)));
-    }
-
-    bool preview_exists = previewLastModified.isValid();
-
-    if (0)
-    {
-        VERBOSE(VB_IMPORTANT,
-                QString("Title: %1\n\t\t\t")
-                .arg(pginfo.toString(ProgramInfo::kTitleSubtitle)) +
-                QString("File  '%1' \n\t\t\tCache '%2'")
-                .arg(filename).arg(ret_file) +
-                QString("\n\t\t\tPreview Exists: %1, "
-                        "Bookmark Updated: %2, "
-                        "Need Preview: %3")
-                .arg(preview_exists).arg(bookmark_updated)
-                .arg((bookmark_updated || !preview_exists)));
-    }
-
-    if ((bookmark_updated || !preview_exists) &&
-        !IsGeneratingPreview(filename))
-    {
-        uint attempts = IncPreviewGeneratorAttempts(filename);
-        if (attempts < PreviewGenState::maxAttempts)
-        {
-            VERBOSE(VB_PLAYBACK, LOC +
-                    QString("Requesting preview for '%1'")
-                    .arg(filename));
-            PreviewGenerator::Mode mode =
-                (PreviewGenerator::Mode) m_previewGeneratorMode;
-            PreviewGenerator *pg = new PreviewGenerator(&pginfo, mode);
-            while (!SetPreviewGenerator(filename, pg)) usleep(50000);
-            VERBOSE(VB_PLAYBACK, LOC +
-                    QString("Requested preview for '%1'")
-                    .arg(filename));
-        }
-        else if (attempts == PreviewGenState::maxAttempts)
-        {
-            VERBOSE(VB_IMPORTANT, LOC_ERR +
-                    QString("Attempted to generate preview for '%1' "
-                            "%2 times, giving up.")
-                    .arg(filename).arg(PreviewGenState::maxAttempts));
-        }
-    }
-    else if (bookmark_updated || !preview_exists)
-    {
-        VERBOSE(VB_PLAYBACK, LOC +
-                "Not requesting preview as it "
-                "is already being generated");
-    }
-
-    UpdatePreviewGeneratorThreads();
-
-    QString ret = (locally_accessible) ?
-        filename : (previewLastModified.isValid()) ?
-        ret_file : (QFileInfo(ret_file).isReadable()) ?
-        ret_file : QString();
-
-    //VERBOSE(VB_IMPORTANT, QString("Returning: '%1'").arg(ret));
-
-    return ret;
+    return token;
 }
-
-void PlaybackBoxHelper::IncPreviewGeneratorPriority(const QString &xfn)
-{
-    QString fn = xfn.mid(max(xfn.lastIndexOf('/') + 1,0));
-
-    QMutexLocker locker(&m_previewGeneratorLock);
-    m_previewGeneratorQueue.removeAll(fn);
-
-    PreviewMap::iterator pit = m_previewGenerator.find(fn);
-    if (pit != m_previewGenerator.end() && (*pit).gen && !(*pit).genStarted)
-        m_previewGeneratorQueue.push_back(fn);
-}
-
-void PlaybackBoxHelper::UpdatePreviewGeneratorThreads(void)
-{
-    QMutexLocker locker(&m_previewGeneratorLock);
-    QStringList &q = m_previewGeneratorQueue;
-    if (!q.empty() &&
-        (m_previewGeneratorRunning < m_previewGeneratorMaxThreads))
-    {
-        QString fn = q.back();
-        q.pop_back();
-        PreviewMap::iterator it = m_previewGenerator.find(fn);
-        if (it != m_previewGenerator.end() && (*it).gen && !(*it).genStarted)
-        {
-            m_previewGeneratorRunning++;
-            (*it).gen->Start();
-            (*it).genStarted = true;
-        }
-    }
-}
-
-/** \fn PlaybackBoxHelper::SetPreviewGenerator(const QString&, PreviewGenerator*)
- *  \brief Sets the PreviewGenerator for a specific file.
- *  \return true iff call succeeded.
- */
-bool PlaybackBoxHelper::SetPreviewGenerator(const QString &xfn, PreviewGenerator *g)
-{
-    QString fn = xfn.mid(max(xfn.lastIndexOf('/') + 1,0));
-
-    if (!m_previewGeneratorLock.tryLock())
-        return false;
-
-    if (!g)
-    {
-        m_previewGeneratorRunning = max(0, (int)m_previewGeneratorRunning - 1);
-        PreviewMap::iterator it = m_previewGenerator.find(fn);
-        if (it == m_previewGenerator.end())
-        {
-            m_previewGeneratorLock.unlock();
-            return false;
-        }
-
-        (*it).gen        = NULL;
-        (*it).genStarted = false;
-        (*it).ready      = false;
-        (*it).lastBlockTime =
-            max(PreviewGenState::minBlockSeconds, (*it).lastBlockTime * 2);
-        (*it).blockRetryUntil =
-            QDateTime::currentDateTime().addSecs((*it).lastBlockTime);
-
-        m_previewGeneratorLock.unlock();
-        return true;
-    }
-
-    g->AttachSignals(this);
-    m_previewGenerator[fn].gen = g;
-    m_previewGenerator[fn].genStarted = false;
-    m_previewGenerator[fn].ready = false;
-
-    m_previewGeneratorLock.unlock();
-    IncPreviewGeneratorPriority(xfn);
-
-    return true;
-}
-
-/** \fn PlaybackBoxHelper::IsGeneratingPreview(const QString&, bool) const
- *  \brief Returns true if we have already started a
- *         PreviewGenerator to create this file.
- */
-bool PlaybackBoxHelper::IsGeneratingPreview(const QString &xfn, bool really) const
-{
-    PreviewMap::const_iterator it;
-    QMutexLocker locker(&m_previewGeneratorLock);
-
-    QString fn = xfn.mid(max(xfn.lastIndexOf('/') + 1,0));
-    if ((it = m_previewGenerator.find(fn)) == m_previewGenerator.end())
-        return false;
-
-    if (really)
-        return ((*it).gen && !(*it).ready);
-
-    if ((*it).blockRetryUntil.isValid())
-        return QDateTime::currentDateTime() < (*it).blockRetryUntil;
-
-    return (*it).gen;
-}
-
-/** \fn PlaybackBoxHelper::IncPreviewGeneratorAttempts(const QString&)
- *  \brief Increments and returns number of times we have
- *         started a PreviewGenerator to create this file.
- */
-uint PlaybackBoxHelper::IncPreviewGeneratorAttempts(const QString &xfn)
-{
-    QMutexLocker locker(&m_previewGeneratorLock);
-    QString fn = xfn.mid(max(xfn.lastIndexOf('/') + 1,0));
-    return m_previewGenerator[fn].attempts++;
-}
-
-/** \fn PlaybackBoxHelper::ClearPreviewGeneratorAttempts(const QString&)
- *  \brief Clears the number of times we have
- *         started a PreviewGenerator to create this file.
- */
-void PlaybackBoxHelper::ClearPreviewGeneratorAttempts(const QString &xfn)
-{
-    QMutexLocker locker(&m_previewGeneratorLock);
-    QString fn = xfn.mid(max(xfn.lastIndexOf('/') + 1,0));
-    m_previewGenerator[fn].attempts = 0;
-    m_previewGenerator[fn].lastBlockTime = 0;
-    m_previewGenerator[fn].blockRetryUntil =
-        QDateTime::currentDateTime().addSecs(-60);
-}
-
-void PlaybackBoxHelper::previewThreadDone(const QString &fn, bool &success)
-{
-    VERBOSE(VB_PLAYBACK, LOC + QString("Preview for '%1' done").arg(fn));
-    success = SetPreviewGenerator(fn, NULL);
-    UpdatePreviewGeneratorThreads();
-}
-
-/** \fn PlaybackBoxHelper::previewReady(const ProgramInfo*)
- *  \brief Callback used by PreviewGenerator to tell us a m_preview
- *         we requested has been returned from the backend.
- *  \param pginfo ProgramInfo describing the previewed recording.
- */
-void PlaybackBoxHelper::previewReady(const ProgramInfo *pginfo)
-{
-    if (!pginfo)
-        return;
-
-    QString xfn = pginfo->GetPathname() + ".png";
-    QString fn = xfn.mid(max(xfn.lastIndexOf('/') + 1,0));
-
-    VERBOSE(VB_PLAYBACK, LOC + QString("Preview for '%1' ready")
-            .arg(pginfo->GetPathname()));
-
-    m_previewGeneratorLock.lock();
-    PreviewMap::iterator it = m_previewGenerator.find(fn);
-    if (it != m_previewGenerator.end())
-    {
-        (*it).ready         = true;
-        (*it).attempts      = 0;
-        (*it).lastBlockTime = 0;
-    }
-    m_previewGeneratorLock.unlock();
-
-    if (pginfo)
-    {
-        QStringList list;
-        list.push_back(pginfo->MakeUniqueKey());
-        list.push_back(xfn);
-        MythEvent *e = new MythEvent("PREVIEW_READY", list);
-        QCoreApplication::postEvent(m_listener, e);
-    }
-}
Index: mythtv/programs/mythfrontend/playbackboxlistitem.h
===================================================================
--- mythtv/programs/mythfrontend/playbackboxlistitem.h	(revision 26104)
+++ mythtv/programs/mythfrontend/playbackboxlistitem.h	(working copy)
@@ -14,7 +14,7 @@
   public:
     PlaybackBoxListItem(PlaybackBox *parent, MythUIButtonList *lbtype, ProgramInfo *pi);
 
-    virtual void SetToRealButton(MythUIStateType *button, bool selected);
+//    virtual void SetToRealButton(MythUIStateType *button, bool selected);
 
   private:
     PlaybackBox *pbbox;
Index: mythtv/programs/mythfrontend/main.cpp
===================================================================
--- mythtv/programs/mythfrontend/main.cpp	(revision 26104)
+++ mythtv/programs/mythfrontend/main.cpp	(working copy)
@@ -19,6 +19,7 @@
 #include <QApplication>
 #include <QTimer>
 
+#include "previewgeneratorqueue.h"
 #include "mythconfig.h"
 #include "tv.h"
 #include "proglist.h"
@@ -1458,8 +1459,13 @@
 
     BackendConnectionManager bcm;
 
+    PreviewGeneratorQueue::CreatePreviewGeneratorQueue(
+        PreviewGenerator::kRemote, 50, 60);
+
     int ret = qApp->exec();
 
+    PreviewGeneratorQueue::TeardownPreviewGeneratorQueue();
+
     delete sysEventHandler;
 
     pmanager->DestroyAllPlugins();
Index: mythtv/programs/mythfrontend/playbackbox.h
===================================================================
--- mythtv/programs/mythfrontend/playbackbox.h	(revision 26104)
+++ mythtv/programs/mythfrontend/playbackbox.h	(working copy)
@@ -16,6 +16,7 @@
 #include <QObject>
 #include <QMutex>
 #include <QMap>
+#include <QSet>
 
 #include "jobqueue.h"
 #include "tv_play.h"
@@ -315,9 +316,9 @@
         ProgramInfo *ProgramInfo_pointer_from_FindProgramInUILists,
         bool force_preview_reload);
     void UpdateUIListItem(MythUIButtonListItem *item, bool is_sel,
-                          bool force_preview_reload = false);
+                          bool force_preview_reload = true);
 
-    void HandlePreviewEvent(const QString &piKey, const QString &previewFile);
+    void HandlePreviewEvent(const QStringList &list);
     void HandleRecordingRemoveEvent(uint chanid, const QDateTime &recstartts);
     void HandleRecordingAddEvent(const ProgramInfo &evinfo);
     void HandleUpdateProgramInfoEvent(const ProgramInfo &evinfo);
@@ -442,6 +443,8 @@
     QStringList         m_player_selected_new_show;
     /// Main helper thread
     PlaybackBoxHelper   m_helper;
+    /// Outstanding preview image requests
+    QSet<QString>       m_preview_tokens;
 };
 
 class GroupSelector : public MythScreenType
Index: mythtv/programs/mythfrontend/networkcontrol.cpp
===================================================================
--- mythtv/programs/mythfrontend/networkcontrol.cpp	(revision 26104)
+++ mythtv/programs/mythfrontend/networkcontrol.cpp	(working copy)
@@ -1403,7 +1403,8 @@
 
         frameNumber = results[7].toLongLong();
 
-        PreviewGenerator *previewgen = new PreviewGenerator(&pginfo);
+        PreviewGenerator *previewgen = new PreviewGenerator(
+            &pginfo, QString(), PreviewGenerator::kLocal);
         previewgen->SetPreviewTimeAsFrameNumber(frameNumber);
         previewgen->SetOutputFilename(outFile);
         previewgen->SetOutputSize(QSize(width, height));
Index: mythtv/programs/mythbackend/mythxml.cpp
===================================================================
--- mythtv/programs/mythbackend/mythxml.cpp	(revision 26104)
+++ mythtv/programs/mythbackend/mythxml.cpp	(working copy)
@@ -1149,7 +1149,7 @@
             return;
 
         PreviewGenerator *previewgen = new PreviewGenerator(
-            &pginfo, PreviewGenerator::kLocal);
+            &pginfo, QString(), PreviewGenerator::kLocal);
         previewgen->SetPreviewTimeAsSeconds(nSecsIn);
         previewgen->SetOutputFilename(sPreviewFileName);
         bool ok = previewgen->Run();
Index: mythtv/programs/mythbackend/playbacksock.h
===================================================================
--- mythtv/programs/mythbackend/playbacksock.h	(revision 26104)
+++ mythtv/programs/mythbackend/playbacksock.h	(working copy)
@@ -70,8 +70,10 @@
     QStringList GetSGFileQuery(QString &host, QString &groupname,
                                QString &filename);
 
-    QStringList GenPreviewPixmap(const ProgramInfo *pginfo);
-    QStringList GenPreviewPixmap(const ProgramInfo *pginfo,
+    QStringList GenPreviewPixmap(const QString     &token,
+                                 const ProgramInfo *pginfo);
+    QStringList GenPreviewPixmap(const QString     &token,
+                                 const ProgramInfo *pginfo,
                                  bool               time_fmt_sec,
                                  long long          time,
                                  const QString     &outputFile,
Index: mythtv/programs/mythbackend/mainserver.h
===================================================================
--- mythtv/programs/mythbackend/mainserver.h	(revision 26104)
+++ mythtv/programs/mythbackend/mainserver.h	(working copy)
@@ -1,10 +1,11 @@
 #ifndef MAINSERVER_H_
 #define MAINSERVER_H_
 
-#include <QMap>
-#include <QMutex>
 #include <QReadWriteLock>
 #include <QEvent>
+#include <QMutex>
+#include <QHash>
+#include <QMap>
 
 #include <vector>
 using namespace std;
@@ -249,6 +250,9 @@
 
     int m_exitCode;
 
+    typedef QHash<QString,QString> RequestedBy;
+    RequestedBy                m_previewRequestedBy;
+
     static const uint kMasterServerReconnectTimeout;
 };
 
Index: mythtv/programs/mythbackend/main.cpp
===================================================================
--- mythtv/programs/mythbackend/main.cpp	(revision 26104)
+++ mythtv/programs/mythbackend/main.cpp	(working copy)
@@ -51,7 +51,6 @@
 #include "programinfo.h"
 #include "dbcheck.h"
 #include "jobqueue.h"
-#include "previewgenerator.h"
 #include "mythcommandlineparser.h"
 #include "mythsystemevent.h"
 
Index: mythtv/programs/mythbackend/playbacksock.cpp
===================================================================
--- mythtv/programs/mythbackend/playbacksock.cpp	(revision 26104)
+++ mythtv/programs/mythbackend/playbacksock.cpp	(working copy)
@@ -262,9 +262,11 @@
     return strlist;
 }
 
-QStringList PlaybackSock::GenPreviewPixmap(const ProgramInfo *pginfo)
+QStringList PlaybackSock::GenPreviewPixmap(
+    const QString &token, const ProgramInfo *pginfo)
 {
-    QStringList strlist( QString("QUERY_GENPIXMAP") );
+    QStringList strlist( QString("QUERY_GENPIXMAP2") );
+    strlist += token;
     pginfo->ToStringList(strlist);
 
     SendReceiveStringList(strlist);
@@ -272,13 +274,15 @@
     return strlist;
 }
 
-QStringList PlaybackSock::GenPreviewPixmap(const ProgramInfo *pginfo,
+QStringList PlaybackSock::GenPreviewPixmap(const QString &token,
+                                           const ProgramInfo *pginfo,
                                            bool               time_fmt_sec,
                                            long long          time,
                                            const QString     &outputFile,
                                            const QSize       &outputSize)
 {
-    QStringList strlist(QString("QUERY_GENPIXMAP"));
+    QStringList strlist(QString("QUERY_GENPIXMAP2"));
+    strlist += token;
     pginfo->ToStringList(strlist);
     strlist.push_back(time_fmt_sec ? "s" : "f");
     encodeLongLong(strlist, time);
Index: mythtv/programs/mythbackend/mainserver.cpp
===================================================================
--- mythtv/programs/mythbackend/mainserver.cpp	(revision 26104)
+++ mythtv/programs/mythbackend/mainserver.cpp	(working copy)
@@ -37,6 +37,7 @@
 #include <QTcpServer>
 #include <QTimer>
 
+#include "previewgeneratorqueue.h"
 #include "exitcodes.h"
 #include "mythcontext.h"
 #include "mythverbose.h"
@@ -53,7 +54,6 @@
 #include "scheduledrecording.h"
 #include "jobqueue.h"
 #include "autoexpire.h"
-#include "previewgenerator.h"
 #include "storagegroup.h"
 #include "compat.h"
 #include "RingBuffer.h"
@@ -184,6 +184,10 @@
 {
     AutoExpire::Update(true);
 
+    PreviewGeneratorQueue::CreatePreviewGeneratorQueue(
+        PreviewGenerator::kLocalAndRemote, ~0, 0);
+    PreviewGeneratorQueue::AddListener(this);
+
     for (int i = 0; i < PRT_STARTUP_THREAD_COUNT; i++)
     {
         ProcessRequestThread *prt = new ProcessRequestThread(this);
@@ -237,6 +241,9 @@
 
 MainServer::~MainServer()
 {
+    PreviewGeneratorQueue::RemoveListener(this);
+    PreviewGeneratorQueue::TeardownPreviewGeneratorQueue();
+
     if (mythserver)
     {
         mythserver->disconnect();
@@ -546,7 +553,7 @@
         else
             HandleFileTransferQuery(listline, tokens, pbs);
     }
-    else if (command == "QUERY_GENPIXMAP")
+    else if (command == "QUERY_GENPIXMAP2")
     {
         HandleGenPreviewPixmap(listline, pbs);
     }
@@ -729,11 +736,108 @@
 void MainServer::customEvent(QEvent *e)
 {
     QStringList broadcast;
+    QSet<QString> receivers;
 
     if ((MythEvent::Type)(e->type()) == MythEvent::MythEventMessage)
     {
         MythEvent *me = (MythEvent *)e;
 
+        QString message = me->Message();
+        QString error;
+        if ((message == "PREVIEW_SUCCESS" || message == "PREVIEW_QUEUED") &&
+            me->ExtraDataCount() >= 5)
+        {
+            bool ok = true;
+            QString pginfokey = me->ExtraData(0); // pginfo->MakeUniqueKey()
+            QString filename  = me->ExtraData(1); // outFileName
+            QString msg       = me->ExtraData(2);
+            QString datetime  = me->ExtraData(3);
+
+            if (message == "PREVIEW_QUEUED")
+            {
+                VERBOSE(VB_PLAYBACK, QString("Preview Queued: '%1' '%2'")
+                        .arg(pginfokey).arg(filename));
+                return;
+            }
+
+            QFile file(filename);
+            ok = ok && file.open(QIODevice::ReadOnly);
+
+            if (ok)
+            {
+                QByteArray data = file.readAll();
+                QStringList extra("OK");
+                extra.push_back(pginfokey);
+                extra.push_back(msg);
+                extra.push_back(datetime);
+                extra.push_back(QString::number(data.size()));
+                extra.push_back(
+                    QString::number(qChecksum(data.constData(), data.size())));
+                extra.push_back(QString(data.toBase64()));
+
+                for (uint i = 4 ; i < (uint) me->ExtraDataCount(); i++)
+                {
+                    QString token = me->ExtraData(i);
+                    extra.push_back(token);
+                    RequestedBy::iterator it = m_previewRequestedBy.find(token);
+                    if (it != m_previewRequestedBy.end())
+                    {
+                        receivers.insert(*it);
+                        m_previewRequestedBy.erase(it);
+                    }
+                }
+
+                if (receivers.empty())
+                {
+                    VERBOSE(VB_IMPORTANT, LOC_ERR +
+                            "PREVIEW_SUCCESS but no receivers.");
+                    return;
+                }
+
+                broadcast.push_back("BACKEND_MESSAGE");
+                broadcast.push_back("GENERATED_PIXMAP");
+                broadcast += extra;
+            }
+            else
+            {
+                message = "PREVIEW_FAILED";
+                error = QString("Failed to read '%1'").arg(filename);
+                VERBOSE(VB_IMPORTANT, LOC_ERR + error);
+            }
+        }
+
+        if (message == "PREVIEW_FAILED" && me->ExtraDataCount() >= 5)
+        {
+            QString pginfokey = me->ExtraData(0); // pginfo->MakeUniqueKey()
+            QString msg       = me->ExtraData(2);
+
+            QStringList extra("ERROR");
+            extra.push_back(pginfokey);
+            extra.push_back(msg);
+            for (uint i = 4 ; i < (uint) me->ExtraDataCount(); i++)
+            {
+                QString token = me->ExtraData(i);
+                extra.push_back(token);
+                RequestedBy::iterator it = m_previewRequestedBy.find(token);
+                if (it != m_previewRequestedBy.end())
+                {
+                    receivers.insert(*it);
+                    m_previewRequestedBy.erase(it);
+                }
+            }
+
+            if (receivers.empty())
+            {
+                VERBOSE(VB_IMPORTANT, LOC_ERR +
+                        "PREVIEW_FAILED but no receivers.");
+                return;
+            }
+
+            broadcast.push_back("BACKEND_MESSAGE");
+            broadcast.push_back("GENERATED_PIXMAP");
+            broadcast += extra;
+        }
+
         if (me->Message().left(11) == "AUTO_EXPIRE")
         {
             QStringList tokens = me->Message()
@@ -945,9 +1049,12 @@
             me = &mod_me;
         }
 
-        broadcast = QStringList( "BACKEND_MESSAGE" );
-        broadcast << me->Message();
-        broadcast += me->ExtraDataList();
+        if (broadcast.empty())
+        {
+            broadcast.push_back("BACKEND_MESSAGE");
+            broadcast.push_back(me->Message());
+            broadcast += me->ExtraDataList();
+        }
     }
 
     if (!broadcast.empty())
@@ -973,7 +1080,7 @@
             sendGlobal = true;
         }
 
-        vector<PlaybackSock*> sentSet;
+        QSet<PlaybackSock*> sentSet;
 
         bool isSystemEvent = broadcast[1].startsWith("SYSTEM_EVENT ");
         QStringList sentSetSystemEvent(gCoreContext->GetHostName());
@@ -983,13 +1090,14 @@
         {
             PlaybackSock *pbs = *iter;
 
-            vector<PlaybackSock*>::const_iterator it =
-                find(sentSet.begin(), sentSet.end(), pbs);
-            if (it != sentSet.end() || pbs->IsDisconnected())
+            if (sentSet.contains(pbs) || pbs->IsDisconnected())
                 continue;
 
-            sentSet.push_back(pbs);
+            if (!receivers.empty() && !receivers.contains(pbs->getHostname()))
+                continue;
 
+            sentSet.insert(pbs);
+
             bool reallysendit = false;
 
             if (broadcast[1] == "CLEAR_SETTINGS_CACHE")
@@ -4913,6 +5021,15 @@
 {
     MythSocket *pbssock = pbs->getSocket();
 
+    if (slist.size() < 3)
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR + "Too few params in pixmap request");
+        QStringList outputlist("ERROR");
+        outputlist += "TOO_FEW_PARAMS";
+        SendResponse(pbssock, outputlist);
+        return;
+    }
+
     bool      time_fmt_sec   = true;
     long long time           = -1;
     QString   outputfile;
@@ -4920,17 +5037,35 @@
     int       height         = -1;
     bool      has_extra_data = false;
 
-    QStringList::const_iterator it = slist.begin() + 1;
+    QString token = slist[1];
+    if (token.isEmpty())
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR + "Failed to parse pixmap request. "
+                "Token absent");
+        QStringList outputlist("ERROR");
+        outputlist += "TOKEN_ABSENT";
+        SendResponse(pbssock, outputlist);
+        return;
+    }
+
+    QStringList::const_iterator it = slist.begin() + 2;
     QStringList::const_iterator end = slist.end();
     ProgramInfo pginfo(it, end);
     bool ok = pginfo.HasPathname();
     if (!ok)
     {
-        VERBOSE(VB_IMPORTANT, "MainServer: Failed to parse pixmap request.");
+        VERBOSE(VB_IMPORTANT, LOC_ERR + "Failed to parse pixmap request. "
+                "ProgramInfo missing pathname");
         QStringList outputlist("BAD");
-        outputlist += "ERROR_INVALID_REQUEST";
+        outputlist += "NO_PATHNAME";
         SendResponse(pbssock, outputlist);
+        return;
     }
+    if (token.toLower() == "do_not_care")
+    {
+        token = QString("%1:%2")
+            .arg(pginfo.MakeUniqueKey()).arg(rand());
+    }
     if (it != slist.end())
         (time_fmt_sec = ((*it).toLower() == "s")), it++;
     if (it != slist.end())
@@ -4961,6 +5096,8 @@
 
     pginfo.SetPathname(GetPlaybackURL(&pginfo));
 
+    m_previewRequestedBy[token] = pbs->getHostname();
+
     if ((ismaster) &&
         (pginfo.GetHostname() != gCoreContext->GetHostName()) &&
         (!masterBackendOverride || !pginfo.IsLocal()))
@@ -4969,18 +5106,22 @@
 
         if (slave)
         {
-            QStringList outputlist("OK");
+            QStringList outputlist;
             if (has_extra_data)
             {
                 outputlist = slave->GenPreviewPixmap(
-                    &pginfo, time_fmt_sec, time, outputfile, outputsize);
+                    token, &pginfo, time_fmt_sec, time, outputfile, outputsize);
             }
             else
             {
-                outputlist = slave->GenPreviewPixmap(&pginfo);
+                outputlist = slave->GenPreviewPixmap(token, &pginfo);
             }
 
             slave->DownRef();
+
+            if (outputlist.empty() || outputlist[0] != "OK")
+                m_previewRequestedBy.remove(token);
+
             SendResponse(pbssock, outputlist);
             return;
         }
@@ -4992,38 +5133,29 @@
 
     if (!pginfo.IsLocal())
     {
-        VERBOSE(VB_IMPORTANT, "MainServer: HandleGenPreviewPixmap: Unable to "
+        VERBOSE(VB_IMPORTANT, LOC_ERR + "HandleGenPreviewPixmap: Unable to "
                 "find file locally, unable to make preview image.");
-        QStringList outputlist( "BAD" );
-        outputlist += "ERROR_NOFILE";
+        QStringList outputlist( "ERROR" );
+        outputlist += "FILE_INACCESSIBLE";
         SendResponse(pbssock, outputlist);
+        m_previewRequestedBy.remove(token);
         return;
     }
 
-    PreviewGenerator *previewgen = new PreviewGenerator(&pginfo);
     if (has_extra_data)
     {
-        previewgen->SetOutputSize(outputsize);
-        previewgen->SetOutputFilename(outputfile);
-        previewgen->SetPreviewTime(time, time_fmt_sec);
+        PreviewGeneratorQueue::GetPreviewImage(
+            pginfo, outputsize, outputfile, time, time_fmt_sec, token);
     }
-    ok = previewgen->Run();
-    previewgen->deleteLater();
-
-    if (ok)
-    {
-        QStringList outputlist("OK");
-        if (!outputfile.isEmpty())
-            outputlist += outputfile;
-        SendResponse(pbssock, outputlist);
-    }
     else
     {
-        VERBOSE(VB_IMPORTANT, "MainServer: Failed to make preview image.");
-        QStringList outputlist( "BAD" );
-        outputlist += "ERROR_UNKNOWN";
-        SendResponse(pbssock, outputlist);
+        PreviewGeneratorQueue::GetPreviewImage(pginfo, token);
     }
+
+    QStringList outputlist("OK");
+    if (!outputfile.isEmpty())
+        outputlist += outputfile;
+    SendResponse(pbssock, outputlist);
 }
 
 void MainServer::HandlePixmapLastModified(QStringList &slist, PlaybackSock *pbs)
Index: mythtv/programs/mythpreviewgen/main.cpp
===================================================================
--- mythtv/programs/mythpreviewgen/main.cpp	(revision 26104)
+++ mythtv/programs/mythpreviewgen/main.cpp	(working copy)
@@ -135,7 +135,7 @@
     }
 
     PreviewGenerator *previewgen = new PreviewGenerator(
-        pginfo, PreviewGenerator::kLocal);
+        pginfo, QString(), PreviewGenerator::kLocal);
 
     if (previewFrameNumber >= 0)
         previewgen->SetPreviewTimeAsFrameNumber(previewFrameNumber);
Index: mythtv/bindings/python/MythTV/static.py
===================================================================
--- mythtv/bindings/python/MythTV/static.py	(revision 26104)
+++ mythtv/bindings/python/MythTV/static.py	(working copy)
@@ -9,7 +9,7 @@
 MVSCHEMA_VERSION = 1036
 NVSCHEMA_VERSION = 1007
 MUSICSCHEMA_VERSION = 1017
-PROTO_VERSION = '60'
+PROTO_VERSION = '61'
 BACKEND_SEP = '[]:[]'
 
 class MARKUP( object ):
Index: mythtv/bindings/perl/MythTV.pm
===================================================================
--- mythtv/bindings/perl/MythTV.pm	(revision 26104)
+++ mythtv/bindings/perl/MythTV.pm	(working copy)
@@ -106,7 +106,7 @@
 # Note: as of July 21, 2010, this is actually a string, to account for proto
 # versions of the form "58a".  This will get used if protocol versions are 
 # changed on a fixes branch ongoing.
-    our $PROTO_VERSION = "60";
+    our $PROTO_VERSION = "61";
 
 # NUMPROGRAMLINES is defined in mythtv/libs/libmythtv/programinfo.h and is
 # the number of items in a ProgramInfo QStringList group used by
Index: mythtv/bindings/perl/MythTV/Recording.pm
===================================================================
--- mythtv/bindings/perl/MythTV/Recording.pm	(revision 26104)
+++ mythtv/bindings/perl/MythTV/Recording.pm	(working copy)
@@ -448,10 +448,11 @@
     sub generate_pixmap {
         my $self = shift;
         my $ret = $self->{'_mythtv'}->backend_command(join($MythTV::BACKEND_SEP,
-                                                           'QUERY_GENPIXMAP',
+                                                           'QUERY_GENPIXMAP2',
+                                                           "do_not_care",
                                                            $self->to_string())
                                                      );
-        if ($ret eq 'BAD') {
+        if ($ret eq 'ERROR') {
             print STDERR "Unknown error generating pixmap for $self->{'chanid'}:$self->{'starttime'}\n";
             return 0;
         }
