MythTV  master
mainserver.cpp
Go to the documentation of this file.
1 #include <algorithm>
2 #include <cerrno>
3 #include <chrono> // for milliseconds
4 #include <cmath>
5 #include <cstdlib>
6 #include <fcntl.h>
7 #include <iostream>
8 #include <list>
9 #include <memory>
10 #include <thread> // for sleep_for
11 using namespace std;
12 
13 #include "mythconfig.h"
14 
15 #ifndef _WIN32
16 #include <sys/ioctl.h>
17 #endif
18 #if CONFIG_SYSTEMD_NOTIFY
19 #include <systemd/sd-daemon.h>
20 #endif
21 
22 #include <sys/stat.h>
23 #ifdef __linux__
24 # include <sys/vfs.h>
25 #else // if !__linux__
26 # include <sys/param.h>
27 # ifndef _WIN32
28 # include <sys/mount.h>
29 # endif // _WIN32
30 #endif // !__linux__
31 
32 #include <QCoreApplication>
33 #include <QDateTime>
34 #include <QFile>
35 #include <QDir>
36 #include <QWaitCondition>
37 #include <QWriteLocker>
38 #include <QRegExp>
39 #include <QEvent>
40 #include <QTcpServer>
41 #include <QTimer>
42 #include <QNetworkInterface>
43 #include <QNetworkProxy>
44 #include <QHostAddress>
45 
46 #include "previewgeneratorqueue.h"
47 #include "mythmiscutil.h"
48 #include "mythsystemlegacy.h"
49 #include "mythcontext.h"
50 #include "mythversion.h"
51 #include "mythdb.h"
52 #include "mainserver.h"
53 #include "server.h"
54 #include "mthread.h"
55 #include "scheduler.h"
57 #include "programinfo.h"
58 #include "mythtimezone.h"
59 #include "recordinginfo.h"
60 #include "recordingrule.h"
61 #include "scheduledrecording.h"
62 #include "jobqueue.h"
63 #include "autoexpire.h"
64 #include "storagegroup.h"
65 #include "compat.h"
66 #include "io/mythmediabuffer.h"
67 #include "remotefile.h"
68 #include "mythsystemevent.h"
69 #include "tv.h"
70 #include "mythcorecontext.h"
71 #include "mythcoreutil.h"
72 #include "mythdirs.h"
73 #include "mythdownloadmanager.h"
74 #include "metadatafactory.h"
75 #include "videoutils.h"
76 #include "mythlogging.h"
77 #include "filesysteminfo.h"
78 #include "metaio.h"
79 #include "musicmetadata.h"
80 #include "imagemanager.h"
81 #include "cardutil.h"
82 #include "tv_rec.h"
83 
84 // mythbackend headers
85 #include "backendcontext.h"
86 
90 #define PRT_TIMEOUT 10
91 
92 #define PRT_STARTUP_THREAD_COUNT 5
93 
94 #define LOC QString("MainServer: ")
95 #define LOC_WARN QString("MainServer, Warning: ")
96 #define LOC_ERR QString("MainServer, Error: ")
97 
98 namespace {
99 
100 bool delete_file_immediately(const QString &filename,
101  bool followLinks, bool checkexists)
102 {
103  /* Return true for success, false for error. */
104  QFile checkFile(filename);
105  bool success1 = true;
106  bool success2 = true;
107 
108  LOG(VB_FILE, LOG_INFO, LOC +
109  QString("About to delete file: %1").arg(filename));
110  if (followLinks)
111  {
112  QFileInfo finfo(filename);
113  if (finfo.isSymLink())
114  {
115  QString linktext = getSymlinkTarget(filename);
116 
117  QFile target(linktext);
118  if (!(success1 = target.remove()))
119  {
120  LOG(VB_GENERAL, LOG_ERR, LOC +
121  QString("Error deleting '%1' -> '%2'")
122  .arg(filename).arg(linktext) + ENO);
123  }
124  }
125  }
126  if ((!checkexists || checkFile.exists()) &&
127  !(success2 = checkFile.remove()))
128  {
129  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error deleting '%1': %2")
130  .arg(filename).arg(strerror(errno)));
131  }
132  return success1 && success2;
133 }
134 
135 };
136 
139 
140 class ProcessRequestRunnable : public QRunnable
141 {
142  public:
144  m_parent(parent), m_sock(sock)
145  {
146  m_sock->IncrRef();
147  }
148 
150  {
151  if (m_sock)
152  {
153  m_sock->DecrRef();
154  m_sock = nullptr;
155  }
156  }
157 
158  void run(void) override // QRunnable
159  {
160  m_parent.ProcessRequest(m_sock);
161  m_sock->DecrRef();
162  m_sock = nullptr;
163  }
164 
165  private:
167  MythSocket *m_sock;
168 };
169 
170 class FreeSpaceUpdater : public QRunnable
171 {
172  public:
173  explicit FreeSpaceUpdater(MainServer &parent) :
174  m_parent(parent), m_dorun(true), m_running(true)
175  {
176  m_lastRequest.start();
177  }
178  ~FreeSpaceUpdater() override
179  {
180  QMutexLocker locker(&m_parent.m_masterFreeSpaceListLock);
181  m_parent.m_masterFreeSpaceListUpdater = nullptr;
182  m_parent.m_masterFreeSpaceListWait.wakeAll();
183  }
184 
185  void run(void) override // QRunnable
186  {
187  while (true)
188  {
189  MythTimer t;
190  t.start();
191  QStringList list;
192  m_parent.BackendQueryDiskSpace(list, true, true);
193  {
194  QMutexLocker locker(&m_parent.m_masterFreeSpaceListLock);
195  m_parent.m_masterFreeSpaceList = list;
196  }
197  QMutexLocker locker(&m_lock);
198  int left = kRequeryTimeout - t.elapsed();
199  if (m_lastRequest.elapsed() + left > kExitTimeout)
200  m_dorun = false;
201  if (!m_dorun)
202  {
203  m_running = false;
204  break;
205  }
206  if (left > 50)
207  m_wait.wait(locker.mutex(), left);
208  }
209  }
210 
211  bool KeepRunning(bool dorun)
212  {
213  QMutexLocker locker(&m_lock);
214  if (dorun && m_running)
215  {
216  m_dorun = true;
217  m_lastRequest.restart();
218  }
219  else
220  {
221  m_dorun = false;
222  m_wait.wakeAll();
223  }
224  return m_running;
225  }
226 
228  QMutex m_lock;
229  bool m_dorun;
230  bool m_running;
232  QWaitCondition m_wait;
233  const static int kRequeryTimeout;
234  const static int kExitTimeout;
235 };
236 const int FreeSpaceUpdater::kRequeryTimeout = 15000;
237 const int FreeSpaceUpdater::kExitTimeout = 61000;
238 
239 MainServer::MainServer(bool master, int port,
240  QMap<int, EncoderLink *> *_tvList,
241  Scheduler *sched, AutoExpire *_expirer) :
242  m_encoderList(_tvList),
243  m_ismaster(master), m_threadPool("ProcessRequestPool"),
244  m_sched(sched), m_expirer(_expirer)
245 {
249 
251 
253  gCoreContext->GetBoolSetting("MasterBackendOverride", false);
254 
255  m_mythserver = new MythServer();
256  m_mythserver->setProxy(QNetworkProxy::NoProxy);
257 
258  QList<QHostAddress> listenAddrs = MythServer::DefaultListen();
259  if (!gCoreContext->GetBoolSetting("ListenOnAllIps",true))
260  {
261  // test to make sure listen addresses are available
262  // no reason to run the backend if the mainserver is not active
263  QHostAddress config_v4(gCoreContext->resolveSettingAddress(
264  "BackendServerIP",
265  QString(),
266  gCoreContext->ResolveIPv4, true));
267  bool v4IsSet = !config_v4.isNull();
268  QHostAddress config_v6(gCoreContext->resolveSettingAddress(
269  "BackendServerIP6",
270  QString(),
271  gCoreContext->ResolveIPv6, true));
272  bool v6IsSet = !config_v6.isNull();
273 
274  if (v6IsSet && !listenAddrs.contains(config_v6))
275  LOG(VB_GENERAL, LOG_WARNING, LOC +
276  "Unable to find IPv6 address to bind");
277 
278  if (v4IsSet && !listenAddrs.contains(config_v4))
279  LOG(VB_GENERAL, LOG_WARNING, LOC +
280  "Unable to find IPv4 address to bind");
281 
282  if ((v4IsSet && !listenAddrs.contains(config_v4))
283  && (v6IsSet && !listenAddrs.contains(config_v6))
284  )
285  {
286  LOG(VB_GENERAL, LOG_ERR, LOC + "Unable to find either IPv4 or IPv6 "
287  "address we can bind to, exiting");
289  return;
290  }
291  }
292  if (!m_mythserver->listen(port))
293  {
295  return;
296  }
297  connect(m_mythserver, SIGNAL(NewConnection(qt_socket_fd_t)),
298  this, SLOT(NewConnection(qt_socket_fd_t)));
299 
300  gCoreContext->addListener(this);
301 
302  if (!m_ismaster)
303  {
304  m_masterServerReconnect = new QTimer(this);
305  m_masterServerReconnect->setSingleShot(true);
306  connect(m_masterServerReconnect, SIGNAL(timeout()),
307  this, SLOT(reconnectTimeout()));
309  }
310 
311  m_deferredDeleteTimer = new QTimer(this);
312  connect(m_deferredDeleteTimer, SIGNAL(timeout()),
313  this, SLOT(deferredDeleteSlot()));
314  m_deferredDeleteTimer->start(30 * 1000);
315 
316  if (sched)
317  {
318  // Make sure we have a good, fsinfo cache before setting
319  // mainServer in the scheduler.
320  QList<FileSystemInfo> m_fsInfos;
321  GetFilesystemInfos(m_fsInfos, false);
322  sched->SetMainServer(this);
323  }
324  if (expirer)
325  expirer->SetMainServer(this);
326 
328 
329  m_autoexpireUpdateTimer = new QTimer(this);
330  connect(m_autoexpireUpdateTimer, SIGNAL(timeout()),
331  this, SLOT(autoexpireUpdate()));
332  m_autoexpireUpdateTimer->setSingleShot(true);
333 
334  AutoExpire::Update(true);
335 
337  m_masterFreeSpaceList << "TotalDiskSpace";
338  m_masterFreeSpaceList << "0";
339  m_masterFreeSpaceList << "-2";
340  m_masterFreeSpaceList << "-2";
341  m_masterFreeSpaceList << "0";
342  m_masterFreeSpaceList << "0";
343  m_masterFreeSpaceList << "0";
344 
345  m_masterFreeSpaceListUpdater = (master ? new FreeSpaceUpdater(*this) : nullptr);
347  {
349  m_masterFreeSpaceListUpdater, "FreeSpaceUpdater");
350  }
351 }
352 
354 {
355  if (!m_stopped)
356  Stop();
357 }
358 
360 {
361  m_stopped = true;
362 
364 
365  {
366  QMutexLocker locker(&m_masterFreeSpaceListLock);
369  }
370 
371  m_threadPool.Stop();
372 
373  // since Scheduler::SetMainServer() isn't thread-safe
374  // we need to shut down the scheduler thread before we
375  // can call SetMainServer(nullptr)
376  if (m_sched)
377  m_sched->Stop();
378 
381 
382  if (m_mythserver)
383  {
384  m_mythserver->disconnect();
385  m_mythserver->deleteLater();
386  m_mythserver = nullptr;
387  }
388 
389  if (m_sched)
390  {
391  m_sched->Wait();
392  m_sched->SetMainServer(nullptr);
393  }
394 
395  if (m_expirer)
396  m_expirer->SetMainServer(nullptr);
397 
398  {
399  QMutexLocker locker(&m_masterFreeSpaceListLock);
401  {
403  m_masterFreeSpaceListWait.wait(locker.mutex());
404  }
405  }
406 
407  // Close all open sockets
408  QWriteLocker locker(&m_sockListLock);
409 
410  for (auto & pbs : m_playbackList)
411  pbs->DecrRef();
412  m_playbackList.clear();
413 
414  for (auto & ft : m_fileTransferList)
415  ft->DecrRef();
416  m_fileTransferList.clear();
417 
418  for (auto *cs : qAsConst(m_controlSocketList))
419  cs->DecrRef();
420  m_controlSocketList.clear();
421 
422  while (!m_decrRefSocketList.empty())
423  {
424  (*m_decrRefSocketList.begin())->DecrRef();
426  }
427 }
428 
430 {
431  AutoExpire::Update(false);
432 }
433 
435 {
436  QWriteLocker locker(&m_sockListLock);
437  auto *ms = new MythSocket(socketDescriptor, this);
438  if (ms->IsConnected())
439  m_controlSocketList.insert(ms);
440  else
441  ms-> DecrRef();
442 }
443 
445 {
447  new ProcessRequestRunnable(*this, sock),
448  "ProcessRequest", PRT_TIMEOUT);
449 
450  QCoreApplication::processEvents();
451 }
452 
454 {
455  if (sock->IsDataAvailable())
456  ProcessRequestWork(sock);
457  else
458  LOG(VB_GENERAL, LOG_INFO, LOC + QString("No data on sock %1")
459  .arg(sock->GetSocketDescriptor()));
460 }
461 
463 {
464  m_sockListLock.lockForRead();
466  if (pbs)
467  pbs->IncrRef();
468 
469  bool bIsControl = (pbs) ? false : m_controlSocketList.contains(sock);
470  m_sockListLock.unlock();
471 
472  QStringList listline;
473  if (pbs)
474  {
475  if (!pbs->ReadStringList(listline) || listline.empty())
476  {
477  pbs->DecrRef();
478  LOG(VB_GENERAL, LOG_INFO, "No data in ProcessRequestWork()");
479  return;
480  }
481  pbs->DecrRef();
482  }
483  else if (!bIsControl)
484  {
485  // The socket has been disconnected
486  return;
487  }
488  else if (!sock->ReadStringList(listline) || listline.empty())
489  {
490  LOG(VB_GENERAL, LOG_INFO, LOC + "No data in ProcessRequestWork()");
491  return;
492  }
493 
494  QString line = listline[0];
495 
496  line = line.simplified();
497 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
498  QStringList tokens = line.split(' ', QString::SkipEmptyParts);
499 #else
500  QStringList tokens = line.split(' ', Qt::SkipEmptyParts);
501 #endif
502  QString command = tokens[0];
503 
504  if (command == "MYTH_PROTO_VERSION")
505  {
506  if (tokens.size() < 2)
507  SendErrorResponse(sock, "Bad MYTH_PROTO_VERSION command");
508  else
509  HandleVersion(sock, tokens);
510  return;
511  }
512  if (command == "ANN")
513  {
514  HandleAnnounce(listline, tokens, sock);
515  return;
516  }
517  if (command == "DONE")
518  {
519  HandleDone(sock);
520  return;
521  }
522 
523  m_sockListLock.lockForRead();
524  pbs = GetPlaybackBySock(sock);
525  if (!pbs)
526  {
527  m_sockListLock.unlock();
528  LOG(VB_GENERAL, LOG_ERR, LOC + "ProcessRequest unknown socket");
529  return;
530  }
531  pbs->IncrRef();
532  m_sockListLock.unlock();
533 
534  if (command == "QUERY_FILETRANSFER")
535  {
536  if (tokens.size() != 2)
537  SendErrorResponse(pbs, "Bad QUERY_FILETRANSFER");
538  else
539  HandleFileTransferQuery(listline, tokens, pbs);
540  }
541  else if (command == "QUERY_RECORDINGS")
542  {
543  if (tokens.size() != 2)
544  SendErrorResponse(pbs, "Bad QUERY_RECORDINGS query");
545  else
546  HandleQueryRecordings(tokens[1], pbs);
547  }
548  else if (command == "QUERY_RECORDING")
549  {
550  HandleQueryRecording(tokens, pbs);
551  }
552  else if (command == "GO_TO_SLEEP")
553  {
555  }
556  else if (command == "QUERY_FREE_SPACE")
557  {
558  HandleQueryFreeSpace(pbs, false);
559  }
560  else if (command == "QUERY_FREE_SPACE_LIST")
561  {
562  HandleQueryFreeSpace(pbs, true);
563  }
564  else if (command == "QUERY_FREE_SPACE_SUMMARY")
565  {
567  }
568  else if (command == "QUERY_LOAD")
569  {
571  }
572  else if (command == "QUERY_UPTIME")
573  {
575  }
576  else if (command == "QUERY_HOSTNAME")
577  {
579  }
580  else if (command == "QUERY_MEMSTATS")
581  {
583  }
584  else if (command == "QUERY_TIME_ZONE")
585  {
587  }
588  else if (command == "QUERY_CHECKFILE")
589  {
590  HandleQueryCheckFile(listline, pbs);
591  }
592  else if (command == "QUERY_FILE_EXISTS")
593  {
594  if (listline.size() < 2)
595  SendErrorResponse(pbs, "Bad QUERY_FILE_EXISTS command");
596  else
597  HandleQueryFileExists(listline, pbs);
598  }
599  else if (command == "QUERY_FINDFILE")
600  {
601  if (listline.size() < 4)
602  SendErrorResponse(pbs, "Bad QUERY_FINDFILE command");
603  else
604  HandleQueryFindFile(listline, pbs);
605  }
606  else if (command == "QUERY_FILE_HASH")
607  {
608  if (listline.size() < 3)
609  SendErrorResponse(pbs, "Bad QUERY_FILE_HASH command");
610  else
611  HandleQueryFileHash(listline, pbs);
612  }
613  else if (command == "QUERY_GUIDEDATATHROUGH")
614  {
616  }
617  else if (command == "DELETE_FILE")
618  {
619  if (listline.size() < 3)
620  SendErrorResponse(pbs, "Bad DELETE_FILE command");
621  else
622  HandleDeleteFile(listline, pbs);
623  }
624  else if (command == "MOVE_FILE")
625  {
626  if (listline.size() < 4)
627  SendErrorResponse(pbs, "Bad MOVE_FILE command");
628  else
629  HandleMoveFile(pbs, listline[1], listline[2], listline[3]);
630  }
631  else if (command == "STOP_RECORDING")
632  {
633  HandleStopRecording(listline, pbs);
634  }
635  else if (command == "CHECK_RECORDING")
636  {
637  HandleCheckRecordingActive(listline, pbs);
638  }
639  else if (command == "DELETE_RECORDING")
640  {
641  if (3 <= tokens.size() && tokens.size() <= 5)
642  {
643  bool force = (tokens.size() >= 4) && (tokens[3] == "FORCE");
644  bool forget = (tokens.size() >= 5) && (tokens[4] == "FORGET");
645  HandleDeleteRecording(tokens[1], tokens[2], pbs, force, forget);
646  }
647  else
648  HandleDeleteRecording(listline, pbs, false);
649  }
650  else if (command == "FORCE_DELETE_RECORDING")
651  {
652  HandleDeleteRecording(listline, pbs, true);
653  }
654  else if (command == "UNDELETE_RECORDING")
655  {
656  HandleUndeleteRecording(listline, pbs);
657  }
658  else if (command == "ADD_CHILD_INPUT")
659  {
660  QStringList reslist;
661  if (m_ismaster)
662  {
663  LOG(VB_GENERAL, LOG_ERR, LOC +
664  "ADD_CHILD_INPUT command received in master context");
665  reslist << QString("ERROR: Called in master context");
666  }
667  else if (tokens.size() != 2)
668  reslist << "ERROR: Bad ADD_CHILD_INPUT request";
669  else if (HandleAddChildInput(tokens[1].toUInt()))
670  reslist << "OK";
671  else
672  reslist << QString("ERROR: Failed to add child input");
673  SendResponse(pbs->getSocket(), reslist);
674  }
675  else if (command == "RESCHEDULE_RECORDINGS")
676  {
677  listline.pop_front();
678  HandleRescheduleRecordings(listline, pbs);
679  }
680  else if (command == "FORGET_RECORDING")
681  {
682  HandleForgetRecording(listline, pbs);
683  }
684  else if (command == "QUERY_GETALLPENDING")
685  {
686  if (tokens.size() == 1)
688  else if (tokens.size() == 2)
689  HandleGetPendingRecordings(pbs, tokens[1]);
690  else
691  HandleGetPendingRecordings(pbs, tokens[1], tokens[2].toInt());
692  }
693  else if (command == "QUERY_GETALLSCHEDULED")
694  {
696  }
697  else if (command == "QUERY_GETCONFLICTING")
698  {
700  }
701  else if (command == "QUERY_GETEXPIRING")
702  {
704  }
705  else if (command == "QUERY_SG_GETFILELIST")
706  {
707  HandleSGGetFileList(listline, pbs);
708  }
709  else if (command == "QUERY_SG_FILEQUERY")
710  {
711  HandleSGFileQuery(listline, pbs);
712  }
713  else if (command == "GET_FREE_INPUT_INFO")
714  {
715  if (tokens.size() != 2)
716  SendErrorResponse(pbs, "Bad GET_FREE_INPUT_INFO");
717  else
718  HandleGetFreeInputInfo(pbs, tokens[1].toUInt());
719  }
720  else if (command == "QUERY_RECORDER")
721  {
722  if (tokens.size() != 2)
723  SendErrorResponse(pbs, "Bad QUERY_RECORDER");
724  else
725  HandleRecorderQuery(listline, tokens, pbs);
726  }
727  else if ((command == "QUERY_RECORDING_DEVICE") ||
728  (command == "QUERY_RECORDING_DEVICES"))
729  {
730  // TODO
731  }
732  else if (command == "SET_NEXT_LIVETV_DIR")
733  {
734  if (tokens.size() != 3)
735  SendErrorResponse(pbs, "Bad SET_NEXT_LIVETV_DIR");
736  else
737  HandleSetNextLiveTVDir(tokens, pbs);
738  }
739  else if (command == "SET_CHANNEL_INFO")
740  {
741  HandleSetChannelInfo(listline, pbs);
742  }
743  else if (command == "QUERY_REMOTEENCODER")
744  {
745  if (tokens.size() != 2)
746  SendErrorResponse(pbs, "Bad QUERY_REMOTEENCODER");
747  else
748  HandleRemoteEncoder(listline, tokens, pbs);
749  }
750  else if (command == "GET_RECORDER_FROM_NUM")
751  {
752  HandleGetRecorderFromNum(listline, pbs);
753  }
754  else if (command == "GET_RECORDER_NUM")
755  {
756  HandleGetRecorderNum(listline, pbs);
757  }
758  else if (command == "QUERY_GENPIXMAP2")
759  {
760  HandleGenPreviewPixmap(listline, pbs);
761  }
762  else if (command == "QUERY_PIXMAP_LASTMODIFIED")
763  {
764  HandlePixmapLastModified(listline, pbs);
765  }
766  else if (command == "QUERY_PIXMAP_GET_IF_MODIFIED")
767  {
768  HandlePixmapGetIfModified(listline, pbs);
769  }
770  else if (command == "QUERY_ISRECORDING")
771  {
772  HandleIsRecording(listline, pbs);
773  }
774  else if (command == "MESSAGE")
775  {
776  if ((listline.size() >= 2) && (listline[1].startsWith("SET_VERBOSE")))
777  HandleSetVerbose(listline, pbs);
778  else if ((listline.size() >= 2) &&
779  (listline[1].startsWith("SET_LOG_LEVEL")))
780  HandleSetLogLevel(listline, pbs);
781  else
782  HandleMessage(listline, pbs);
783  }
784  else if (command == "FILL_PROGRAM_INFO")
785  {
786  HandleFillProgramInfo(listline, pbs);
787  }
788  else if (command == "LOCK_TUNER")
789  {
790  if (tokens.size() == 1)
792  else if (tokens.size() == 2)
793  HandleLockTuner(pbs, tokens[1].toInt());
794  else
795  SendErrorResponse(pbs, "Bad LOCK_TUNER query");
796  }
797  else if (command == "FREE_TUNER")
798  {
799  if (tokens.size() != 2)
800  SendErrorResponse(pbs, "Bad FREE_TUNER query");
801  else
802  HandleFreeTuner(tokens[1].toInt(), pbs);
803  }
804  else if (command == "QUERY_ACTIVE_BACKENDS")
805  {
807  }
808  else if (command == "QUERY_IS_ACTIVE_BACKEND")
809  {
810  if (tokens.size() != 1)
811  SendErrorResponse(pbs, "Bad QUERY_IS_ACTIVE_BACKEND");
812  else
813  HandleIsActiveBackendQuery(listline, pbs);
814  }
815  else if (command == "QUERY_COMMBREAK")
816  {
817  if (tokens.size() != 3)
818  SendErrorResponse(pbs, "Bad QUERY_COMMBREAK");
819  else
820  HandleCommBreakQuery(tokens[1], tokens[2], pbs);
821  }
822  else if (command == "QUERY_CUTLIST")
823  {
824  if (tokens.size() != 3)
825  SendErrorResponse(pbs, "Bad QUERY_CUTLIST");
826  else
827  HandleCutlistQuery(tokens[1], tokens[2], pbs);
828  }
829  else if (command == "QUERY_BOOKMARK")
830  {
831  if (tokens.size() != 3)
832  SendErrorResponse(pbs, "Bad QUERY_BOOKMARK");
833  else
834  HandleBookmarkQuery(tokens[1], tokens[2], pbs);
835  }
836  else if (command == "SET_BOOKMARK")
837  {
838  if (tokens.size() != 4)
839  SendErrorResponse(pbs, "Bad SET_BOOKMARK");
840  else
841  HandleSetBookmark(tokens, pbs);
842  }
843  else if (command == "QUERY_SETTING")
844  {
845  if (tokens.size() != 3)
846  SendErrorResponse(pbs, "Bad QUERY_SETTING");
847  else
848  HandleSettingQuery(tokens, pbs);
849  }
850  else if (command == "SET_SETTING")
851  {
852  if (tokens.size() != 4)
853  SendErrorResponse(pbs, "Bad SET_SETTING");
854  else
855  HandleSetSetting(tokens, pbs);
856  }
857  else if (command == "SCAN_VIDEOS")
858  {
860  }
861  else if (command == "SCAN_MUSIC")
862  {
863  HandleScanMusic(tokens, pbs);
864  }
865  else if (command == "MUSIC_TAG_UPDATE_VOLATILE")
866  {
867  if (listline.size() != 6)
868  SendErrorResponse(pbs, "Bad MUSIC_TAG_UPDATE_VOLATILE");
869  else
871  }
872  else if (command == "MUSIC_CALC_TRACK_LENGTH")
873  {
874  if (listline.size() != 3)
875  SendErrorResponse(pbs, "Bad MUSIC_CALC_TRACK_LENGTH");
876  else
877  HandleMusicCalcTrackLen(listline, pbs);
878  }
879  else if (command == "MUSIC_TAG_UPDATE_METADATA")
880  {
881  if (listline.size() != 3)
882  SendErrorResponse(pbs, "Bad MUSIC_TAG_UPDATE_METADATA");
883  else
885  }
886  else if (command == "MUSIC_FIND_ALBUMART")
887  {
888  if (listline.size() != 4)
889  SendErrorResponse(pbs, "Bad MUSIC_FIND_ALBUMART");
890  else
891  HandleMusicFindAlbumArt(listline, pbs);
892  }
893  else if (command == "MUSIC_TAG_GETIMAGE")
894  {
895  if (listline.size() < 4)
896  SendErrorResponse(pbs, "Bad MUSIC_TAG_GETIMAGE");
897  else
898  HandleMusicTagGetImage(listline, pbs);
899  }
900  else if (command == "MUSIC_TAG_ADDIMAGE")
901  {
902  if (listline.size() < 5)
903  SendErrorResponse(pbs, "Bad MUSIC_TAG_ADDIMAGE");
904  else
905  HandleMusicTagAddImage(listline, pbs);
906  }
907  else if (command == "MUSIC_TAG_REMOVEIMAGE")
908  {
909  if (listline.size() < 4)
910  SendErrorResponse(pbs, "Bad MUSIC_TAG_REMOVEIMAGE");
911  else
912  HandleMusicTagRemoveImage(listline, pbs);
913  }
914  else if (command == "MUSIC_TAG_CHANGEIMAGE")
915  {
916  if (listline.size() < 5)
917  SendErrorResponse(pbs, "Bad MUSIC_TAG_CHANGEIMAGE");
918  else
919  HandleMusicTagChangeImage(listline, pbs);
920  }
921  else if (command == "MUSIC_LYRICS_FIND")
922  {
923  if (listline.size() < 3)
924  SendErrorResponse(pbs, "Bad MUSIC_LYRICS_FIND");
925  else
926  HandleMusicFindLyrics(listline, pbs);
927  }
928  else if (command == "MUSIC_LYRICS_GETGRABBERS")
929  {
930  HandleMusicGetLyricGrabbers(listline, pbs);
931  }
932  else if (command == "MUSIC_LYRICS_SAVE")
933  {
934  if (listline.size() < 3)
935  SendErrorResponse(pbs, "Bad MUSIC_LYRICS_SAVE");
936  else
937  HandleMusicSaveLyrics(listline, pbs);
938  }
939  else if (command == "IMAGE_SCAN")
940  {
941  // Expects command
942  QStringList reply = (listline.size() == 2)
944  : QStringList("ERROR") << "Bad: " << listline;
945 
946  SendResponse(pbs->getSocket(), reply);
947  }
948  else if (command == "IMAGE_COPY")
949  {
950  // Expects at least 1 comma-delimited image definition
951  QStringList reply = (listline.size() >= 2)
952  ? ImageManagerBe::getInstance()->HandleDbCreate(listline.mid(1))
953  : QStringList("ERROR") << "Bad: " << listline;
954 
955  SendResponse(pbs->getSocket(), reply);
956  }
957  else if (command == "IMAGE_MOVE")
958  {
959  // Expects comma-delimited dir/file ids, path to replace, new path
960  QStringList reply = (listline.size() == 4)
962  HandleDbMove(listline[1], listline[2], listline[3])
963  : QStringList("ERROR") << "Bad: " << listline;
964 
965  SendResponse(pbs->getSocket(), reply);
966  }
967  else if (command == "IMAGE_DELETE")
968  {
969  // Expects comma-delimited dir/file ids
970  QStringList reply = (listline.size() == 2)
971  ? ImageManagerBe::getInstance()->HandleDelete(listline[1])
972  : QStringList("ERROR") << "Bad: " << listline;
973 
974  SendResponse(pbs->getSocket(), reply);
975  }
976  else if (command == "IMAGE_HIDE")
977  {
978  // Expects hide flag, comma-delimited file/dir ids
979  QStringList reply = (listline.size() == 3)
981  HandleHide(listline[1].toInt() != 0, listline[2])
982  : QStringList("ERROR") << "Bad: " << listline;
983 
984  SendResponse(pbs->getSocket(), reply);
985  }
986  else if (command == "IMAGE_TRANSFORM")
987  {
988  // Expects transformation, write file flag,
989  QStringList reply = (listline.size() == 3)
991  HandleTransform(listline[1].toInt(), listline[2])
992  : QStringList("ERROR") << "Bad: " << listline;
993 
994  SendResponse(pbs->getSocket(), reply);
995  }
996  else if (command == "IMAGE_RENAME")
997  {
998  // Expects file/dir id, new basename
999  QStringList reply = (listline.size() == 3)
1000  ? ImageManagerBe::getInstance()->HandleRename(listline[1], listline[2])
1001  : QStringList("ERROR") << "Bad: " << listline;
1002 
1003  SendResponse(pbs->getSocket(), reply);
1004  }
1005  else if (command == "IMAGE_CREATE_DIRS")
1006  {
1007  // Expects destination path, rescan flag, list of dir names
1008  QStringList reply = (listline.size() >= 4)
1010  HandleDirs(listline[1], listline[2].toInt() != 0, listline.mid(3))
1011  : QStringList("ERROR") << "Bad: " << listline;
1012 
1013  SendResponse(pbs->getSocket(), reply);
1014  }
1015  else if (command == "IMAGE_COVER")
1016  {
1017  // Expects dir id, cover id. Cover id of 0 resets dir to use its own
1018  QStringList reply = (listline.size() == 3)
1020  HandleCover(listline[1].toInt(), listline[2].toInt())
1021  : QStringList("ERROR") << "Bad: " << listline;
1022 
1023  SendResponse(pbs->getSocket(), reply);
1024  }
1025  else if (command == "IMAGE_IGNORE")
1026  {
1027  // Expects list of exclusion patterns
1028  QStringList reply = (listline.size() == 2)
1029  ? ImageManagerBe::getInstance()->HandleIgnore(listline[1])
1030  : QStringList("ERROR") << "Bad: " << listline;
1031 
1032  SendResponse(pbs->getSocket(), reply);
1033  }
1034  else if (command == "ALLOW_SHUTDOWN")
1035  {
1036  if (tokens.size() != 1)
1037  SendErrorResponse(pbs, "Bad ALLOW_SHUTDOWN");
1038  else
1039  HandleBlockShutdown(false, pbs);
1040  }
1041  else if (command == "BLOCK_SHUTDOWN")
1042  {
1043  if (tokens.size() != 1)
1044  SendErrorResponse(pbs, "Bad BLOCK_SHUTDOWN");
1045  else
1046  HandleBlockShutdown(true, pbs);
1047  }
1048  else if (command == "SHUTDOWN_NOW")
1049  {
1050  if (tokens.size() != 1)
1051  SendErrorResponse(pbs, "Bad SHUTDOWN_NOW query");
1052  else if (!m_ismaster)
1053  {
1054  QString halt_cmd;
1055  if (listline.size() >= 2)
1056  halt_cmd = listline[1];
1057 
1058  if (!halt_cmd.isEmpty())
1059  {
1060  LOG(VB_GENERAL, LOG_NOTICE, LOC +
1061  "Going down now as of Mainserver request!");
1062  myth_system(halt_cmd);
1063  }
1064  else
1065  SendErrorResponse(pbs, "Received an empty SHUTDOWN_NOW query!");
1066  }
1067  }
1068  else if (command == "BACKEND_MESSAGE")
1069  {
1070  QString message = listline[1];
1071  QStringList extra( listline[2] );
1072  for (int i = 3; i < listline.size(); i++)
1073  extra << listline[i];
1074  MythEvent me(message, extra);
1075  gCoreContext->dispatch(me);
1076  }
1077  else if ((command == "DOWNLOAD_FILE") ||
1078  (command == "DOWNLOAD_FILE_NOW"))
1079  {
1080  if (listline.size() != 4)
1081  SendErrorResponse(pbs, QString("Bad %1 command").arg(command));
1082  else
1083  HandleDownloadFile(listline, pbs);
1084  }
1085  else if (command == "REFRESH_BACKEND")
1086  {
1087  LOG(VB_GENERAL, LOG_INFO , LOC + "Reloading backend settings");
1088  HandleBackendRefresh(sock);
1089  }
1090  else if (command == "OK")
1091  {
1092  LOG(VB_GENERAL, LOG_ERR, LOC + "Got 'OK' out of sequence.");
1093  }
1094  else if (command == "UNKNOWN_COMMAND")
1095  {
1096  LOG(VB_GENERAL, LOG_ERR, LOC + "Got 'UNKNOWN_COMMAND' out of sequence.");
1097  }
1098  else
1099  {
1100  LOG(VB_GENERAL, LOG_ERR, LOC + "Unknown command: " + command);
1101 
1102  MythSocket *pbssock = pbs->getSocket();
1103 
1104  QStringList strlist;
1105  strlist << "UNKNOWN_COMMAND";
1106 
1107  SendResponse(pbssock, strlist);
1108  }
1109 
1110  pbs->DecrRef();
1111 }
1112 
1114 {
1115  if (!e)
1116  return;
1117 
1118  QStringList broadcast;
1119  QSet<QString> receivers;
1120 
1121  // delete stale sockets in the UI thread
1122  m_sockListLock.lockForRead();
1123  bool decrRefEmpty = m_decrRefSocketList.empty();
1124  m_sockListLock.unlock();
1125  if (!decrRefEmpty)
1126  {
1127  QWriteLocker locker(&m_sockListLock);
1128  while (!m_decrRefSocketList.empty())
1129  {
1130  (*m_decrRefSocketList.begin())->DecrRef();
1132  }
1133  }
1134 
1135  if (e->type() == MythEvent::MythEventMessage)
1136  {
1137  auto *me = dynamic_cast<MythEvent *>(e);
1138  if (me == nullptr)
1139  return;
1140 
1141  QString message = me->Message();
1142  QString error;
1143  if ((message == "PREVIEW_SUCCESS" || message == "PREVIEW_QUEUED") &&
1144  me->ExtraDataCount() >= 5)
1145  {
1146  bool ok = true;
1147  uint recordingID = me->ExtraData(0).toUInt(); // pginfo->GetRecordingID()
1148  const QString& filename = me->ExtraData(1); // outFileName
1149  const QString& msg = me->ExtraData(2);
1150  const QString& datetime = me->ExtraData(3);
1151 
1152  if (message == "PREVIEW_QUEUED")
1153  {
1154  LOG(VB_PLAYBACK, LOG_INFO, LOC +
1155  QString("Preview Queued: '%1' '%2'")
1156  .arg(recordingID).arg(filename));
1157  return;
1158  }
1159 
1160  QFile file(filename);
1161  ok = ok && file.open(QIODevice::ReadOnly);
1162 
1163  if (ok)
1164  {
1165  QByteArray data = file.readAll();
1166  QStringList extra("OK");
1167  extra.push_back(QString::number(recordingID));
1168  extra.push_back(msg);
1169  extra.push_back(datetime);
1170  extra.push_back(QString::number(data.size()));
1171  extra.push_back(
1172  QString::number(qChecksum(data.constData(), data.size())));
1173  extra.push_back(QString(data.toBase64()));
1174 
1175  for (uint i = 4 ; i < (uint) me->ExtraDataCount(); i++)
1176  {
1177  const QString& token = me->ExtraData(i);
1178  extra.push_back(token);
1179  RequestedBy::iterator it = m_previewRequestedBy.find(token);
1180  if (it != m_previewRequestedBy.end())
1181  {
1182  receivers.insert(*it);
1183  m_previewRequestedBy.erase(it);
1184  }
1185  }
1186 
1187  if (receivers.empty())
1188  {
1189  LOG(VB_GENERAL, LOG_ERR, LOC +
1190  "PREVIEW_SUCCESS but no receivers.");
1191  return;
1192  }
1193 
1194  broadcast.push_back("BACKEND_MESSAGE");
1195  broadcast.push_back("GENERATED_PIXMAP");
1196  broadcast += extra;
1197  }
1198  else
1199  {
1200  message = "PREVIEW_FAILED";
1201  error = QString("Failed to read '%1'").arg(filename);
1202  LOG(VB_GENERAL, LOG_ERR, LOC + error);
1203  }
1204  }
1205 
1206  if (message == "PREVIEW_FAILED" && me->ExtraDataCount() >= 5)
1207  {
1208  const QString& pginfokey = me->ExtraData(0); // pginfo->MakeUniqueKey()
1209  const QString& msg = me->ExtraData(2);
1210 
1211  QStringList extra("ERROR");
1212  extra.push_back(pginfokey);
1213  extra.push_back(msg);
1214  for (uint i = 4 ; i < (uint) me->ExtraDataCount(); i++)
1215  {
1216  const QString& token = me->ExtraData(i);
1217  extra.push_back(token);
1218  RequestedBy::iterator it = m_previewRequestedBy.find(token);
1219  if (it != m_previewRequestedBy.end())
1220  {
1221  receivers.insert(*it);
1222  m_previewRequestedBy.erase(it);
1223  }
1224  }
1225 
1226  if (receivers.empty())
1227  {
1228  LOG(VB_GENERAL, LOG_ERR, LOC +
1229  "PREVIEW_FAILED but no receivers.");
1230  return;
1231  }
1232 
1233  broadcast.push_back("BACKEND_MESSAGE");
1234  broadcast.push_back("GENERATED_PIXMAP");
1235  broadcast += extra;
1236  }
1237 
1238  if (me->Message().startsWith("AUTO_EXPIRE"))
1239  {
1240 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1241  QStringList tokens = me->Message()
1242  .split(" ", QString::SkipEmptyParts);
1243 #else
1244  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1245 #endif
1246 
1247  if (tokens.size() != 3)
1248  {
1249  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad AUTO_EXPIRE message");
1250  return;
1251  }
1252 
1253  QDateTime startts = MythDate::fromString(tokens[2]);
1254  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1255 
1256  if (recInfo.GetChanID())
1257  {
1258  SendMythSystemPlayEvent("REC_EXPIRED", &recInfo);
1259 
1260  // allow re-record if auto expired but not expired live
1261  // or already "deleted" programs
1262  if (recInfo.GetRecordingGroup() != "LiveTV" &&
1263  recInfo.GetRecordingGroup() != "Deleted" &&
1264  (gCoreContext->GetBoolSetting("RerecordWatched", false) ||
1265  !recInfo.IsWatched()))
1266  {
1267  recInfo.ForgetHistory();
1268  }
1269  DoHandleDeleteRecording(recInfo, nullptr, false, true, false);
1270  }
1271  else
1272  {
1273  QString msg = QString("Cannot find program info for '%1', "
1274  "while attempting to Auto-Expire.")
1275  .arg(me->Message());
1276  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
1277  }
1278 
1279  return;
1280  }
1281 
1282  if (me->Message().startsWith("QUERY_NEXT_LIVETV_DIR") && m_sched)
1283  {
1284 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1285  QStringList tokens = me->Message()
1286  .split(" ", QString::SkipEmptyParts);
1287 #else
1288  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1289 #endif
1290 
1291  if (tokens.size() != 2)
1292  {
1293  LOG(VB_GENERAL, LOG_ERR, LOC +
1294  QString("Bad %1 message").arg(tokens[0]));
1295  return;
1296  }
1297 
1298  m_sched->GetNextLiveTVDir(tokens[1].toInt());
1299  return;
1300  }
1301 
1302  if (me->Message().startsWith("STOP_RECORDING"))
1303  {
1304 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1305  QStringList tokens = me->Message().split(" ",
1306  QString::SkipEmptyParts);
1307 #else
1308  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1309 #endif
1310 
1311 
1312  if (tokens.size() < 3 || tokens.size() > 3)
1313  {
1314  LOG(VB_GENERAL, LOG_ERR, LOC +
1315  QString("Bad STOP_RECORDING message: %1")
1316  .arg(me->Message()));
1317  return;
1318  }
1319 
1320  QDateTime startts = MythDate::fromString(tokens[2]);
1321  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1322 
1323  if (recInfo.GetChanID())
1324  {
1325  DoHandleStopRecording(recInfo, nullptr);
1326  }
1327  else
1328  {
1329  LOG(VB_GENERAL, LOG_ERR, LOC +
1330  QString("Cannot find program info for '%1' while "
1331  "attempting to stop recording.").arg(me->Message()));
1332  }
1333 
1334  return;
1335  }
1336 
1337  if ((me->Message().startsWith("DELETE_RECORDING")) ||
1338  (me->Message().startsWith("FORCE_DELETE_RECORDING")))
1339  {
1340 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1341  QStringList tokens = me->Message()
1342  .split(" ", QString::SkipEmptyParts);
1343 #else
1344  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1345 #endif
1346 
1347 
1348  if (tokens.size() < 3 || tokens.size() > 5)
1349  {
1350  LOG(VB_GENERAL, LOG_ERR, LOC +
1351  QString("Bad %1 message").arg(tokens[0]));
1352  return;
1353  }
1354 
1355  bool force = (tokens.size() >= 4) && (tokens[3] == "FORCE");
1356  bool forget = (tokens.size() >= 5) && (tokens[4] == "FORGET");
1357 
1358  QDateTime startts = MythDate::fromString(tokens[2]);
1359  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1360 
1361  if (recInfo.GetChanID())
1362  {
1363  if (tokens[0] == "FORCE_DELETE_RECORDING")
1364  DoHandleDeleteRecording(recInfo, nullptr, true, false, forget);
1365  else
1366  DoHandleDeleteRecording(recInfo, nullptr, force, false, forget);
1367  }
1368  else
1369  {
1370  LOG(VB_GENERAL, LOG_ERR, LOC +
1371  QString("Cannot find program info for '%1' while "
1372  "attempting to delete.").arg(me->Message()));
1373  }
1374 
1375  return;
1376  }
1377 
1378  if (me->Message().startsWith("UNDELETE_RECORDING"))
1379  {
1380 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1381  QStringList tokens = me->Message().split(" ",
1382  QString::SkipEmptyParts);
1383 #else
1384  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1385 #endif
1386 
1387 
1388  if (tokens.size() < 3 || tokens.size() > 3)
1389  {
1390  LOG(VB_GENERAL, LOG_ERR, LOC +
1391  QString("Bad UNDELETE_RECORDING message: %1")
1392  .arg(me->Message()));
1393  return;
1394  }
1395 
1396  QDateTime startts = MythDate::fromString(tokens[2]);
1397  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1398 
1399  if (recInfo.GetChanID())
1400  {
1401  DoHandleUndeleteRecording(recInfo, nullptr);
1402  }
1403  else
1404  {
1405  LOG(VB_GENERAL, LOG_ERR, LOC +
1406  QString("Cannot find program info for '%1' while "
1407  "attempting to undelete.").arg(me->Message()));
1408  }
1409 
1410  return;
1411  }
1412 
1413  if (me->Message().startsWith("ADD_CHILD_INPUT"))
1414  {
1415 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1416  QStringList tokens = me->Message()
1417  .split(" ", QString::SkipEmptyParts);
1418 #else
1419  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1420 #endif
1421  if (!m_ismaster)
1422  {
1423  LOG(VB_GENERAL, LOG_ERR, LOC +
1424  "ADD_CHILD_INPUT event received in slave context");
1425  }
1426  else if (tokens.size() != 2)
1427  {
1428  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad ADD_CHILD_INPUT message");
1429  }
1430  else
1431  {
1432  HandleAddChildInput(tokens[1].toUInt());
1433  }
1434  return;
1435  }
1436 
1437  if (me->Message().startsWith("RESCHEDULE_RECORDINGS") && m_sched)
1438  {
1439  const QStringList& request = me->ExtraDataList();
1440  m_sched->Reschedule(request);
1441  return;
1442  }
1443 
1444  if (me->Message().startsWith("SCHEDULER_ADD_RECORDING") && m_sched)
1445  {
1446  ProgramInfo pi(me->ExtraDataList());
1447  if (!pi.GetChanID())
1448  {
1449  LOG(VB_GENERAL, LOG_ERR, LOC +
1450  "Bad SCHEDULER_ADD_RECORDING message");
1451  return;
1452  }
1453 
1454  m_sched->AddRecording(pi);
1455  return;
1456  }
1457 
1458  if (me->Message().startsWith("UPDATE_RECORDING_STATUS") && m_sched)
1459  {
1460 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1461  QStringList tokens = me->Message()
1462  .split(" ", QString::SkipEmptyParts);
1463 #else
1464  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1465 #endif
1466 
1467  if (tokens.size() != 6)
1468  {
1469  LOG(VB_GENERAL, LOG_ERR, LOC +
1470  "Bad UPDATE_RECORDING_STATUS message");
1471  return;
1472  }
1473 
1474  uint cardid = tokens[1].toUInt();
1475  uint chanid = tokens[2].toUInt();
1476  QDateTime startts = MythDate::fromString(tokens[3]);
1477  auto recstatus = RecStatus::Type(tokens[4].toInt());
1478  QDateTime recendts = MythDate::fromString(tokens[5]);
1479  m_sched->UpdateRecStatus(cardid, chanid, startts,
1480  recstatus, recendts);
1481 
1483  return;
1484  }
1485 
1486  if (me->Message().startsWith("LIVETV_EXITED"))
1487  {
1488  const QString& chainid = me->ExtraData();
1489  LiveTVChain *chain = GetExistingChain(chainid);
1490  if (chain)
1491  DeleteChain(chain);
1492 
1493  return;
1494  }
1495 
1496  if (me->Message() == "CLEAR_SETTINGS_CACHE")
1498 
1499  if (me->Message().startsWith("RESET_IDLETIME") && m_sched)
1501 
1502  if (me->Message() == "LOCAL_RECONNECT_TO_MASTER")
1504 
1505  if (me->Message() == "LOCAL_SLAVE_BACKEND_ENCODERS_OFFLINE")
1507 
1508  if (me->Message().startsWith("LOCAL_"))
1509  return;
1510 
1511  if (me->Message() == "CREATE_THUMBNAILS")
1512  ImageManagerBe::getInstance()->HandleCreateThumbnails(me->ExtraDataList());
1513 
1514  if (me->Message() == "IMAGE_GET_METADATA")
1515  ImageManagerBe::getInstance()->HandleGetMetadata(me->ExtraData());
1516 
1517  MythEvent mod_me("");
1518  if (me->Message().startsWith("MASTER_UPDATE_REC_INFO"))
1519  {
1520  QStringList tokens = me->Message().simplified().split(" ");
1521  uint recordedid = 0;
1522  if (tokens.size() >= 2)
1523  recordedid = tokens[1].toUInt();
1524 
1525  ProgramInfo evinfo(recordedid);
1526  if (evinfo.GetChanID())
1527  {
1528  QDateTime rectime = MythDate::current().addSecs(
1529  -gCoreContext->GetNumSetting("RecordOverTime"));
1530 
1531  if (m_sched && evinfo.GetRecordingEndTime() > rectime)
1532  evinfo.SetRecordingStatus(m_sched->GetRecStatus(evinfo));
1533 
1534  QStringList list;
1535  evinfo.ToStringList(list);
1536  mod_me = MythEvent("RECORDING_LIST_CHANGE UPDATE", list);
1537  me = &mod_me;
1538  }
1539  else
1540  {
1541  return;
1542  }
1543  }
1544 
1545  if (me->Message().startsWith("DOWNLOAD_FILE"))
1546  {
1547  QStringList extraDataList = me->ExtraDataList();
1548  QString localFile = extraDataList[1];
1549  QFile file(localFile);
1550  QStringList tokens = me->Message().simplified().split(" ");
1551  QMutexLocker locker(&m_downloadURLsLock);
1552 
1553  if (!m_downloadURLs.contains(localFile))
1554  return;
1555 
1556  extraDataList[1] = m_downloadURLs[localFile];
1557 
1558  if ((tokens.size() >= 2) && (tokens[1] == "FINISHED"))
1559  m_downloadURLs.remove(localFile);
1560 
1561  mod_me = MythEvent(me->Message(), extraDataList);
1562  me = &mod_me;
1563  }
1564 
1565  if (broadcast.empty())
1566  {
1567  broadcast.push_back("BACKEND_MESSAGE");
1568  broadcast.push_back(me->Message());
1569  broadcast += me->ExtraDataList();
1570  }
1571  }
1572 
1573  if (!broadcast.empty())
1574  {
1575  // Make a local copy of the list, upping the refcount as we go..
1576  vector<PlaybackSock *> localPBSList;
1577  m_sockListLock.lockForRead();
1578  for (auto & pbs : m_playbackList)
1579  {
1580  pbs->IncrRef();
1581  localPBSList.push_back(pbs);
1582  }
1583  m_sockListLock.unlock();
1584 
1585  bool sendGlobal = false;
1586  if (m_ismaster && broadcast[1].startsWith("GLOBAL_"))
1587  {
1588  broadcast[1].replace("GLOBAL_", "LOCAL_");
1589  MythEvent me(broadcast[1], broadcast[2]);
1590  gCoreContext->dispatch(me);
1591 
1592  sendGlobal = true;
1593  }
1594 
1595  QSet<PlaybackSock*> sentSet;
1596 
1597  bool isSystemEvent = broadcast[1].startsWith("SYSTEM_EVENT ");
1598  QStringList sentSetSystemEvent(gCoreContext->GetHostName());
1599 
1600  vector<PlaybackSock*>::const_iterator iter;
1601  for (iter = localPBSList.begin(); iter != localPBSList.end(); ++iter)
1602  {
1603  PlaybackSock *pbs = *iter;
1604 
1605  if (sentSet.contains(pbs) || pbs->IsDisconnected())
1606  continue;
1607 
1608  if (!receivers.empty() && !receivers.contains(pbs->getHostname()))
1609  continue;
1610 
1611  sentSet.insert(pbs);
1612 
1613  bool reallysendit = false;
1614 
1615  if (broadcast[1] == "CLEAR_SETTINGS_CACHE")
1616  {
1617  if ((m_ismaster) &&
1618  (pbs->isSlaveBackend() || pbs->wantsEvents()))
1619  reallysendit = true;
1620  }
1621  else if (sendGlobal)
1622  {
1623  if (pbs->isSlaveBackend())
1624  reallysendit = true;
1625  }
1626  else if (pbs->wantsEvents())
1627  {
1628  reallysendit = true;
1629  }
1630 
1631  if (reallysendit)
1632  {
1633  if (isSystemEvent)
1634  {
1635  if (!pbs->wantsSystemEvents())
1636  {
1637  continue;
1638  }
1639  if (!pbs->wantsOnlySystemEvents())
1640  {
1641  if (sentSetSystemEvent.contains(pbs->getHostname()))
1642  continue;
1643 
1644  sentSetSystemEvent << pbs->getHostname();
1645  }
1646  }
1647  else if (pbs->wantsOnlySystemEvents())
1648  continue;
1649  }
1650 
1651  MythSocket *sock = pbs->getSocket();
1652  if (reallysendit && sock->IsConnected())
1653  sock->WriteStringList(broadcast);
1654  }
1655 
1656  // Done with the pbs list, so decrement all the instances..
1657  for (iter = localPBSList.begin(); iter != localPBSList.end(); ++iter)
1658  {
1659  PlaybackSock *pbs = *iter;
1660  pbs->DecrRef();
1661  }
1662  }
1663 }
1664 
1673 void MainServer::HandleVersion(MythSocket *socket, const QStringList &slist)
1674 {
1675  QStringList retlist;
1676  QString version = slist[1];
1677  if (version != MYTH_PROTO_VERSION)
1678  {
1679  LOG(VB_GENERAL, LOG_CRIT, LOC +
1680  "MainServer::HandleVersion - Client speaks protocol version " +
1681  version + " but we speak " + MYTH_PROTO_VERSION + '!');
1682  retlist << "REJECT" << MYTH_PROTO_VERSION;
1683  socket->WriteStringList(retlist);
1684  HandleDone(socket);
1685  return;
1686  }
1687 
1688  if (slist.size() < 3)
1689  {
1690  LOG(VB_GENERAL, LOG_CRIT, LOC +
1691  "MainServer::HandleVersion - Client did not pass protocol "
1692  "token. Refusing connection!");
1693  retlist << "REJECT" << MYTH_PROTO_VERSION;
1694  socket->WriteStringList(retlist);
1695  HandleDone(socket);
1696  return;
1697  }
1698 
1699  QString token = slist[2];
1700  if (token != QString::fromUtf8(MYTH_PROTO_TOKEN))
1701  {
1702  LOG(VB_GENERAL, LOG_CRIT, LOC +
1703  QString("MainServer::HandleVersion - Client sent incorrect "
1704  "protocol token \"%1\" for protocol version. Refusing "
1705  "connection!").arg(token));
1706  retlist << "REJECT" << MYTH_PROTO_VERSION;
1707  socket->WriteStringList(retlist);
1708  HandleDone(socket);
1709  return;
1710  }
1711 
1712  retlist << "ACCEPT" << MYTH_PROTO_VERSION;
1713  socket->WriteStringList(retlist);
1714 }
1715 
1738 void MainServer::HandleAnnounce(QStringList &slist, QStringList commands,
1739  MythSocket *socket)
1740 {
1741  QStringList retlist( "OK" );
1742  QStringList errlist( "ERROR" );
1743 
1744  if (commands.size() < 3 || commands.size() > 6)
1745  {
1746  QString info = "";
1747  if (commands.size() == 2)
1748  info = QString(" %1").arg(commands[1]);
1749 
1750  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Received malformed ANN%1 query")
1751  .arg(info));
1752 
1753  errlist << "malformed_ann_query";
1754  socket->WriteStringList(errlist);
1755  return;
1756  }
1757 
1758  m_sockListLock.lockForRead();
1759  for (auto *pbs : m_playbackList)
1760  {
1761  if (pbs->getSocket() == socket)
1762  {
1763  LOG(VB_GENERAL, LOG_WARNING, LOC +
1764  QString("Client %1 is trying to announce a socket "
1765  "multiple times.")
1766  .arg(commands[2]));
1767  socket->WriteStringList(retlist);
1768  m_sockListLock.unlock();
1769  return;
1770  }
1771  }
1772  m_sockListLock.unlock();
1773 
1774  if (commands[1] == "Playback" || commands[1] == "Monitor" ||
1775  commands[1] == "Frontend")
1776  {
1777  if (commands.size() < 4)
1778  {
1779  LOG(VB_GENERAL, LOG_ERR, LOC +
1780  QString("Received malformed ANN %1 query")
1781  .arg(commands[1]));
1782 
1783  errlist << "malformed_ann_query";
1784  socket->WriteStringList(errlist);
1785  return;
1786  }
1787 
1788  // Monitor connections are same as Playback but they don't
1789  // block shutdowns. See the Scheduler event loop for more.
1790 
1791  auto eventsMode = (PlaybackSockEventsMode)commands[3].toInt();
1792 
1793  QWriteLocker lock(&m_sockListLock);
1794  if (!m_controlSocketList.remove(socket))
1795  return; // socket was disconnected
1796  auto *pbs = new PlaybackSock(this, socket, commands[2], eventsMode);
1797  m_playbackList.push_back(pbs);
1798  lock.unlock();
1799 
1800  LOG(VB_GENERAL, LOG_INFO, LOC + QString("MainServer::ANN %1")
1801  .arg(commands[1]));
1802  LOG(VB_GENERAL, LOG_INFO, LOC +
1803  QString("adding: %1(%2) as a client (events: %3)")
1804  .arg(commands[2])
1805  .arg(quintptr(socket),0,16)
1806  .arg(eventsMode));
1807  pbs->setBlockShutdown((commands[1] == "Playback") ||
1808  (commands[1] == "Frontend"));
1809 
1810  if (commands[1] == "Frontend")
1811  {
1812  pbs->SetAsFrontend();
1813  auto *frontend = new Frontend();
1814  frontend->m_name = commands[2];
1815  // On a combined mbe/fe the frontend will connect using the localhost
1816  // address, we need the external IP which happily will be the same as
1817  // the backend's external IP
1818  if (frontend->m_name == gCoreContext->GetMasterHostName())
1819  frontend->m_ip = QHostAddress(gCoreContext->GetBackendServerIP());
1820  else
1821  frontend->m_ip = socket->GetPeerAddress();
1822  if (gBackendContext)
1824  else
1825  delete frontend;
1826  }
1827 
1828  }
1829  else if (commands[1] == "MediaServer")
1830  {
1831  if (commands.size() < 3)
1832  {
1833  LOG(VB_GENERAL, LOG_ERR, LOC +
1834  "Received malformed ANN MediaServer query");
1835  errlist << "malformed_ann_query";
1836  socket->WriteStringList(errlist);
1837  return;
1838  }
1839 
1840  QWriteLocker lock(&m_sockListLock);
1841  if (!m_controlSocketList.remove(socket))
1842  return; // socket was disconnected
1843  auto *pbs = new PlaybackSock(this, socket, commands[2],
1845  pbs->setAsMediaServer();
1846  pbs->setBlockShutdown(false);
1847  m_playbackList.push_back(pbs);
1848  lock.unlock();
1849 
1851  QString("CLIENT_CONNECTED HOSTNAME %1").arg(commands[2]));
1852  }
1853  else if (commands[1] == "SlaveBackend")
1854  {
1855  if (commands.size() < 4)
1856  {
1857  LOG(VB_GENERAL, LOG_ERR, LOC +
1858  QString("Received malformed ANN %1 query")
1859  .arg(commands[1]));
1860  errlist << "malformed_ann_query";
1861  socket->WriteStringList(errlist);
1862  return;
1863  }
1864 
1865  QWriteLocker lock(&m_sockListLock);
1866  if (!m_controlSocketList.remove(socket))
1867  return; // socket was disconnected
1868  auto *pbs = new PlaybackSock(this, socket, commands[2],
1869  kPBSEvents_None);
1870  m_playbackList.push_back(pbs);
1871  lock.unlock();
1872 
1873  LOG(VB_GENERAL, LOG_INFO, LOC +
1874  QString("adding: %1 as a slave backend server")
1875  .arg(commands[2]));
1876  pbs->setAsSlaveBackend();
1877  pbs->setIP(commands[3]);
1878 
1879  if (m_sched)
1880  {
1881  RecordingList slavelist;
1882  QStringList::const_iterator sit = slist.begin()+1;
1883  while (sit != slist.end())
1884  {
1885  auto *recinfo = new RecordingInfo(sit, slist.end());
1886  if (!recinfo->GetChanID())
1887  {
1888  delete recinfo;
1889  break;
1890  }
1891  slavelist.push_back(recinfo);
1892  }
1893  m_sched->SlaveConnected(slavelist);
1894  }
1895 
1896  bool wasAsleep = true;
1897  TVRec::s_inputsLock.lockForRead();
1898  for (auto * elink : qAsConst(*m_encoderList))
1899  {
1900  if (elink->GetHostName() == commands[2])
1901  {
1902  if (! (elink->IsWaking() || elink->IsAsleep()))
1903  wasAsleep = false;
1904  elink->SetSocket(pbs);
1905  }
1906  }
1907  TVRec::s_inputsLock.unlock();
1908 
1909  if (!wasAsleep && m_sched)
1910  m_sched->ReschedulePlace("SlaveConnected");
1911 
1912  QString message = QString("LOCAL_SLAVE_BACKEND_ONLINE %2")
1913  .arg(commands[2]);
1914  MythEvent me(message);
1915  gCoreContext->dispatch(me);
1916 
1917  pbs->setBlockShutdown(false);
1918 
1919  m_autoexpireUpdateTimer->start(1000);
1920 
1922  QString("SLAVE_CONNECTED HOSTNAME %1").arg(commands[2]));
1923  }
1924  else if (commands[1] == "FileTransfer")
1925  {
1926  if (slist.size() < 3)
1927  {
1928  LOG(VB_GENERAL, LOG_ERR, LOC +
1929  "Received malformed FileTransfer command");
1930  errlist << "malformed_filetransfer_command";
1931  socket->WriteStringList(errlist);
1932  return;
1933  }
1934 
1935  LOG(VB_NETWORK, LOG_INFO, LOC +
1936  "MainServer::HandleAnnounce FileTransfer");
1937  LOG(VB_NETWORK, LOG_INFO, LOC +
1938  QString("adding: %1 as a remote file transfer") .arg(commands[2]));
1939  QStringList::const_iterator it = slist.begin();
1940  QString path = *(++it);
1941  QString wantgroup = *(++it);
1942  QString filename;
1943  QStringList checkfiles;
1944 
1945  for (++it; it != slist.end(); ++it)
1946  checkfiles += *it;
1947 
1948  FileTransfer *ft = nullptr;
1949  bool writemode = false;
1950  bool usereadahead = true;
1951  int timeout_ms = 2000;
1952  if (commands.size() > 3)
1953  writemode = (commands[3].toInt() != 0);
1954 
1955  if (commands.size() > 4)
1956  usereadahead = (commands[4].toInt() != 0);
1957 
1958  if (commands.size() > 5)
1959  timeout_ms = commands[5].toInt();
1960 
1961  if (writemode)
1962  {
1963  if (wantgroup.isEmpty())
1964  wantgroup = "Default";
1965 
1966  StorageGroup sgroup(wantgroup, gCoreContext->GetHostName(), false);
1967  QString dir = sgroup.FindNextDirMostFree();
1968  if (dir.isEmpty())
1969  {
1970  LOG(VB_GENERAL, LOG_ERR, LOC + "Unable to determine directory "
1971  "to write to in FileTransfer write command");
1972  errlist << "filetransfer_directory_not_found";
1973  socket->WriteStringList(errlist);
1974  return;
1975  }
1976 
1977  if (path.isEmpty())
1978  {
1979  LOG(VB_GENERAL, LOG_ERR, LOC +
1980  QString("FileTransfer write filename is empty in path '%1'.")
1981  .arg(path));
1982  errlist << "filetransfer_filename_empty";
1983  socket->WriteStringList(errlist);
1984  return;
1985  }
1986 
1987  if ((path.contains("/../")) ||
1988  (path.startsWith("../")))
1989  {
1990  LOG(VB_GENERAL, LOG_ERR, LOC +
1991  QString("FileTransfer write filename '%1' does not pass "
1992  "sanity checks.") .arg(path));
1993  errlist << "filetransfer_filename_dangerous";
1994  socket->WriteStringList(errlist);
1995  return;
1996  }
1997 
1998  filename = dir + "/" + path;
1999  }
2000  else
2001  filename = LocalFilePath(path, wantgroup);
2002 
2003  if (filename.isEmpty())
2004  {
2005  LOG(VB_GENERAL, LOG_ERR, LOC + "Empty filename, cowardly aborting!");
2006  errlist << "filetransfer_filename_empty";
2007  socket->WriteStringList(errlist);
2008  return;
2009  }
2010 
2011 
2012  QFileInfo finfo(filename);
2013  if (finfo.isDir())
2014  {
2015  LOG(VB_GENERAL, LOG_ERR, LOC +
2016  QString("FileTransfer filename '%1' is actually a directory, "
2017  "cannot transfer.") .arg(filename));
2018  errlist << "filetransfer_filename_is_a_directory";
2019  socket->WriteStringList(errlist);
2020  return;
2021  }
2022 
2023  if (writemode)
2024  {
2025  QString dirPath = finfo.absolutePath();
2026  QDir qdir(dirPath);
2027  if (!qdir.exists())
2028  {
2029  if (!qdir.mkpath(dirPath))
2030  {
2031  LOG(VB_GENERAL, LOG_ERR, LOC +
2032  QString("FileTransfer filename '%1' is in a "
2033  "subdirectory which does not exist, and can "
2034  "not be created.") .arg(filename));
2035  errlist << "filetransfer_unable_to_create_subdirectory";
2036  socket->WriteStringList(errlist);
2037  return;
2038  }
2039  }
2040  QWriteLocker lock(&m_sockListLock);
2041  if (!m_controlSocketList.remove(socket))
2042  return; // socket was disconnected
2043  ft = new FileTransfer(filename, socket, writemode);
2044  }
2045  else
2046  {
2047  QWriteLocker lock(&m_sockListLock);
2048  if (!m_controlSocketList.remove(socket))
2049  return; // socket was disconnected
2050  ft = new FileTransfer(filename, socket, usereadahead, timeout_ms);
2051  }
2052 
2053  if (!ft->isOpen())
2054  {
2055  LOG(VB_GENERAL, LOG_ERR, LOC +
2056  QString("Can't open %1").arg(filename));
2057  errlist << "filetransfer_unable_to_open_file";
2058  socket->WriteStringList(errlist);
2059  socket->IncrRef(); // FileTransfer took ownership of the socket, take it back
2060  ft->DecrRef();
2061  return;
2062  }
2063  ft->IncrRef();
2064  LOG(VB_GENERAL, LOG_INFO, LOC +
2065  QString("adding: %1(%2) as a file transfer")
2066  .arg(commands[2])
2067  .arg(quintptr(socket),0,16));
2068  m_sockListLock.lockForWrite();
2069  m_fileTransferList.push_back(ft);
2070  m_sockListLock.unlock();
2071 
2072  retlist << QString::number(socket->GetSocketDescriptor());
2073  retlist << QString::number(ft->GetFileSize());
2074 
2075  ft->DecrRef();
2076 
2077  if (!checkfiles.empty())
2078  {
2079  QFileInfo fi(filename);
2080  QDir dir = fi.absoluteDir();
2081  for (it = checkfiles.begin(); it != checkfiles.end(); ++it)
2082  {
2083  if (dir.exists(*it) &&
2084  ((*it).endsWith(".srt") ||
2085  QFileInfo(dir, *it).size() >= kReadTestSize))
2086  {
2087  retlist<<*it;
2088  }
2089  }
2090  }
2091  }
2092 
2093  socket->WriteStringList(retlist);
2095 }
2096 
2103 {
2104  socket->DisconnectFromHost();
2106 }
2107 
2109 {
2110  SendErrorResponse(pbs->getSocket(), error);
2111 }
2112 
2114 {
2115  LOG(VB_GENERAL, LOG_ERR, LOC + error);
2116 
2117  QStringList strList("ERROR");
2118  strList << error;
2119 
2120  SendResponse(sock, strList);
2121 }
2122 
2123 void MainServer::SendResponse(MythSocket *socket, QStringList &commands)
2124 {
2125  // Note: this method assumes that the playback or filetransfer
2126  // handler has already been uprefed and the socket as well.
2127 
2128  // These checks are really just to check if the socket has
2129  // been remotely disconnected while we were working on the
2130  // response.
2131 
2132  bool do_write = false;
2133  if (socket)
2134  {
2135  m_sockListLock.lockForRead();
2136  do_write = (GetPlaybackBySock(socket) ||
2137  GetFileTransferBySock(socket));
2138  m_sockListLock.unlock();
2139  }
2140 
2141  if (do_write)
2142  {
2143  socket->WriteStringList(commands);
2144  }
2145  else
2146  {
2147  LOG(VB_GENERAL, LOG_ERR, LOC +
2148  "SendResponse: Unable to write to client socket, as it's no "
2149  "longer there");
2150  }
2151 }
2152 
2162 {
2163  MythSocket *pbssock = pbs->getSocket();
2164  QString playbackhost = pbs->getHostname();
2165 
2166  QMap<QString,ProgramInfo*> recMap;
2167  if (m_sched)
2168  recMap = m_sched->GetRecording();
2169 
2170  QMap<QString,uint32_t> inUseMap = ProgramInfo::QueryInUseMap();
2171  QMap<QString,bool> isJobRunning =
2173 
2174  int sort = 0;
2175  // Allow "Play" and "Delete" for backwards compatibility with protocol
2176  // version 56 and below.
2177  if ((type == "Ascending") || (type == "Play"))
2178  sort = 1;
2179  else if ((type == "Descending") || (type == "Delete"))
2180  sort = -1;
2181 
2182  ProgramList destination;
2184  destination, (type == "Recording"),
2185  inUseMap, isJobRunning, recMap, sort);
2186 
2187  QMap<QString,ProgramInfo*>::iterator mit = recMap.begin();
2188  for (; mit != recMap.end(); mit = recMap.erase(mit))
2189  delete *mit;
2190 
2191  QStringList outputlist(QString::number(destination.size()));
2192  QMap<QString, int> backendPortMap;
2193  int port = gCoreContext->GetBackendServerPort();
2194  QString host = gCoreContext->GetHostName();
2195 
2196  auto it = destination.begin();
2197  for (it = destination.begin(); it != destination.end(); ++it)
2198  {
2199  ProgramInfo *proginfo = *it;
2200  PlaybackSock *slave = nullptr;
2201 
2202  if (proginfo->GetHostname() != gCoreContext->GetHostName())
2203  slave = GetSlaveByHostname(proginfo->GetHostname());
2204 
2205  if ((proginfo->GetHostname() == gCoreContext->GetHostName()) ||
2206  (!slave && m_masterBackendOverride))
2207  {
2208  proginfo->SetPathname(MythCoreContext::GenMythURL(host,port,
2209  proginfo->GetBasename()));
2210  if (!proginfo->GetFilesize())
2211  {
2212  QString tmpURL = GetPlaybackURL(proginfo);
2213  if (tmpURL.startsWith('/'))
2214  {
2215  QFile checkFile(tmpURL);
2216  if (!tmpURL.isEmpty() && checkFile.exists())
2217  {
2218  proginfo->SetFilesize(checkFile.size());
2219  if (proginfo->GetRecordingEndTime() <
2221  {
2222  proginfo->SaveFilesize(proginfo->GetFilesize());
2223  }
2224  }
2225  }
2226  }
2227  }
2228  else if (!slave)
2229  {
2230  proginfo->SetPathname(GetPlaybackURL(proginfo));
2231  if (proginfo->GetPathname().isEmpty())
2232  {
2233  LOG(VB_GENERAL, LOG_ERR, LOC +
2234  QString("HandleQueryRecordings() "
2235  "Couldn't find backend for:\n\t\t\t%1")
2237 
2238  proginfo->SetFilesize(0);
2239  proginfo->SetPathname("file not found");
2240  }
2241  }
2242  else
2243  {
2244  if (!proginfo->GetFilesize())
2245  {
2246  if (!slave->FillProgramInfo(*proginfo, playbackhost))
2247  {
2248  LOG(VB_GENERAL, LOG_ERR, LOC +
2249  "MainServer::HandleQueryRecordings()"
2250  "\n\t\t\tCould not fill program info "
2251  "from backend");
2252  }
2253  else
2254  {
2255  if (proginfo->GetRecordingEndTime() <
2257  {
2258  proginfo->SaveFilesize(proginfo->GetFilesize());
2259  }
2260  }
2261  }
2262  else
2263  {
2264  ProgramInfo *p = proginfo;
2265  QString hostname = p->GetHostname();
2266 
2267  if (!backendPortMap.contains(hostname))
2268  backendPortMap[hostname] = gCoreContext->GetBackendServerPort(hostname);
2269 
2270  p->SetPathname(MythCoreContext::GenMythURL(hostname,
2271  backendPortMap[hostname],
2272  p->GetBasename()));
2273  }
2274  }
2275 
2276  if (slave)
2277  slave->DecrRef();
2278 
2279  proginfo->ToStringList(outputlist);
2280  }
2281 
2282  SendResponse(pbssock, outputlist);
2283 }
2284 
2291 {
2292  if (slist.size() < 3)
2293  {
2294  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad QUERY_RECORDING query");
2295  return;
2296  }
2297 
2298  MythSocket *pbssock = pbs->getSocket();
2299  QString command = slist[1].toUpper();
2300  ProgramInfo *pginfo = nullptr;
2301 
2302  if (command == "BASENAME")
2303  {
2304  pginfo = new ProgramInfo(slist[2]);
2305  }
2306  else if (command == "TIMESLOT")
2307  {
2308  if (slist.size() < 4)
2309  {
2310  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad QUERY_RECORDING query");
2311  return;
2312  }
2313 
2314  QDateTime recstartts = MythDate::fromString(slist[3]);
2315  pginfo = new ProgramInfo(slist[2].toUInt(), recstartts);
2316  }
2317 
2318  QStringList strlist;
2319 
2320  if (pginfo && pginfo->GetChanID())
2321  {
2322  strlist << "OK";
2323  pginfo->ToStringList(strlist);
2324  }
2325  else
2326  {
2327  strlist << "ERROR";
2328  }
2329 
2330  delete pginfo;
2331 
2332  SendResponse(pbssock, strlist);
2333 }
2334 
2336 {
2337  MythSocket *pbssock = pbs->getSocket();
2338 
2339  QString playbackhost = slist[1];
2340 
2341  QStringList::const_iterator it = slist.begin() + 2;
2342  ProgramInfo pginfo(it, slist.end());
2343 
2344  if (pginfo.HasPathname())
2345  {
2346  QString lpath = GetPlaybackURL(&pginfo);
2347  int port = gCoreContext->GetBackendServerPort();
2348  QString host = gCoreContext->GetHostName();
2349 
2350  if (playbackhost == gCoreContext->GetHostName())
2351  pginfo.SetPathname(lpath);
2352  else
2353  pginfo.SetPathname(MythCoreContext::GenMythURL(host,port,
2354  pginfo.GetBasename()));
2355 
2356  const QFileInfo info(lpath);
2357  pginfo.SetFilesize(info.size());
2358  }
2359 
2360  QStringList strlist;
2361 
2362  pginfo.ToStringList(strlist);
2363 
2364  SendResponse(pbssock, strlist);
2365 }
2366 
2367 
2368 void DeleteThread::run(void)
2369 {
2370  if (m_ms)
2371  m_ms->DoDeleteThread(this);
2372 }
2373 
2375 {
2376  // sleep a little to let frontends reload the recordings list
2377  // after deleting a recording, then we can hammer the DB and filesystem
2378  std::this_thread::sleep_for(std::chrono::seconds(3));
2379  std::this_thread::sleep_for(std::chrono::milliseconds(random()%2));
2380 
2381  m_deletelock.lock();
2382 
2383 #if 0
2384  QString logInfo = QString("recording id %1 (chanid %2 at %3)")
2385  .arg(ds->m_recordedid)
2386  .arg(ds->m_chanid)
2387  .arg(ds->m_recstartts.toString(Qt::ISODate));
2388 
2389  QString name = QString("deleteThread%1%2").arg(getpid()).arg(random());
2390 #endif
2391  QFile checkFile(ds->m_filename);
2392 
2394  {
2395  QString msg = QString("ERROR opening database connection for Delete "
2396  "Thread for chanid %1 recorded at %2. Program "
2397  "will NOT be deleted.")
2398  .arg(ds->m_chanid)
2399  .arg(ds->m_recstartts.toString(Qt::ISODate));
2400  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2401 
2402  m_deletelock.unlock();
2403  return;
2404  }
2405 
2406  ProgramInfo pginfo(ds->m_chanid, ds->m_recstartts);
2407 
2408  if (!pginfo.GetChanID())
2409  {
2410  QString msg = QString("ERROR retrieving program info when trying to "
2411  "delete program for chanid %1 recorded at %2. "
2412  "Recording will NOT be deleted.")
2413  .arg(ds->m_chanid)
2414  .arg(ds->m_recstartts.toString(Qt::ISODate));
2415  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2416 
2417  m_deletelock.unlock();
2418  return;
2419  }
2420 
2421  // Don't allow deleting files where filesize != 0 and we can't find
2422  // the file, unless forceMetadataDelete has been set. This allows
2423  // deleting failed recordings without fuss, but blocks accidental
2424  // deletion of metadata for files where the filesystem has gone missing.
2425  if ((!checkFile.exists()) && pginfo.GetFilesize() &&
2426  (!ds->m_forceMetadataDelete))
2427  {
2428  LOG(VB_GENERAL, LOG_ERR, LOC +
2429  QString("ERROR when trying to delete file: %1. File "
2430  "doesn't exist. Database metadata will not be removed.")
2431  .arg(ds->m_filename));
2432 
2433  pginfo.SaveDeletePendingFlag(false);
2434  m_deletelock.unlock();
2435  return;
2436  }
2437 
2439 
2440  LiveTVChain *tvchain = GetChainWithRecording(pginfo);
2441  if (tvchain)
2442  tvchain->DeleteProgram(&pginfo);
2443 
2444  bool followLinks = gCoreContext->GetBoolSetting("DeletesFollowLinks", false);
2445  bool slowDeletes = gCoreContext->GetBoolSetting("TruncateDeletesSlowly", false);
2446  int fd = -1;
2447  off_t size = 0;
2448  bool errmsg = false;
2449 
2450  //-----------------------------------------------------------------------
2451  // TODO Move the following into DeleteRecordedFiles
2452  //-----------------------------------------------------------------------
2453 
2454  // Delete recording.
2455  if (slowDeletes)
2456  {
2457  // Since stat fails after unlinking on some filesystems,
2458  // get the filesize first
2459  const QFileInfo info(ds->m_filename);
2460  size = info.size();
2461  fd = DeleteFile(ds->m_filename, followLinks, ds->m_forceMetadataDelete);
2462 
2463  if ((fd < 0) && checkFile.exists())
2464  errmsg = true;
2465  }
2466  else
2467  {
2468  delete_file_immediately(ds->m_filename, followLinks, false);
2469  std::this_thread::sleep_for(std::chrono::seconds(2));
2470  if (checkFile.exists())
2471  errmsg = true;
2472  }
2473 
2474  if (errmsg)
2475  {
2476  LOG(VB_GENERAL, LOG_ERR, LOC +
2477  QString("Error deleting file: %1. Keeping metadata in database.")
2478  .arg(ds->m_filename));
2479 
2480  pginfo.SaveDeletePendingFlag(false);
2481  m_deletelock.unlock();
2482  return;
2483  }
2484 
2485  // Delete all related files, though not the recording itself
2486  // i.e. preview thumbnails, srt subtitles, orphaned transcode temporary
2487  // files
2488  //
2489  // TODO: Delete everything with this basename to catch stray
2490  // .tmp and .old files, and future proof it
2491  QFileInfo fInfo( ds->m_filename );
2492  QStringList nameFilters;
2493  nameFilters.push_back(fInfo.fileName() + "*.png");
2494  nameFilters.push_back(fInfo.fileName() + "*.jpg");
2495  nameFilters.push_back(fInfo.fileName() + ".tmp");
2496  nameFilters.push_back(fInfo.fileName() + ".old");
2497  nameFilters.push_back(fInfo.fileName() + ".map");
2498  nameFilters.push_back(fInfo.fileName() + ".tmp.map");
2499  nameFilters.push_back(fInfo.baseName() + ".srt"); // e.g. 1234_20150213165800.srt
2500 
2501  QDir dir (fInfo.path());
2502  QFileInfoList miscFiles = dir.entryInfoList(nameFilters);
2503 
2504  for (const auto & file : qAsConst(miscFiles))
2505  {
2506  QString sFileName = file.absoluteFilePath();
2507  delete_file_immediately( sFileName, followLinks, true);
2508  }
2509  // -----------------------------------------------------------------------
2510 
2511  // TODO Have DeleteRecordedFiles do the deletion of all associated files
2512  DeleteRecordedFiles(ds);
2513 
2514  DoDeleteInDB(ds);
2515 
2516  m_deletelock.unlock();
2517 
2518  if (slowDeletes && fd >= 0)
2519  TruncateAndClose(&pginfo, fd, ds->m_filename, size);
2520 }
2521 
2523 {
2524  QString logInfo = QString("recording id %1 filename %2")
2525  .arg(ds->m_recordedid).arg(ds->m_filename);
2526 
2527  LOG(VB_GENERAL, LOG_NOTICE, "DeleteRecordedFiles - " + logInfo);
2528 
2529  MSqlQuery update(MSqlQuery::InitCon());
2531  query.prepare("SELECT basename, hostname, storagegroup FROM recordedfile "
2532  "WHERE recordedid = :RECORDEDID;");
2533  query.bindValue(":RECORDEDID", ds->m_recordedid);
2534 
2535  if (!query.exec() || !query.size())
2536  {
2537  MythDB::DBError("RecordedFiles deletion", query);
2538  LOG(VB_GENERAL, LOG_ERR, LOC +
2539  QString("Error querying recordedfiles for %1.") .arg(logInfo));
2540  }
2541 
2542  while (query.next())
2543  {
2544  QString basename = query.value(0).toString();
2545  //QString hostname = query.value(1).toString();
2546  //QString storagegroup = query.value(2).toString();
2547  bool deleteInDB = false;
2548 
2549  if (basename == QFileInfo(ds->m_filename).fileName())
2550  deleteInDB = true;
2551  else
2552  {
2553 // LOG(VB_FILE, LOG_INFO, LOC +
2554 // QString("DeleteRecordedFiles(%1), deleting '%2'")
2555 // .arg(logInfo).arg(query.value(0).toString()));
2556 //
2557 // StorageGroup sgroup(storagegroup);
2558 // QString localFile = sgroup.FindFile(basename);
2559 //
2560 // QString url = gCoreContext->GenMythURL(hostname,
2561 // gCoreContext->GetBackendServerPort(hostname),
2562 // basename,
2563 // storagegroup);
2564 //
2565 // if ((((hostname == gCoreContext->GetHostName()) ||
2566 // (!localFile.isEmpty())) &&
2567 // (HandleDeleteFile(basename, storagegroup))) ||
2568 // (((hostname != gCoreContext->GetHostName()) ||
2569 // (localFile.isEmpty())) &&
2570 // (RemoteFile::DeleteFile(url))))
2571 // {
2572 // deleteInDB = true;
2573 // }
2574  }
2575 
2576  if (deleteInDB)
2577  {
2578  update.prepare("DELETE FROM recordedfile "
2579  "WHERE recordedid = :RECORDEDID "
2580  "AND basename = :BASENAME ;");
2581  update.bindValue(":RECORDEDID", ds->m_recordedid);
2582  update.bindValue(":BASENAME", basename);
2583  if (!update.exec())
2584  {
2585  MythDB::DBError("RecordedFiles deletion", update);
2586  LOG(VB_GENERAL, LOG_ERR, LOC +
2587  QString("Error querying recordedfile (%1) for %2.")
2588  .arg(query.value(1).toString())
2589  .arg(logInfo));
2590  }
2591  }
2592  }
2593 }
2594 
2596 {
2597  QString logInfo = QString("recording id %1 (chanid %2 at %3)")
2598  .arg(ds->m_recordedid)
2599  .arg(ds->m_chanid).arg(ds->m_recstartts.toString(Qt::ISODate));
2600 
2601  LOG(VB_GENERAL, LOG_NOTICE, "DoDeleteINDB - " + logInfo);
2602 
2604  query.prepare("DELETE FROM recorded WHERE recordedid = :RECORDEDID AND "
2605  "title = :TITLE;");
2606  query.bindValue(":RECORDEDID", ds->m_recordedid);
2607  query.bindValue(":TITLE", ds->m_title);
2608 
2609  if (!query.exec() || !query.size())
2610  {
2611  MythDB::DBError("Recorded program deletion", query);
2612  LOG(VB_GENERAL, LOG_ERR, LOC +
2613  QString("Error deleting recorded entry for %1.") .arg(logInfo));
2614  }
2615 
2616  std::this_thread::sleep_for(std::chrono::seconds(1));
2617 
2618  // Notify the frontend so it can requery for Free Space
2619  QString msg = QString("RECORDING_LIST_CHANGE DELETE %1")
2620  .arg(ds->m_recordedid);
2622 
2623  // sleep a little to let frontends reload the recordings list
2624  std::this_thread::sleep_for(std::chrono::seconds(3));
2625 
2626  query.prepare("DELETE FROM recordedmarkup "
2627  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
2628  query.bindValue(":CHANID", ds->m_chanid);
2629  query.bindValue(":STARTTIME", ds->m_recstartts);
2630 
2631  if (!query.exec())
2632  {
2633  MythDB::DBError("Recorded program delete recordedmarkup", query);
2634  LOG(VB_GENERAL, LOG_ERR, LOC +
2635  QString("Error deleting recordedmarkup for %1.") .arg(logInfo));
2636  }
2637 
2638  query.prepare("DELETE FROM recordedseek "
2639  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
2640  query.bindValue(":CHANID", ds->m_chanid);
2641  query.bindValue(":STARTTIME", ds->m_recstartts);
2642 
2643  if (!query.exec())
2644  {
2645  MythDB::DBError("Recorded program delete recordedseek", query);
2646  LOG(VB_GENERAL, LOG_ERR, LOC +
2647  QString("Error deleting recordedseek for %1.")
2648  .arg(logInfo));
2649  }
2650 }
2651 
2661 int MainServer::DeleteFile(const QString &filename, bool followLinks,
2662  bool deleteBrokenSymlinks)
2663 {
2664  QFileInfo finfo(filename);
2665  int fd = -1;
2666  QString linktext = "";
2667  QByteArray fname = filename.toLocal8Bit();
2668 
2669  LOG(VB_FILE, LOG_INFO, LOC +
2670  QString("About to unlink/delete file: '%1'")
2671  .arg(fname.constData()));
2672 
2673  QString errmsg = QString("Delete Error '%1'").arg(fname.constData());
2674  if (finfo.isSymLink())
2675  {
2676  linktext = getSymlinkTarget(filename);
2677  QByteArray alink = linktext.toLocal8Bit();
2678  errmsg += QString(" -> '%2'").arg(alink.constData());
2679  }
2680 
2681  if (followLinks && finfo.isSymLink())
2682  {
2683  if (!finfo.exists() && deleteBrokenSymlinks)
2684  unlink(fname.constData());
2685  else
2686  {
2687  fd = OpenAndUnlink(linktext);
2688  if (fd >= 0)
2689  unlink(fname.constData());
2690  }
2691  }
2692  else if (!finfo.isSymLink())
2693  {
2694  fd = OpenAndUnlink(filename);
2695  }
2696  else // just delete symlinks immediately
2697  {
2698  int err = unlink(fname.constData());
2699  if (err == 0)
2700  return -2; // valid result, not an error condition
2701  }
2702 
2703  if (fd < 0)
2704  LOG(VB_GENERAL, LOG_ERR, LOC + errmsg + ENO);
2705 
2706  return fd;
2707 }
2708 
2719 {
2720  QByteArray fname = filename.toLocal8Bit();
2721  QString msg = QString("Error deleting '%1'").arg(fname.constData());
2722  int fd = open(fname.constData(), O_WRONLY);
2723 
2724  if (fd == -1)
2725  {
2726  LOG(VB_GENERAL, LOG_ERR, LOC + msg + " could not open " + ENO);
2727  return -1;
2728  }
2729 
2730  if (unlink(fname.constData()))
2731  {
2732  LOG(VB_GENERAL, LOG_ERR, LOC + msg + " could not unlink " + ENO);
2733  close(fd);
2734  return -1;
2735  }
2736 
2737  return fd;
2738 }
2739 
2749  const QString &filename, off_t fsize)
2750 {
2751  QMutexLocker locker(&s_truncate_and_close_lock);
2752 
2753  if (pginfo)
2754  {
2755  pginfo->SetPathname(filename);
2756  pginfo->MarkAsInUse(true, kTruncatingDeleteInUseID);
2757  }
2758 
2759  int cards = 5;
2760  {
2762  query.prepare("SELECT COUNT(cardid) FROM capturecard;");
2763  if (query.exec() && query.next())
2764  cards = query.value(0).toInt();
2765  }
2766 
2767  // Time between truncation steps in milliseconds
2768  const size_t sleep_time = 500;
2769  const size_t min_tps = 8 * 1024 * 1024;
2770  const auto calc_tps = (size_t) (cards * 1.2 * (22200000LL / 8.0));
2771  const size_t tps = max(min_tps, calc_tps);
2772  const auto increment = (size_t) (tps * (sleep_time * 0.001F));
2773 
2774  LOG(VB_FILE, LOG_INFO, LOC +
2775  QString("Truncating '%1' by %2 MB every %3 milliseconds")
2776  .arg(filename)
2777  .arg(increment / (1024.0 * 1024.0), 0, 'f', 2)
2778  .arg(sleep_time));
2779 
2780  GetMythDB()->GetDBManager()->PurgeIdleConnections(false);
2781 
2782  int count = 0;
2783  while (fsize > 0)
2784  {
2785 #if 0
2786  LOG(VB_FILE, LOG_DEBUG, LOC + QString("Truncating '%1' to %2 MB")
2787  .arg(filename).arg(fsize / (1024.0 * 1024.0), 0, 'f', 2));
2788 #endif
2789 
2790  int err = ftruncate(fd, fsize);
2791  if (err)
2792  {
2793  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error truncating '%1'")
2794  .arg(filename) + ENO);
2795  if (pginfo)
2796  pginfo->MarkAsInUse(false, kTruncatingDeleteInUseID);
2797  return 0 == close(fd);
2798  }
2799 
2800  fsize -= increment;
2801 
2802  if (pginfo && ((count % 100) == 0))
2803  pginfo->UpdateInUseMark(true);
2804 
2805  count++;
2806 
2807  std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
2808  }
2809 
2810  bool ok = (0 == close(fd));
2811 
2812  if (pginfo)
2813  pginfo->MarkAsInUse(false, kTruncatingDeleteInUseID);
2814 
2815  LOG(VB_FILE, LOG_INFO, LOC +
2816  QString("Finished truncating '%1'").arg(filename));
2817 
2818  return ok;
2819 }
2820 
2822  PlaybackSock *pbs)
2823 {
2824  MythSocket *pbssock = nullptr;
2825  if (pbs)
2826  pbssock = pbs->getSocket();
2827 
2828  QStringList::const_iterator it = slist.begin() + 1;
2829  ProgramInfo pginfo(it, slist.end());
2830 
2831  int result = 0;
2832 
2833  if (m_ismaster && pginfo.GetHostname() != gCoreContext->GetHostName())
2834  {
2835  PlaybackSock *slave = GetSlaveByHostname(pginfo.GetHostname());
2836  if (slave)
2837  {
2838  result = slave->CheckRecordingActive(&pginfo);
2839  slave->DecrRef();
2840  }
2841  }
2842  else
2843  {
2844  TVRec::s_inputsLock.lockForRead();
2845  for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter)
2846  {
2847  EncoderLink *elink = *iter;
2848 
2849  if (elink->IsLocal() && elink->MatchesRecording(&pginfo))
2850  result = iter.key();
2851  }
2852  TVRec::s_inputsLock.unlock();
2853  }
2854 
2855  QStringList outputlist( QString::number(result) );
2856  if (pbssock)
2857  SendResponse(pbssock, outputlist);
2858 }
2859 
2861 {
2862  QStringList::const_iterator it = slist.begin() + 1;
2863  RecordingInfo recinfo(it, slist.end());
2864  if (recinfo.GetChanID())
2865  {
2866  if (m_ismaster)
2867  {
2868  // Stop recording may have been called for the same program on
2869  // different channel in the guide, we need to find the actual channel
2870  // that the recording is occurring on. This only needs doing once
2871  // on the master backend, as the correct chanid will then be sent
2872  // to the slave
2873  ProgramList schedList;
2874  bool hasConflicts = false;
2875  LoadFromScheduler(schedList, hasConflicts);
2876  for (auto *pInfo : schedList)
2877  {
2878  if ((pInfo->GetRecordingStatus() == RecStatus::Tuning ||
2879  pInfo->GetRecordingStatus() == RecStatus::Failing ||
2880  pInfo->GetRecordingStatus() == RecStatus::Recording)
2881  && recinfo.IsSameProgram(*pInfo))
2882  recinfo.SetChanID(pInfo->GetChanID());
2883  }
2884  }
2885  DoHandleStopRecording(recinfo, pbs);
2886  }
2887 }
2888 
2890  RecordingInfo &recinfo, PlaybackSock *pbs)
2891 {
2892  MythSocket *pbssock = nullptr;
2893  if (pbs)
2894  pbssock = pbs->getSocket();
2895 
2896  // FIXME! We don't know what state the recorder is in at this
2897  // time. Simply set the recstatus to RecStatus::Unknown and let the
2898  // scheduler do the best it can with it. The proper long term fix
2899  // is probably to have the recorder return the actual recstatus as
2900  // part of the stop recording response. That's a more involved
2901  // change than I care to make during the 0.25 code freeze.
2903 
2904  if (m_ismaster && recinfo.GetHostname() != gCoreContext->GetHostName())
2905  {
2906  PlaybackSock *slave = GetSlaveByHostname(recinfo.GetHostname());
2907 
2908  if (slave)
2909  {
2910  int num = slave->StopRecording(&recinfo);
2911 
2912  if (num > 0)
2913  {
2914  TVRec::s_inputsLock.lockForRead();
2915  if (m_encoderList->contains(num))
2916  {
2917  (*m_encoderList)[num]->StopRecording();
2918  }
2919  TVRec::s_inputsLock.unlock();
2920  if (m_sched)
2921  m_sched->UpdateRecStatus(&recinfo);
2922  }
2923  if (pbssock)
2924  {
2925  QStringList outputlist( "0" );
2926  SendResponse(pbssock, outputlist);
2927  }
2928 
2929  slave->DecrRef();
2930  return;
2931  }
2932 
2933  // If the slave is unreachable, we can assume that the
2934  // recording has stopped and the status should be updated.
2935  // Continue so that the master can try to update the endtime
2936  // of the file is in a shared directory.
2937  if (m_sched)
2938  m_sched->UpdateRecStatus(&recinfo);
2939  }
2940 
2941  int recnum = -1;
2942 
2943  TVRec::s_inputsLock.lockForRead();
2944  for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter)
2945  {
2946  EncoderLink *elink = *iter;
2947 
2948  if (elink->IsLocal() && elink->MatchesRecording(&recinfo))
2949  {
2950  recnum = iter.key();
2951 
2952  elink->StopRecording();
2953 
2954  while (elink->IsBusyRecording() ||
2955  elink->GetState() == kState_ChangingState)
2956  {
2957  std::this_thread::sleep_for(std::chrono::microseconds(100));
2958  }
2959 
2960  if (m_ismaster)
2961  {
2962  if (m_sched)
2963  m_sched->UpdateRecStatus(&recinfo);
2964  }
2965 
2966  break;
2967  }
2968  }
2969  TVRec::s_inputsLock.unlock();
2970 
2971  if (pbssock)
2972  {
2973  QStringList outputlist( QString::number(recnum) );
2974  SendResponse(pbssock, outputlist);
2975  }
2976 }
2977 
2978 void MainServer::HandleDeleteRecording(QString &chanid, QString &starttime,
2979  PlaybackSock *pbs,
2980  bool forceMetadataDelete,
2981  bool forgetHistory)
2982 {
2983  QDateTime recstartts = MythDate::fromString(starttime);
2984  RecordingInfo recinfo(chanid.toUInt(), recstartts);
2985 
2986  if (!recinfo.GetRecordingID())
2987  {
2988  qDebug() << "HandleDeleteRecording(chanid, starttime) Empty Recording ID";
2989  }
2990 
2991  if (!recinfo.GetChanID()) // !recinfo.GetRecordingID()
2992  {
2993  MythSocket *pbssock = nullptr;
2994  if (pbs)
2995  pbssock = pbs->getSocket();
2996 
2997  QStringList outputlist( QString::number(0) );
2998 
2999  SendResponse(pbssock, outputlist);
3000  return;
3001  }
3002 
3003  DoHandleDeleteRecording(recinfo, pbs, forceMetadataDelete, false, forgetHistory);
3004 }
3005 
3007  bool forceMetadataDelete)
3008 {
3009  QStringList::const_iterator it = slist.begin() + 1;
3010  RecordingInfo recinfo(it, slist.end());
3011 
3012  if (!recinfo.GetRecordingID())
3013  {
3014  qDebug() << "HandleDeleteRecording(QStringList) Empty Recording ID";
3015  }
3016 
3017  if (recinfo.GetChanID()) // !recinfo.GetRecordingID()
3018  DoHandleDeleteRecording(recinfo, pbs, forceMetadataDelete, false, false);
3019 }
3020 
3022  RecordingInfo &recinfo, PlaybackSock *pbs,
3023  bool forceMetadataDelete, bool lexpirer, bool forgetHistory)
3024 {
3025  int resultCode = -1;
3026  MythSocket *pbssock = nullptr;
3027  if (pbs)
3028  pbssock = pbs->getSocket();
3029 
3030  bool justexpire = lexpirer ? false :
3031  ( //gCoreContext->GetNumSetting("AutoExpireInsteadOfDelete") &&
3032  (recinfo.GetRecordingGroup() != "Deleted") &&
3033  (recinfo.GetRecordingGroup() != "LiveTV"));
3034 
3035  QString filename = GetPlaybackURL(&recinfo, false);
3036  if (filename.isEmpty())
3037  {
3038  LOG(VB_GENERAL, LOG_ERR, LOC +
3039  QString("ERROR when trying to delete file for %1. Unable "
3040  "to determine filename of recording.")
3042 
3043  if (pbssock)
3044  {
3045  resultCode = -2;
3046  QStringList outputlist(QString::number(resultCode));
3047  SendResponse(pbssock, outputlist);
3048  }
3049 
3050  return;
3051  }
3052 
3053  // Stop the recording if it's still in progress.
3054  DoHandleStopRecording(recinfo, nullptr);
3055 
3056  if (justexpire && !forceMetadataDelete &&
3057  recinfo.GetFilesize() > (1024 * 1024) )
3058  {
3059  recinfo.ApplyRecordRecGroupChange("Deleted");
3060  recinfo.SaveAutoExpire(kDeletedAutoExpire, true);
3061  if (forgetHistory)
3062  recinfo.ForgetHistory();
3063  else if (m_sched)
3064  m_sched->RescheduleCheck(recinfo, "DoHandleDelete1");
3065  QStringList outputlist( QString::number(0) );
3066  SendResponse(pbssock, outputlist);
3067  return;
3068  }
3069 
3070  // If this recording was made by a another recorder, and that
3071  // recorder is available, tell it to do the deletion.
3072  if (m_ismaster && recinfo.GetHostname() != gCoreContext->GetHostName())
3073  {
3074  PlaybackSock *slave = GetSlaveByHostname(recinfo.GetHostname());
3075 
3076  if (slave)
3077  {
3078  int num = slave->DeleteRecording(&recinfo, forceMetadataDelete);
3079 
3080  if (forgetHistory)
3081  recinfo.ForgetHistory();
3082  else if (m_sched &&
3083  recinfo.GetRecordingGroup() != "Deleted" &&
3084  recinfo.GetRecordingGroup() != "LiveTV")
3085  m_sched->RescheduleCheck(recinfo, "DoHandleDelete2");
3086 
3087  if (pbssock)
3088  {
3089  QStringList outputlist( QString::number(num) );
3090  SendResponse(pbssock, outputlist);
3091  }
3092 
3093  slave->DecrRef();
3094  return;
3095  }
3096  }
3097 
3098  QFile checkFile(filename);
3099  bool fileExists = checkFile.exists();
3100  if (!fileExists)
3101  {
3102  QFile checkFileUTF8(QString::fromUtf8(filename.toLatin1().constData()));
3103  fileExists = checkFileUTF8.exists();
3104  if (fileExists)
3105  filename = QString::fromUtf8(filename.toLatin1().constData());
3106  }
3107 
3108  // Allow deleting of files where the recording failed meaning size == 0
3109  // But do not allow deleting of files that appear to be completely absent.
3110  // The latter condition indicates the filesystem containing the file is
3111  // most likely absent and deleting the file metadata is unsafe.
3112  if (fileExists || !recinfo.GetFilesize() || forceMetadataDelete)
3113  {
3114  recinfo.SaveDeletePendingFlag(true);
3115 
3116  if (!recinfo.GetRecordingID())
3117  {
3118  qDebug() << "DoHandleDeleteRecording() Empty Recording ID";
3119  }
3120 
3121  auto *deleteThread = new DeleteThread(this, filename,
3122  recinfo.GetTitle(), recinfo.GetChanID(),
3123  recinfo.GetRecordingStartTime(), recinfo.GetRecordingEndTime(),
3124  recinfo.GetRecordingID(),
3125  forceMetadataDelete);
3126  deleteThread->start();
3127  }
3128  else
3129  {
3130 #if 0
3131  QString logInfo = QString("chanid %1")
3132  .arg(recinfo.toString(ProgramInfo::kRecordingKey));
3133 #endif
3134 
3135  LOG(VB_GENERAL, LOG_ERR, LOC +
3136  QString("ERROR when trying to delete file: %1. File doesn't "
3137  "exist. Database metadata will not be removed.")
3138  .arg(filename));
3139  resultCode = -2;
3140  }
3141 
3142  if (pbssock)
3143  {
3144  QStringList outputlist( QString::number(resultCode) );
3145  SendResponse(pbssock, outputlist);
3146  }
3147 
3148  if (forgetHistory)
3149  recinfo.ForgetHistory();
3150  else if (m_sched &&
3151  recinfo.GetRecordingGroup() != "Deleted" &&
3152  recinfo.GetRecordingGroup() != "LiveTV")
3153  m_sched->RescheduleCheck(recinfo, "DoHandleDelete3");
3154 
3155  // Tell MythTV frontends that the recording list needs to be updated.
3156  if (fileExists || !recinfo.GetFilesize() || forceMetadataDelete)
3157  {
3159  QString("REC_DELETED CHANID %1 STARTTIME %2")
3160  .arg(recinfo.GetChanID())
3161  .arg(recinfo.GetRecordingStartTime(MythDate::ISODate)));
3162 
3163  recinfo.SendDeletedEvent();
3164  }
3165 }
3166 
3168 {
3169  if (slist.size() == 3)
3170  {
3171  RecordingInfo recinfo(
3172  slist[1].toUInt(), MythDate::fromString(slist[2]));
3173  if (recinfo.GetChanID())
3174  DoHandleUndeleteRecording(recinfo, pbs);
3175  }
3176  else if (slist.size() >= (1 + NUMPROGRAMLINES))
3177  {
3178  QStringList::const_iterator it = slist.begin()+1;
3179  RecordingInfo recinfo(it, slist.end());
3180  if (recinfo.GetChanID())
3181  DoHandleUndeleteRecording(recinfo, pbs);
3182  }
3183 }
3184 
3186  RecordingInfo &recinfo, PlaybackSock *pbs)
3187 {
3188  int ret = -1;
3189 
3190  MythSocket *pbssock = nullptr;
3191  if (pbs)
3192  pbssock = pbs->getSocket();
3193 
3194 #if 0
3195  if (gCoreContext->GetNumSetting("AutoExpireInsteadOfDelete", 0))
3196 #endif
3197  {
3198  recinfo.ApplyRecordRecGroupChange("Default");
3199  recinfo.UpdateLastDelete(false);
3201  if (m_sched)
3202  m_sched->RescheduleCheck(recinfo, "DoHandleUndelete");
3203  ret = 0;
3204  }
3205 
3206  QStringList outputlist( QString::number(ret) );
3207  SendResponse(pbssock, outputlist);
3208 }
3209 
3236 void MainServer::HandleRescheduleRecordings(const QStringList &request,
3237  PlaybackSock *pbs)
3238 {
3239  QStringList result;
3240  if (m_sched)
3241  {
3242  m_sched->Reschedule(request);
3243  result = QStringList(QString::number(1));
3244  }
3245  else
3246  result = QStringList(QString::number(0));
3247 
3248  if (pbs)
3249  {
3250  MythSocket *pbssock = pbs->getSocket();
3251  if (pbssock)
3252  SendResponse(pbssock, result);
3253  }
3254 }
3255 
3257 {
3258  // If we're already trying to add a child input, ignore this
3259  // attempt. The scheduler will keep asking until it gets added.
3260  // This makes the whole operation asynchronous and allows the
3261  // scheduler to continue servicing other recordings.
3262  if (!m_addChildInputLock.tryLock())
3263  {
3264  LOG(VB_GENERAL, LOG_INFO, LOC + "HandleAddChildInput: Already locked");
3265  return false;
3266  }
3267 
3268  LOG(VB_GENERAL, LOG_INFO, LOC +
3269  QString("HandleAddChildInput: Handling input %1").arg(inputid));
3270 
3271  TVRec::s_inputsLock.lockForWrite();
3272 
3273  if (m_ismaster)
3274  {
3275  // First, add the new input to the database.
3276  uint childid = CardUtil::AddChildInput(inputid);
3277  if (!childid)
3278  {
3279  LOG(VB_GENERAL, LOG_ERR, LOC +
3280  QString("HandleAddChildInput: "
3281  "Failed to add child to input %1").arg(inputid));
3282  TVRec::s_inputsLock.unlock();
3283  m_addChildInputLock.unlock();
3284  return false;
3285  }
3286 
3287  LOG(VB_GENERAL, LOG_INFO, LOC +
3288  QString("HandleAddChildInput: Added child input %1").arg(childid));
3289 
3290  // Next, create the master TVRec and/or EncoderLink.
3291  QString localhostname = gCoreContext->GetHostName();
3292  QString hostname = CardUtil::GetHostname(childid);
3293 
3294  if (hostname == localhostname)
3295  {
3296  auto *tv = new TVRec(childid);
3297  if (!tv || !tv->Init())
3298  {
3299  LOG(VB_GENERAL, LOG_ERR, LOC +
3300  QString("HandleAddChildInput: "
3301  "Failed to initialize input %1").arg(childid));
3302  delete tv;
3303  CardUtil::DeleteInput(childid);
3304  TVRec::s_inputsLock.unlock();
3305  m_addChildInputLock.unlock();
3306  return false;
3307  }
3308 
3309  auto *enc = new EncoderLink(childid, tv);
3310  (*m_encoderList)[childid] = enc;
3311  }
3312  else
3313  {
3314  EncoderLink *enc = (*m_encoderList)[inputid];
3315  if (!enc->AddChildInput(childid))
3316  {
3317  LOG(VB_GENERAL, LOG_ERR, LOC +
3318  QString("HandleAddChildInput: "
3319  "Failed to add remote input %1").arg(childid));
3320  CardUtil::DeleteInput(childid);
3321  TVRec::s_inputsLock.unlock();
3322  m_addChildInputLock.unlock();
3323  return false;
3324  }
3325 
3326  PlaybackSock *pbs = enc->GetSocket();
3327  enc = new EncoderLink(childid, nullptr, hostname);
3328  enc->SetSocket(pbs);
3329  (*m_encoderList)[childid] = enc;
3330  }
3331 
3332  // Finally, add the new input to the Scheduler.
3333  m_sched->AddChildInput(inputid, childid);
3334  }
3335  else
3336  {
3337  // Create the slave TVRec and EncoderLink.
3338  auto *tv = new TVRec(inputid);
3339  if (!tv || !tv->Init())
3340  {
3341  LOG(VB_GENERAL, LOG_ERR, LOC +
3342  QString("HandleAddChildInput: "
3343  "Failed to initialize input %1").arg(inputid));
3344  delete tv;
3345  TVRec::s_inputsLock.unlock();
3346  m_addChildInputLock.unlock();
3347  return false;
3348  }
3349 
3350  auto *enc = new EncoderLink(inputid, tv);
3351  (*m_encoderList)[inputid] = enc;
3352  }
3353 
3354  TVRec::s_inputsLock.unlock();
3355  m_addChildInputLock.unlock();
3356 
3357  LOG(VB_GENERAL, LOG_INFO, LOC +
3358  QString("HandleAddChildInput: "
3359  "Successfully handled input %1").arg(inputid));
3360 
3361  return true;
3362 }
3363 
3365 {
3366  QStringList::const_iterator it = slist.begin() + 1;
3367  RecordingInfo recinfo(it, slist.end());
3368  if (recinfo.GetChanID())
3369  recinfo.ForgetHistory();
3370 
3371  MythSocket *pbssock = nullptr;
3372  if (pbs)
3373  pbssock = pbs->getSocket();
3374  if (pbssock)
3375  {
3376  QStringList outputlist( QString::number(0) );
3377  SendResponse(pbssock, outputlist);
3378  }
3379 }
3380 
3387 {
3388  QStringList strlist;
3389 
3390  QString sleepCmd = gCoreContext->GetSetting("SleepCommand");
3391  if (!sleepCmd.isEmpty())
3392  {
3393  strlist << "OK";
3394  SendResponse(pbs->getSocket(), strlist);
3395  LOG(VB_GENERAL, LOG_NOTICE, LOC +
3396  "Received GO_TO_SLEEP command from master, running SleepCommand.");
3397  myth_system(sleepCmd);
3398  }
3399  else
3400  {
3401  strlist << "ERROR: SleepCommand is empty";
3402  LOG(VB_GENERAL, LOG_ERR, LOC +
3403  "ERROR: in HandleGoToSleep(), but no SleepCommand found!");
3404  SendResponse(pbs->getSocket(), strlist);
3405  }
3406 }
3407 
3418 {
3419  QStringList strlist;
3420 
3421  if (allHosts)
3422  {
3423  QMutexLocker locker(&m_masterFreeSpaceListLock);
3424  strlist = m_masterFreeSpaceList;
3427  {
3429  {
3431  m_masterFreeSpaceListWait.wait(locker.mutex());
3432  }
3435  m_masterFreeSpaceListUpdater, "FreeSpaceUpdater");
3436  }
3437  }
3438  else
3439  BackendQueryDiskSpace(strlist, allHosts, allHosts);
3440 
3441  SendResponse(pbs->getSocket(), strlist);
3442 }
3443 
3450 {
3451  QStringList strlist;
3452  {
3453  QMutexLocker locker(&m_masterFreeSpaceListLock);
3454  strlist = m_masterFreeSpaceList;
3457  {
3459  {
3461  m_masterFreeSpaceListWait.wait(locker.mutex());
3462  }
3465  m_masterFreeSpaceListUpdater, "FreeSpaceUpdater");
3466  }
3467  }
3468 
3469  // The TotalKB and UsedKB are the last two numbers encoded in the list
3470  QStringList shortlist;
3471  if (strlist.size() < 4)
3472  {
3473  shortlist << QString("0");
3474  shortlist << QString("0");
3475  }
3476  else
3477  {
3478  unsigned int index = (uint)(strlist.size()) - 2;
3479  shortlist << strlist[index++];
3480  shortlist << strlist[index++];
3481  }
3482 
3483  SendResponse(pbs->getSocket(), shortlist);
3484 }
3485 
3493 {
3494  MythSocket *pbssock = pbs->getSocket();
3495 
3496  QStringList strlist;
3497 
3498 #if defined(_WIN32) || defined(Q_OS_ANDROID)
3499  strlist << "0" << "0" << "0";
3500 #else
3501  loadArray loads = getLoadAvgs();
3502  if (loads[0] == -1)
3503  {
3504  strlist << "ERROR";
3505  strlist << "getloadavg() failed";
3506  }
3507  else
3508  {
3509  strlist << QString::number(loads[0])
3510  << QString::number(loads[1])
3511  << QString::number(loads[2]);
3512  }
3513 #endif
3514 
3515  SendResponse(pbssock, strlist);
3516 }
3517 
3524 {
3525  MythSocket *pbssock = pbs->getSocket();
3526  QStringList strlist;
3527  time_t uptime = 0;
3528 
3529  if (getUptime(uptime))
3530  strlist << QString::number(uptime);
3531  else
3532  {
3533  strlist << "ERROR";
3534  strlist << "Could not determine uptime.";
3535  }
3536 
3537  SendResponse(pbssock, strlist);
3538 }
3539 
3546 {
3547  MythSocket *pbssock = pbs->getSocket();
3548  QStringList strlist;
3549 
3550  strlist << gCoreContext->GetHostName();
3551 
3552  SendResponse(pbssock, strlist);
3553 }
3554 
3561 {
3562  MythSocket *pbssock = pbs->getSocket();
3563  QStringList strlist;
3564  int totalMB = 0;
3565  int freeMB = 0;
3566  int totalVM = 0;
3567  int freeVM = 0;
3568 
3569  if (getMemStats(totalMB, freeMB, totalVM, freeVM))
3570  {
3571  strlist << QString::number(totalMB) << QString::number(freeMB)
3572  << QString::number(totalVM) << QString::number(freeVM);
3573  }
3574  else
3575  {
3576  strlist << "ERROR";
3577  strlist << "Could not determine memory stats.";
3578  }
3579 
3580  SendResponse(pbssock, strlist);
3581 }
3582 
3589 {
3590  MythSocket *pbssock = pbs->getSocket();
3591  QStringList strlist;
3592  strlist << MythTZ::getTimeZoneID()
3593  << QString::number(MythTZ::calc_utc_offset())
3595 
3596  SendResponse(pbssock, strlist);
3597 }
3598 
3604 {
3605  MythSocket *pbssock = pbs->getSocket();
3606  bool checkSlaves = slist[1].toInt() != 0;
3607 
3608  QStringList::const_iterator it = slist.begin() + 2;
3609  RecordingInfo recinfo(it, slist.end());
3610 
3611  bool exists = false;
3612 
3613  if (recinfo.HasPathname() && (m_ismaster) &&
3614  (recinfo.GetHostname() != gCoreContext->GetHostName()) &&
3615  (checkSlaves))
3616  {
3618 
3619  if (slave)
3620  {
3621  exists = slave->CheckFile(&recinfo);
3622  slave->DecrRef();
3623 
3624  QStringList outputlist( QString::number(static_cast<int>(exists)) );
3625  if (exists)
3626  outputlist << recinfo.GetPathname();
3627  else
3628  outputlist << "";
3629 
3630  SendResponse(pbssock, outputlist);
3631  return;
3632  }
3633  }
3634 
3635  QString pburl;
3636  if (recinfo.HasPathname())
3637  {
3638  pburl = GetPlaybackURL(&recinfo);
3639  exists = QFileInfo(pburl).exists();
3640  if (!exists)
3641  pburl.clear();
3642  }
3643 
3644  QStringList strlist( QString::number(static_cast<int>(exists)) );
3645  strlist << pburl;
3646  SendResponse(pbssock, strlist);
3647 }
3648 
3649 
3655 {
3656  QString storageGroup = "Default";
3657  QString hostname = gCoreContext->GetHostName();
3658  QString filename = "";
3659  QStringList res;
3660 
3661  switch (slist.size()) {
3662  case 4:
3663  if (!slist[3].isEmpty())
3664  hostname = slist[3];
3665  [[clang::fallthrough]];
3666  case 3:
3667  if (slist[2].isEmpty())
3668  storageGroup = slist[2];
3669  [[clang::fallthrough]];
3670  case 2:
3671  filename = slist[1];
3672  if (filename.isEmpty() ||
3673  filename.contains("/../") ||
3674  filename.startsWith("../"))
3675  {
3676  LOG(VB_GENERAL, LOG_ERR, LOC +
3677  QString("ERROR checking for file, filename '%1' "
3678  "fails sanity checks").arg(filename));
3679  res << "";
3680  SendResponse(pbs->getSocket(), res);
3681  return;
3682  }
3683  break;
3684  default:
3685  LOG(VB_GENERAL, LOG_ERR, LOC +
3686  "ERROR, invalid input count for QUERY_FILE_HASH");
3687  res << "";
3688  SendResponse(pbs->getSocket(), res);
3689  return;
3690  }
3691 
3692  QString hash = "";
3693 
3695  {
3696  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName());
3697  QString fullname = sgroup.FindFile(filename);
3698  hash = FileHash(fullname);
3699  }
3700  else
3701  {
3703  if (slave)
3704  {
3705  hash = slave->GetFileHash(filename, storageGroup);
3706  slave->DecrRef();
3707  }
3708  // I deleted the incorrect SQL select that was supposed to get
3709  // host name from ip address. Since it cannot work and has
3710  // been there 6 years I assume it is not important.
3711  }
3712 
3713  res << hash;
3714  SendResponse(pbs->getSocket(), res);
3715 }
3716 
3722 {
3723  QString filename = slist[1];
3724  QString storageGroup = "Default";
3725  QStringList retlist;
3726 
3727  if (slist.size() > 2)
3728  storageGroup = slist[2];
3729 
3730  if ((filename.isEmpty()) ||
3731  (filename.contains("/../")) ||
3732  (filename.startsWith("../")))
3733  {
3734  LOG(VB_GENERAL, LOG_ERR, LOC +
3735  QString("ERROR checking for file, filename '%1' "
3736  "fails sanity checks").arg(filename));
3737  retlist << "0";
3738  SendResponse(pbs->getSocket(), retlist);
3739  return;
3740  }
3741 
3742  if (storageGroup.isEmpty())
3743  storageGroup = "Default";
3744 
3745  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName());
3746 
3747  QString fullname = sgroup.FindFile(filename);
3748 
3749  if (!fullname.isEmpty())
3750  {
3751  retlist << "1";
3752  retlist << fullname;
3753 
3754  struct stat fileinfo {};
3755  if (stat(fullname.toLocal8Bit().constData(), &fileinfo) >= 0)
3756  {
3757  retlist << QString::number(fileinfo.st_dev);
3758  retlist << QString::number(fileinfo.st_ino);
3759  retlist << QString::number(fileinfo.st_mode);
3760  retlist << QString::number(fileinfo.st_nlink);
3761  retlist << QString::number(fileinfo.st_uid);
3762  retlist << QString::number(fileinfo.st_gid);
3763  retlist << QString::number(fileinfo.st_rdev);
3764  retlist << QString::number(fileinfo.st_size);
3765 #ifdef _WIN32
3766  retlist << "0"; // st_blksize
3767  retlist << "0"; // st_blocks
3768 #else
3769  retlist << QString::number(fileinfo.st_blksize);
3770  retlist << QString::number(fileinfo.st_blocks);
3771 #endif
3772  retlist << QString::number(fileinfo.st_atime);
3773  retlist << QString::number(fileinfo.st_mtime);
3774  retlist << QString::number(fileinfo.st_ctime);
3775  }
3776  }
3777  else
3778  retlist << "0";
3779 
3780  SendResponse(pbs->getSocket(), retlist);
3781 }
3782 
3783 void MainServer::getGuideDataThrough(QDateTime &GuideDataThrough)
3784 {
3786  query.prepare("SELECT MAX(endtime) FROM program WHERE manualid = 0;");
3787 
3788  if (query.exec() && query.next())
3789  {
3790  GuideDataThrough = MythDate::fromString(query.value(0).toString());
3791  }
3792 }
3793 
3795 {
3796  QDateTime GuideDataThrough;
3797  MythSocket *pbssock = pbs->getSocket();
3798  QStringList strlist;
3799 
3800  getGuideDataThrough(GuideDataThrough);
3801 
3802  if (GuideDataThrough.isNull())
3803  strlist << QString("0000-00-00 00:00");
3804  else
3805  strlist << GuideDataThrough.toString("yyyy-MM-dd hh:mm");
3806 
3807  SendResponse(pbssock, strlist);
3808 }
3809 
3811  const QString& tmptable, int recordid)
3812 {
3813  MythSocket *pbssock = pbs->getSocket();
3814 
3815  QStringList strList;
3816 
3817  if (m_sched)
3818  {
3819  if (tmptable.isEmpty())
3820  m_sched->GetAllPending(strList);
3821  else
3822  {
3823  auto *sched = new Scheduler(false, m_encoderList, tmptable, m_sched);
3824  sched->FillRecordListFromDB(recordid);
3825  sched->GetAllPending(strList);
3826  delete sched;
3827 
3828  if (recordid > 0)
3829  {
3831  query.prepare("SELECT NULL FROM record "
3832  "WHERE recordid = :RECID;");
3833  query.bindValue(":RECID", recordid);
3834 
3835  if (query.exec() && query.size())
3836  {
3837  auto *record = new RecordingRule();
3838  record->m_recordID = recordid;
3839  if (record->Load() &&
3840  record->m_searchType == kManualSearch)
3841  m_sched->RescheduleMatch(recordid, 0, 0, QDateTime(),
3842  "Speculation");
3843  delete record;
3844  }
3845  query.prepare("DELETE FROM program WHERE manualid = :RECID;");
3846  query.bindValue(":RECID", recordid);
3847  if (!query.exec())
3848  MythDB::DBError("MainServer::HandleGetPendingRecordings "
3849  "- delete", query);
3850  }
3851  }
3852  }
3853  else
3854  {
3855  strList << QString::number(0);
3856  strList << QString::number(0);
3857  }
3858 
3859  SendResponse(pbssock, strList);
3860 }
3861 
3863 {
3864  MythSocket *pbssock = pbs->getSocket();
3865 
3866  QStringList strList;
3867 
3868  if (m_sched)
3869  Scheduler::GetAllScheduled(strList);
3870  else
3871  strList << QString::number(0);
3872 
3873  SendResponse(pbssock, strList);
3874 }
3875 
3877  PlaybackSock *pbs)
3878 {
3879  MythSocket *pbssock = pbs->getSocket();
3880 
3881  QStringList::const_iterator it = slist.begin() + 1;
3882  RecordingInfo recinfo(it, slist.end());
3883 
3884  QStringList strlist;
3885 
3886  if (m_sched && recinfo.GetChanID())
3887  m_sched->getConflicting(&recinfo, strlist);
3888  else
3889  strlist << QString::number(0);
3890 
3891  SendResponse(pbssock, strlist);
3892 }
3893 
3895 {
3896  MythSocket *pbssock = pbs->getSocket();
3897 
3898  QStringList strList;
3899 
3900  if (m_expirer)
3901  m_expirer->GetAllExpiring(strList);
3902  else
3903  strList << QString::number(0);
3904 
3905  SendResponse(pbssock, strList);
3906 }
3907 
3908 void MainServer::HandleSGGetFileList(QStringList &sList,
3909  PlaybackSock *pbs)
3910 {
3911  MythSocket *pbssock = pbs->getSocket();
3912  QStringList strList;
3913 
3914  if ((sList.size() < 4) || (sList.size() > 5))
3915  {
3916  LOG(VB_GENERAL, LOG_ERR, LOC +
3917  QString("HandleSGGetFileList: Invalid Request. %1")
3918  .arg(sList.join("[]:[]")));
3919  strList << "EMPTY LIST";
3920  SendResponse(pbssock, strList);
3921  return;
3922  }
3923 
3924  QString host = gCoreContext->GetHostName();
3925  QString wantHost = sList.at(1);
3926  QHostAddress wantHostaddr(wantHost);
3927  QString groupname = sList.at(2);
3928  QString path = sList.at(3);
3929  bool fileNamesOnly = false;
3930 
3931  if (sList.size() >= 5)
3932  fileNamesOnly = (sList.at(4).toInt() != 0);
3933 
3934  bool slaveUnreachable = false;
3935 
3936  LOG(VB_FILE, LOG_INFO, LOC +
3937  QString("HandleSGGetFileList: group = %1 host = %2 "
3938  " path = %3 wanthost = %4")
3939  .arg(groupname).arg(host).arg(path).arg(wantHost));
3940 
3941  QString addr = gCoreContext->GetBackendServerIP();
3942 
3943  if ((host.toLower() == wantHost.toLower()) ||
3944  (!addr.isEmpty() && addr == wantHostaddr.toString()))
3945  {
3946  StorageGroup sg(groupname, host);
3947  LOG(VB_FILE, LOG_INFO, LOC + "HandleSGGetFileList: Getting local info");
3948  if (fileNamesOnly)
3949  strList = sg.GetFileList(path);
3950  else
3951  strList = sg.GetFileInfoList(path);
3952  }
3953  else
3954  {
3955  PlaybackSock *slave = GetMediaServerByHostname(wantHost);
3956  if (slave)
3957  {
3958  LOG(VB_FILE, LOG_INFO, LOC +
3959  "HandleSGGetFileList: Getting remote info");
3960  strList = slave->GetSGFileList(wantHost, groupname, path,
3961  fileNamesOnly);
3962  slave->DecrRef();
3963  slaveUnreachable = false;
3964  }
3965  else
3966  {
3967  LOG(VB_FILE, LOG_INFO, LOC +
3968  QString("HandleSGGetFileList: Failed to grab slave socket "
3969  ": %1 :").arg(wantHost));
3970  slaveUnreachable = true;
3971  }
3972 
3973  }
3974 
3975  if (slaveUnreachable)
3976  strList << "SLAVE UNREACHABLE: " << host;
3977 
3978  if (strList.isEmpty() || (strList.at(0) == "0"))
3979  strList << "EMPTY LIST";
3980 
3981  SendResponse(pbssock, strList);
3982 }
3983 
3985 {
3986 //format: QUERY_FINDFILE <host> <storagegroup> <filename> <useregex (optional)> <allowfallback (optional)>
3987 
3988  QString hostname = slist[1];
3989  QString storageGroup = slist[2];
3990  QString filename = slist[3];
3991  bool allowFallback = true;
3992  bool useRegex = false;
3993  QStringList fileList;
3994 
3995  if (!QHostAddress(hostname).isNull())
3996  {
3997  LOG(VB_GENERAL, LOG_ERR, QString("Mainserver: QUERY_FINDFILE called "
3998  "with IP (%1) instead of hostname. "
3999  "This is invalid.").arg(hostname));
4000  }
4001 
4002  if (hostname.isEmpty())
4004 
4005  if (storageGroup.isEmpty())
4006  storageGroup = "Default";
4007 
4008  if (filename.isEmpty() || filename.contains("/../") ||
4009  filename.startsWith("../"))
4010  {
4011  LOG(VB_GENERAL, LOG_ERR, LOC +
4012  QString("ERROR QueryFindFile, filename '%1' "
4013  "fails sanity checks").arg(filename));
4014  fileList << "ERROR: Bad/Missing Filename";
4015  SendResponse(pbs->getSocket(), fileList);
4016  return;
4017  }
4018 
4019  if (slist.size() >= 5)
4020  useRegex = (slist[4].toInt() > 0);
4021 
4022  if (slist.size() >= 6)
4023  allowFallback = (slist[5].toInt() > 0);
4024 
4025  LOG(VB_FILE, LOG_INFO, LOC +
4026  QString("Looking for file '%1' on host '%2' in group '%3' (useregex: %4, allowfallback: %5")
4027  .arg(filename).arg(hostname).arg(storageGroup).arg(useRegex).arg(allowFallback));
4028 
4029  // first check the given host
4031  {
4032  LOG(VB_FILE, LOG_INFO, LOC + QString("Checking local host '%1' for file").arg(gCoreContext->GetHostName()));
4033 
4034  // check the local storage group
4035  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName(), false);
4036 
4037  if (useRegex)
4038  {
4039  QFileInfo fi(filename);
4040  QStringList files = sgroup.GetFileList('/' + fi.path());
4041 
4042  LOG(VB_FILE, LOG_INFO, LOC + QString("Looking in dir '%1' for '%2'").arg(fi.path()).arg(fi.fileName()));
4043 
4044  for (int x = 0; x < files.size(); x++)
4045  {
4046  LOG(VB_FILE, LOG_INFO, LOC + QString("Found '%1 - %2'").arg(x).arg(files[x]));
4047  }
4048 
4049  QStringList filteredFiles = files.filter(QRegExp(fi.fileName()));
4050  for (int x = 0; x < filteredFiles.size(); x++)
4051  {
4054  fi.path() + '/' + filteredFiles[x],
4055  storageGroup);
4056  }
4057  }
4058  else
4059  {
4060  if (!sgroup.FindFile(filename).isEmpty())
4061  {
4064  filename, storageGroup);
4065  }
4066  }
4067  }
4068  else
4069  {
4070  LOG(VB_FILE, LOG_INFO, LOC + QString("Checking remote host '%1' for file").arg(hostname));
4071 
4072  // check the given slave hostname
4074  if (slave)
4075  {
4076  QStringList slaveFiles = slave->GetFindFile(hostname, filename, storageGroup, useRegex);
4077 
4078  if (!slaveFiles.isEmpty() && slaveFiles[0] != "NOT FOUND" && !slaveFiles[0].startsWith("ERROR: "))
4079  fileList += slaveFiles;
4080 
4081  slave->DecrRef();
4082  }
4083  else
4084  {
4085  LOG(VB_FILE, LOG_INFO, LOC + QString("Slave '%1' was unreachable").arg(hostname));
4086  fileList << QString("ERROR: SLAVE UNREACHABLE: %1").arg(hostname);
4087  SendResponse(pbs->getSocket(), fileList);
4088  return;
4089  }
4090  }
4091 
4092  // if we still haven't found it and this is the master and fallback is enabled
4093  // check all other slaves that have a directory in the storagegroup
4094  if (m_ismaster && fileList.isEmpty() && allowFallback)
4095  {
4096  // get a list of hosts
4098 
4099  QString sql = "SELECT DISTINCT hostname "
4100  "FROM storagegroup "
4101  "WHERE groupname = :GROUP "
4102  "AND hostname != :HOSTNAME";
4103  query.prepare(sql);
4104  query.bindValue(":GROUP", storageGroup);
4105  query.bindValue(":HOSTNAME", hostname);
4106 
4107  if (!query.exec() || !query.isActive())
4108  {
4109  MythDB::DBError(LOC + "FindFile() get host list", query);
4110  fileList << "ERROR: failed to get host list";
4111  SendResponse(pbs->getSocket(), fileList);
4112  return;
4113  }
4114 
4115  while(query.next())
4116  {
4117  hostname = query.value(0).toString();
4118 
4120  {
4121  StorageGroup sgroup(storageGroup, hostname);
4122 
4123  if (useRegex)
4124  {
4125  QFileInfo fi(filename);
4126  QStringList files = sgroup.GetFileList('/' + fi.path());
4127 
4128  LOG(VB_FILE, LOG_INFO, LOC + QString("Looking in dir '%1' for '%2'").arg(fi.path()).arg(fi.fileName()));
4129 
4130  for (int x = 0; x < files.size(); x++)
4131  {
4132  LOG(VB_FILE, LOG_INFO, LOC + QString("Found '%1 - %2'").arg(x).arg(files[x]));
4133  }
4134 
4135  QStringList filteredFiles = files.filter(QRegExp(fi.fileName()));
4136 
4137  for (int x = 0; x < filteredFiles.size(); x++)
4138  {
4141  fi.path() + '/' + filteredFiles[x],
4142  storageGroup);
4143  }
4144  }
4145  else
4146  {
4147  QString fname = sgroup.FindFile(filename);
4148  if (!fname.isEmpty())
4149  {
4152  filename, storageGroup);
4153  }
4154  }
4155  }
4156  else
4157  {
4158  // check the slave host
4160  if (slave)
4161  {
4162  QStringList slaveFiles = slave->GetFindFile(hostname, filename, storageGroup, useRegex);
4163  if (!slaveFiles.isEmpty() && slaveFiles[0] != "NOT FOUND" && !slaveFiles[0].startsWith("ERROR: "))
4164  fileList += slaveFiles;
4165 
4166  slave->DecrRef();
4167  }
4168  }
4169 
4170  if (!fileList.isEmpty())
4171  break;
4172  }
4173  }
4174 
4175  if (fileList.isEmpty())
4176  {
4177  fileList << "NOT FOUND";
4178  LOG(VB_FILE, LOG_INFO, LOC + QString("File was not found"));
4179  }
4180  else
4181  {
4182  for (int x = 0; x < fileList.size(); x++)
4183  {
4184  LOG(VB_FILE, LOG_INFO, LOC + QString("File %1 was found at: '%2'").arg(x).arg(fileList[0]));
4185  }
4186  }
4187 
4188  SendResponse(pbs->getSocket(), fileList);
4189 }
4190 
4191 void MainServer::HandleSGFileQuery(QStringList &sList,
4192  PlaybackSock *pbs)
4193 {
4194 //format: QUERY_SG_FILEQUERY <host> <storagegroup> <filename> <allowfallback (optional)>
4195 
4196  MythSocket *pbssock = pbs->getSocket();
4197  QStringList strList;
4198 
4199  if (sList.size() < 4)
4200  {
4201  LOG(VB_GENERAL, LOG_ERR, LOC +
4202  QString("HandleSGFileQuery: Invalid Request. %1")
4203  .arg(sList.join("[]:[]")));
4204  strList << "EMPTY LIST";
4205  SendResponse(pbssock, strList);
4206  return;
4207  }
4208 
4209  QString host = gCoreContext->GetHostName();
4210  QString wantHost = sList.at(1);
4211  QHostAddress wantHostaddr(wantHost);
4212  QString groupname = sList.at(2);
4213  QString filename = sList.at(3);
4214 
4215  bool allowFallback = true;
4216  if (sList.size() >= 5)
4217  allowFallback = (sList.at(4).toInt() > 0);
4218  LOG(VB_FILE, LOG_ERR, QString("HandleSGFileQuery - allowFallback: %1").arg(allowFallback));
4219 
4220  bool slaveUnreachable = false;
4221 
4222  LOG(VB_FILE, LOG_INFO, LOC + QString("HandleSGFileQuery: %1")
4223  .arg(gCoreContext->GenMythURL(wantHost, 0, filename, groupname)));
4224 
4225  QString addr = gCoreContext->GetBackendServerIP();
4226 
4227  if ((host.toLower() == wantHost.toLower()) ||
4228  (!addr.isEmpty() && addr == wantHostaddr.toString()))
4229  {
4230  LOG(VB_FILE, LOG_INFO, LOC + "HandleSGFileQuery: Getting local info");
4231  StorageGroup sg(groupname, gCoreContext->GetHostName(), allowFallback);
4232  strList = sg.GetFileInfo(filename);
4233  }
4234  else
4235  {
4236  PlaybackSock *slave = GetMediaServerByHostname(wantHost);
4237  if (slave)
4238  {
4239  LOG(VB_FILE, LOG_INFO, LOC +
4240  "HandleSGFileQuery: Getting remote info");
4241  strList = slave->GetSGFileQuery(wantHost, groupname, filename);
4242  slave->DecrRef();
4243  slaveUnreachable = false;
4244  }
4245  else
4246  {
4247  LOG(VB_FILE, LOG_INFO, LOC +
4248  QString("HandleSGFileQuery: Failed to grab slave socket : %1 :")
4249  .arg(wantHost));
4250  slaveUnreachable = true;
4251  }
4252 
4253  }
4254 
4255  if (slaveUnreachable)
4256  strList << "SLAVE UNREACHABLE: " << wantHost;
4257 
4258  if (strList.count() == 0 || (strList.at(0) == "0"))
4259  strList << "EMPTY LIST";
4260 
4261  SendResponse(pbssock, strList);
4262 }
4263 
4265 {
4266  MythSocket *pbssock = pbs->getSocket();
4267  QString pbshost = pbs->getHostname();
4268 
4269  QStringList strlist;
4270 
4271  EncoderLink *encoder = nullptr;
4272  QString enchost;
4273 
4274  TVRec::s_inputsLock.lockForRead();
4275  for (auto * elink : qAsConst(*m_encoderList))
4276  {
4277  // we're looking for a specific card but this isn't the one we want
4278  if ((cardid != -1) && (cardid != elink->GetInputID()))
4279  continue;
4280 
4281  if (elink->IsLocal())
4282  enchost = gCoreContext->GetHostName();
4283  else
4284  enchost = elink->GetHostName();
4285 
4286  if ((enchost == pbshost) &&
4287  (elink->IsConnected()) &&
4288  (!elink->IsBusy()) &&
4289  (!elink->IsTunerLocked()))
4290  {
4291  encoder = elink;
4292  break;
4293  }
4294  }
4295  TVRec::s_inputsLock.unlock();
4296 
4297  if (encoder)
4298  {
4299  int retval = encoder->LockTuner();
4300 
4301  if (retval != -1)
4302  {
4303  QString msg = QString("Cardid %1 LOCKed for external use on %2.")
4304  .arg(retval).arg(pbshost);
4305  LOG(VB_GENERAL, LOG_INFO, LOC + msg);
4306 
4308  query.prepare("SELECT videodevice, audiodevice, "
4309  "vbidevice "
4310  "FROM capturecard "
4311  "WHERE cardid = :CARDID ;");
4312  query.bindValue(":CARDID", retval);
4313 
4314  if (query.exec() && query.next())
4315  {
4316  // Success
4317  strlist << QString::number(retval)
4318  << query.value(0).toString()
4319  << query.value(1).toString()
4320  << query.value(2).toString();
4321 
4322  if (m_sched)
4323  m_sched->ReschedulePlace("LockTuner");
4324 
4325  SendResponse(pbssock, strlist);
4326  return;
4327  }
4328  LOG(VB_GENERAL, LOG_ERR, LOC +
4329  "MainServer::LockTuner(): Could not find "
4330  "card info in database");
4331  }
4332  else
4333  {
4334  // Tuner already locked
4335  strlist << "-2" << "" << "" << "";
4336  SendResponse(pbssock, strlist);
4337  return;
4338  }
4339  }
4340 
4341  strlist << "-1" << "" << "" << "";
4342  SendResponse(pbssock, strlist);
4343 }
4344 
4346 {
4347  MythSocket *pbssock = pbs->getSocket();
4348  QStringList strlist;
4349  EncoderLink *encoder = nullptr;
4350 
4351  TVRec::s_inputsLock.lockForRead();
4352  auto iter = m_encoderList->constFind(cardid);
4353  if (iter == m_encoderList->constEnd())
4354  {
4355  LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleFreeTuner() " +
4356  QString("Unknown encoder: %1").arg(cardid));
4357  strlist << "FAILED";
4358  }
4359  else
4360  {
4361  encoder = *iter;
4362  encoder->FreeTuner();
4363 
4364  QString msg = QString("Cardid %1 FREED from external use on %2.")
4365  .arg(cardid).arg(pbs->getHostname());
4366  LOG(VB_GENERAL, LOG_INFO, LOC + msg);
4367 
4368  if (m_sched)
4369  m_sched->ReschedulePlace("FreeTuner");
4370 
4371  strlist << "OK";
4372  }
4373  TVRec::s_inputsLock.unlock();
4374 
4375  SendResponse(pbssock, strlist);
4376 }
4377 
4378 static bool comp_livetvorder(const InputInfo &a, const InputInfo &b)
4379 {
4380  if (a.m_liveTvOrder != b.m_liveTvOrder)
4381  return a.m_liveTvOrder < b.m_liveTvOrder;
4382  return a.m_inputId < b.m_inputId;
4383 }
4384 
4386  uint excluded_input)
4387 {
4388  LOG(VB_CHANNEL, LOG_INFO,
4389  LOC + QString("Excluding input %1")
4390  .arg(excluded_input));
4391 
4392  MythSocket *pbssock = pbs->getSocket();
4393  vector<InputInfo> busyinputs;
4394  vector<InputInfo> freeinputs;
4395  QMap<uint, QSet<uint> > groupids;
4396 
4397  // Lopp over each encoder and divide the inputs into busy and free
4398  // lists.
4399  TVRec::s_inputsLock.lockForRead();
4400  for (auto * elink : qAsConst(*m_encoderList))
4401  {
4402  InputInfo info;
4403  info.m_inputId = elink->GetInputID();
4404 
4405  if (!elink->IsConnected() || elink->IsTunerLocked())
4406  {
4407  LOG(VB_CHANNEL, LOG_INFO,
4408  LOC + QString("Input %1 is locked or not connected")
4409  .arg(info.m_inputId));
4410  continue;
4411  }
4412 
4413  vector<uint> infogroups;
4414  CardUtil::GetInputInfo(info, &infogroups);
4415  for (uint group : infogroups)
4416  groupids[info.m_inputId].insert(group);
4417 
4418  InputInfo busyinfo;
4419  if (info.m_inputId != excluded_input && elink->IsBusy(&busyinfo))
4420  {
4421  LOG(VB_CHANNEL, LOG_DEBUG,
4422  LOC + QString("Input %1 is busy on %2/%3")
4423  .arg(info.m_inputId).arg(busyinfo.m_chanId).arg(busyinfo.m_mplexId));
4424  info.m_chanId = busyinfo.m_chanId;
4425  info.m_mplexId = busyinfo.m_mplexId;
4426  busyinputs.push_back(info);
4427  }
4428  else if (info.m_liveTvOrder)
4429  {
4430  LOG(VB_CHANNEL, LOG_DEBUG,
4431  LOC + QString("Input %1 is free")
4432  .arg(info.m_inputId));
4433  freeinputs.push_back(info);
4434  }
4435  }
4436  TVRec::s_inputsLock.unlock();
4437 
4438  // Loop over each busy input and restrict or delete any free
4439  // inputs that are in the same group.
4440  for (auto & busyinfo : busyinputs)
4441  {
4442  auto freeiter = freeinputs.begin();
4443  while (freeiter != freeinputs.end())
4444  {
4445  InputInfo &freeinfo = *freeiter;
4446 
4447  if ((groupids[busyinfo.m_inputId] & groupids[freeinfo.m_inputId])
4448  .isEmpty())
4449  {
4450  ++freeiter;
4451  continue;
4452  }
4453 
4454  if (busyinfo.m_sourceId == freeinfo.m_sourceId)
4455  {
4456  LOG(VB_CHANNEL, LOG_DEBUG,
4457  LOC + QString("Input %1 is limited to %2/%3 by input %4")
4458  .arg(freeinfo.m_inputId).arg(busyinfo.m_chanId)
4459  .arg(busyinfo.m_mplexId).arg(busyinfo.m_inputId));
4460  freeinfo.m_chanId = busyinfo.m_chanId;
4461  freeinfo.m_mplexId = busyinfo.m_mplexId;
4462  ++freeiter;
4463  continue;
4464  }
4465 
4466  LOG(VB_CHANNEL, LOG_DEBUG,
4467  LOC + QString("Input %1 is unavailable by input %2")
4468  .arg(freeinfo.m_inputId).arg(busyinfo.m_inputId));
4469  freeiter = freeinputs.erase(freeiter);
4470  }
4471  }
4472 
4473  // Return the results in livetvorder.
4474  stable_sort(freeinputs.begin(), freeinputs.end(), comp_livetvorder);
4475  QStringList strlist;
4476  for (auto & input : freeinputs)
4477  {
4478  LOG(VB_CHANNEL, LOG_INFO,
4479  LOC + QString("Input %1 is available on %2/%3")
4480  .arg(input.m_inputId).arg(input.m_chanId)
4481  .arg(input.m_mplexId));
4482  input.ToStringList(strlist);
4483  }
4484 
4485  if (strlist.empty())
4486  strlist << "OK";
4487 
4488  SendResponse(pbssock, strlist);
4489 }
4490 
4491 static QString cleanup(const QString &str)
4492 {
4493  if (str == " ")
4494  return "";
4495  return str;
4496 }
4497 
4498 static QString make_safe(const QString &str)
4499 {
4500  if (str.isEmpty())
4501  return " ";
4502  return str;
4503 }
4504 
4505 void MainServer::HandleRecorderQuery(QStringList &slist, QStringList &commands,
4506  PlaybackSock *pbs)
4507 {
4508  MythSocket *pbssock = pbs->getSocket();
4509 
4510  if (commands.size() < 2 || slist.size() < 2)
4511  return;
4512 
4513  int recnum = commands[1].toInt();
4514 
4515  TVRec::s_inputsLock.lockForRead();
4516  auto iter = m_encoderList->constFind(recnum);
4517  if (iter == m_encoderList->constEnd())
4518  {
4519  TVRec::s_inputsLock.unlock();
4520  LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleRecorderQuery() " +
4521  QString("Unknown encoder: %1").arg(recnum));
4522  QStringList retlist( "bad" );
4523  SendResponse(pbssock, retlist);
4524  return;
4525  }
4526  TVRec::s_inputsLock.unlock();
4527 
4528  QString command = slist[1];
4529 
4530  QStringList retlist;
4531 
4532  EncoderLink *enc = *iter;
4533  if (!enc->IsConnected())
4534  {
4535  LOG(VB_GENERAL, LOG_ERR, LOC + " MainServer::HandleRecorderQuery() " +
4536  QString("Command %1 for unconnected encoder %2")
4537  .arg(command).arg(recnum));
4538  retlist << "bad";
4539  SendResponse(pbssock, retlist);
4540  return;
4541  }
4542 
4543  if (command == "IS_RECORDING")
4544  {
4545  retlist << QString::number((int)enc->IsReallyRecording());
4546  }
4547  else if (command == "GET_FRAMERATE")
4548  {
4549  retlist << QString::number(enc->GetFramerate());
4550  }
4551  else if (command == "GET_FRAMES_WRITTEN")
4552  {
4553  retlist << QString::number(enc->GetFramesWritten());
4554  }
4555  else if (command == "GET_FILE_POSITION")
4556  {
4557  retlist << QString::number(enc->GetFilePosition());
4558  }
4559  else if (command == "GET_MAX_BITRATE")
4560  {
4561  retlist << QString::number(enc->GetMaxBitrate());
4562  }
4563  else if (command == "GET_CURRENT_RECORDING")
4564  {
4565  ProgramInfo *info = enc->GetRecording();
4566  if (info)
4567  {
4568  info->ToStringList(retlist);
4569  delete info;
4570  }
4571  else
4572  {
4573  ProgramInfo dummy;
4574  dummy.SetInputID(enc->GetInputID());
4575  dummy.ToStringList(retlist);
4576  }
4577  }
4578  else if (command == "GET_KEYFRAME_POS")
4579  {
4580  long long desired = slist[2].toLongLong();
4581  retlist << QString::number(enc->GetKeyframePosition(desired));
4582  }
4583  else if (command == "FILL_POSITION_MAP")
4584  {
4585  int64_t start = slist[2].toLongLong();
4586  int64_t end = slist[3].toLongLong();
4587  frm_pos_map_t map;
4588 
4589  if (!enc->GetKeyframePositions(start, end, map))
4590  {
4591  retlist << "error";
4592  }
4593  else
4594  {
4595  frm_pos_map_t::const_iterator it = map.begin();
4596  for (; it != map.end(); ++it)
4597  {
4598  retlist += QString::number(it.key());
4599  retlist += QString::number(*it);
4600  }
4601  if (retlist.empty())
4602  retlist << "OK";
4603  }
4604  }
4605  else if (command == "FILL_DURATION_MAP")
4606  {
4607  int64_t start = slist[2].toLongLong();
4608  int64_t end = slist[3].toLongLong();
4609  frm_pos_map_t map;
4610 
4611  if (!enc->GetKeyframeDurations(start, end, map))
4612  {
4613  retlist << "error";
4614  }
4615  else
4616  {
4617  frm_pos_map_t::const_iterator it = map.begin();
4618  for (; it != map.end(); ++it)
4619  {
4620  retlist += QString::number(it.key());
4621  retlist += QString::number(*it);
4622  }
4623  if (retlist.empty())
4624  retlist << "OK";
4625  }
4626  }
4627  else if (command == "GET_RECORDING")
4628  {
4629  ProgramInfo *pginfo = enc->GetRecording();
4630  if (pginfo)
4631  {
4632  pginfo->ToStringList(retlist);
4633  delete pginfo;
4634  }
4635  else
4636  {
4637  ProgramInfo dummy;
4638  dummy.SetInputID(enc->GetInputID());
4639  dummy.ToStringList(retlist);
4640  }
4641  }
4642  else if (command == "FRONTEND_READY")
4643  {
4644  enc->FrontendReady();
4645  retlist << "OK";
4646  }
4647  else if (command == "CANCEL_NEXT_RECORDING")
4648  {
4649  QString cancel = slist[2];
4650  LOG(VB_GENERAL, LOG_NOTICE, LOC +
4651  QString("Received: CANCEL_NEXT_RECORDING %1").arg(cancel));
4652  enc->CancelNextRecording(cancel == "1");
4653  retlist << "OK";
4654  }
4655  else if (command == "SPAWN_LIVETV")
4656  {
4657  QString chainid = slist[2];
4658  LiveTVChain *chain = GetExistingChain(chainid);
4659  if (!chain)
4660  {
4661  chain = new LiveTVChain();
4662  chain->LoadFromExistingChain(chainid);
4663  AddToChains(chain);
4664  }
4665 
4666  chain->SetHostSocket(pbssock);
4667 
4668  enc->SpawnLiveTV(chain, slist[3].toInt() != 0, slist[4]);
4669  retlist << "OK";
4670  }
4671  else if (command == "STOP_LIVETV")
4672  {
4673  QString chainid = enc->GetChainID();
4674  enc->StopLiveTV();
4675 
4676  LiveTVChain *chain = GetExistingChain(chainid);
4677  if (chain)
4678  {
4679  chain->DelHostSocket(pbssock);
4680  if (chain->HostSocketCount() == 0)
4681  {
4682  DeleteChain(chain);
4683  }
4684  }
4685 
4686  retlist << "OK";
4687  }
4688  else if (command == "PAUSE")
4689  {
4690  enc->PauseRecorder();
4691  retlist << "OK";
4692  }
4693  else if (command == "FINISH_RECORDING")
4694  {
4695  enc->FinishRecording();
4696  retlist << "OK";
4697  }
4698  else if (command == "SET_LIVE_RECORDING")
4699  {
4700  int recording = slist[2].toInt();
4701  enc->SetLiveRecording(recording);
4702  retlist << "OK";
4703  }
4704  else if (command == "GET_INPUT")
4705  {
4706  QString ret = enc->GetInput();
4707  ret = (ret.isEmpty()) ? "UNKNOWN" : ret;
4708  retlist << ret;
4709  }
4710  else if (command == "SET_INPUT")
4711  {
4712  QString input = slist[2];
4713  QString ret = enc->SetInput(input);
4714  ret = (ret.isEmpty()) ? "UNKNOWN" : ret;
4715  retlist << ret;
4716  }
4717  else if (command == "TOGGLE_CHANNEL_FAVORITE")
4718  {
4719  QString changroup = slist[2];
4720  enc->ToggleChannelFavorite(changroup);
4721  retlist << "OK";
4722  }
4723  else if (command == "CHANGE_CHANNEL")
4724  {
4725  auto direction = (ChannelChangeDirection) slist[2].toInt();
4726  enc->ChangeChannel(direction);
4727  retlist << "OK";
4728  }
4729  else if (command == "SET_CHANNEL")
4730  {
4731  QString name = slist[2];
4732  enc->SetChannel(name);
4733  retlist << "OK";
4734  }
4735  else if (command == "SET_SIGNAL_MONITORING_RATE")
4736  {
4737  int rate = slist[2].toInt();
4738  int notifyFrontend = slist[3].toInt();
4739  int oldrate = enc->SetSignalMonitoringRate(rate, notifyFrontend);
4740  retlist << QString::number(oldrate);
4741  }
4742  else if (command == "GET_COLOUR")
4743  {
4745  retlist << QString::number(ret);
4746  }
4747  else if (command == "GET_CONTRAST")
4748  {
4750  retlist << QString::number(ret);
4751  }
4752  else if (command == "GET_BRIGHTNESS")
4753  {
4755  retlist << QString::number(ret);
4756  }
4757  else if (command == "GET_HUE")
4758  {
4760  retlist << QString::number(ret);
4761  }
4762  else if (command == "CHANGE_COLOUR")
4763  {
4764  int type = slist[2].toInt();
4765  bool up = slist[3].toInt() != 0;
4766  int ret = enc->ChangePictureAttribute(
4768  retlist << QString::number(ret);
4769  }
4770  else if (command == "CHANGE_CONTRAST")
4771  {
4772  int type = slist[2].toInt();
4773  bool up = slist[3].toInt() != 0;
4774  int ret = enc->ChangePictureAttribute(
4776  retlist << QString::number(ret);
4777  }
4778  else if (command == "CHANGE_BRIGHTNESS")
4779  {
4780  int type= slist[2].toInt();
4781  bool up = slist[3].toInt() != 0;
4782  int ret = enc->ChangePictureAttribute(
4784  retlist << QString::number(ret);
4785  }
4786  else if (command == "CHANGE_HUE")
4787  {
4788  int type= slist[2].toInt();
4789  bool up = slist[3].toInt() != 0;
4790  int ret = enc->ChangePictureAttribute(
4792  retlist << QString::number(ret);
4793  }
4794  else if (command == "CHECK_CHANNEL")
4795  {
4796  QString name = slist[2];
4797  retlist << QString::number((int)(enc->CheckChannel(name)));
4798  }
4799  else if (command == "SHOULD_SWITCH_CARD")
4800  {
4801  QString chanid = slist[2];
4802  retlist << QString::number((int)(enc->ShouldSwitchToAnotherInput(chanid)));
4803  }
4804  else if (command == "CHECK_CHANNEL_PREFIX")
4805  {
4806  QString needed_spacer;
4807  QString prefix = slist[2];
4808  uint complete_valid_channel_on_rec = 0;
4809  bool is_extra_char_useful = false;
4810 
4811  bool match = enc->CheckChannelPrefix(
4812  prefix, complete_valid_channel_on_rec,
4813  is_extra_char_useful, needed_spacer);
4814 
4815  retlist << QString::number((int)match);
4816  retlist << QString::number(complete_valid_channel_on_rec);
4817  retlist << QString::number((int)is_extra_char_useful);
4818  retlist << ((needed_spacer.isEmpty()) ? QString("X") : needed_spacer);
4819  }
4820  else if (command == "GET_NEXT_PROGRAM_INFO" && (slist.size() >= 6))
4821  {
4822  QString channelname = slist[2];
4823  uint chanid = slist[3].toUInt();
4824  auto direction = (BrowseDirection)slist[4].toInt();
4825  QString starttime = slist[5];
4826 
4827  QString title = "";
4828  QString subtitle = "";
4829  QString desc = "";
4830  QString category = "";
4831  QString endtime = "";
4832  QString callsign = "";
4833  QString iconpath = "";
4834  QString seriesid = "";
4835  QString programid = "";
4836 
4837  enc->GetNextProgram(direction,
4838  title, subtitle, desc, category, starttime,
4839  endtime, callsign, iconpath, channelname, chanid,
4840  seriesid, programid);
4841 
4842  retlist << make_safe(title);
4843  retlist << make_safe(subtitle);
4844  retlist << make_safe(desc);
4845  retlist << make_safe(category);
4846  retlist << make_safe(starttime);
4847  retlist << make_safe(endtime);
4848  retlist << make_safe(callsign);
4849  retlist << make_safe(iconpath);
4850  retlist << make_safe(channelname);
4851  retlist << QString::number(chanid);
4852  retlist << make_safe(seriesid);
4853  retlist << make_safe(programid);
4854  }
4855  else if (command == "GET_CHANNEL_INFO")
4856  {
4857  uint chanid = slist[2].toUInt();
4858  uint sourceid = 0;
4859  QString callsign = "";
4860  QString channum = "";
4861  QString channame = "";
4862  QString xmltv = "";
4863 
4864  enc->GetChannelInfo(chanid, sourceid,
4865  callsign, channum, channame, xmltv);
4866 
4867  retlist << QString::number(chanid);
4868  retlist << QString::number(sourceid);
4869  retlist << make_safe(callsign);
4870  retlist << make_safe(channum);
4871  retlist << make_safe(channame);
4872  retlist << make_safe(xmltv);
4873  }
4874  else
4875  {
4876  LOG(VB_GENERAL, LOG_ERR, LOC +
4877  QString("Unknown command: %1").arg(command));
4878  retlist << "OK";
4879  }
4880 
4881  SendResponse(pbssock, retlist);
4882 }
4883 
4884 void MainServer::HandleSetNextLiveTVDir(QStringList &commands,
4885  PlaybackSock *pbs)
4886 {
4887  MythSocket *pbssock = pbs->getSocket();
4888 
4889  int recnum = commands[1].toInt();
4890 
4891  TVRec::s_inputsLock.lockForRead();
4892  auto iter = m_encoderList->constFind(recnum);
4893  if (iter == m_encoderList->constEnd())
4894  {
4895  TVRec::s_inputsLock.unlock();
4896  LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleSetNextLiveTVDir() " +
4897  QString("Unknown encoder: %1").arg(recnum));
4898  QStringList retlist( "bad" );
4899  SendResponse(pbssock, retlist);
4900  return;
4901  }
4902  TVRec::s_inputsLock.unlock();
4903 
4904  EncoderLink *enc = *iter;
4905  enc->SetNextLiveTVDir(commands[2]);
4906 
4907  QStringList retlist( "OK" );
4908  SendResponse(pbssock, retlist);
4909 }
4910 
4912 {
4913  bool ok = true;
4914  MythSocket *pbssock = pbs->getSocket();
4915  uint chanid = slist[1].toUInt();
4916  uint sourceid = slist[2].toUInt();
4917  QString oldcnum = cleanup(slist[3]);
4918  QString callsign = cleanup(slist[4]);
4919  QString channum = cleanup(slist[5]);
4920  QString channame = cleanup(slist[6]);
4921  QString xmltv = cleanup(slist[7]);
4922 
4923  QStringList retlist;
4924  if (!chanid || !sourceid)
4925  {
4926  retlist << "0";
4927  SendResponse(pbssock, retlist);
4928  return;
4929  }
4930 
4931  TVRec::s_inputsLock.lockForRead();
4932  for (auto * encoder : qAsConst(*m_encoderList))
4933  {
4934  if (encoder)
4935  {
4936  ok &= encoder->SetChannelInfo(chanid, sourceid, oldcnum,
4937  callsign, channum, channame, xmltv);
4938  }
4939  }
4940  TVRec::s_inputsLock.unlock();
4941 
4942  retlist << ((ok) ? "1" : "0");
4943  SendResponse(pbssock, retlist);
4944 }
4945 
4946 void MainServer::HandleRemoteEncoder(QStringList &slist, QStringList &commands,
4947  PlaybackSock *pbs)
4948 {
4949  MythSocket *pbssock = pbs->getSocket();
4950 
4951  int recnum = commands[1].toInt();
4952  QStringList retlist;
4953 
4954  TVRec::s_inputsLock.lockForRead();
4955  auto iter = m_encoderList->constFind(recnum);
4956  if (iter == m_encoderList->constEnd())
4957  {
4958  TVRec::s_inputsLock.unlock();
4959  LOG(VB_GENERAL, LOG_ERR, LOC +
4960  QString("HandleRemoteEncoder(cmd %1) ").arg(slist[1]) +
4961  QString("Unknown encoder: %1").arg(recnum));
4962  retlist << QString::number((int) kState_Error);
4963  SendResponse(pbssock, retlist);
4964  return;
4965  }
4966  TVRec::s_inputsLock.unlock();
4967 
4968  EncoderLink *enc = *iter;
4969 
4970  QString command = slist[1];
4971 
4972  if (command == "GET_STATE")
4973  {
4974  retlist << QString::number((int)enc->GetState());
4975  }
4976  else if (command == "GET_SLEEPSTATUS")
4977  {
4978  retlist << QString::number(enc->GetSleepStatus());
4979  }
4980  else if (command == "GET_FLAGS")
4981  {
4982  retlist << QString::number(enc->GetFlags());
4983  }
4984  else if (command == "IS_BUSY")
4985  {
4986  int time_buffer = (slist.size() >= 3) ? slist[2].toInt() : 5;
4987  InputInfo busy_input;
4988  retlist << QString::number((int)enc->IsBusy(&busy_input, time_buffer));
4989  busy_input.ToStringList(retlist);
4990  }
4991  else if (command == "MATCHES_RECORDING" &&
4992  slist.size() >= (2 + NUMPROGRAMLINES))
4993  {
4994  QStringList::const_iterator it = slist.begin() + 2;
4995  ProgramInfo pginfo(it, slist.end());
4996 
4997  retlist << QString::number((int)enc->MatchesRecording(&pginfo));
4998  }
4999  else if (command == "START_RECORDING" &&
5000  slist.size() >= (2 + NUMPROGRAMLINES))
5001  {
5002  QStringList::const_iterator it = slist.begin() + 2;
5003  ProgramInfo pginfo(it, slist.end());
5004 
5005  retlist << QString::number(enc->StartRecording(&pginfo));
5006  retlist << QString::number(pginfo.GetRecordingID());
5007  retlist << QString::number(pginfo.GetRecordingStartTime().toSecsSinceEpoch());
5008  }
5009  else if (command == "GET_RECORDING_STATUS")
5010  {
5011  retlist << QString::number((int)enc->GetRecordingStatus());
5012  }
5013  else if (command == "RECORD_PENDING" &&
5014  (slist.size() >= 4 + NUMPROGRAMLINES))
5015  {
5016  int secsleft = slist[2].toInt();
5017  int haslater = slist[3].toInt();
5018  QStringList::const_iterator it = slist.begin() + 4;
5019  ProgramInfo pginfo(it, slist.end());
5020 
5021  enc->RecordPending(&pginfo, secsleft, haslater != 0);
5022 
5023  retlist << "OK";
5024  }
5025  else if (command == "CANCEL_NEXT_RECORDING" &&
5026  (slist.size() >= 3))
5027  {
5028  bool cancel = (bool) slist[2].toInt();
5029  enc->CancelNextRecording(cancel);
5030  retlist << "OK";
5031  }
5032  else if (command == "STOP_RECORDING")
5033  {
5034  enc->StopRecording();
5035  retlist << "OK";
5036  }
5037  else if (command == "GET_MAX_BITRATE")
5038  {
5039  retlist << QString::number(enc->GetMaxBitrate());
5040  }
5041  else if (command == "GET_CURRENT_RECORDING")
5042  {
5043  ProgramInfo *info = enc->GetRecording();
5044  if (info)
5045  {
5046  info->ToStringList(retlist);
5047  delete info;
5048  }
5049  else
5050  {
5051  ProgramInfo dummy;
5052  dummy.SetInputID(enc->GetInputID());
5053  dummy.ToStringList(retlist);
5054  }
5055  }
5056 
5057  SendResponse(pbssock, retlist);
5058 }
5059 
5060 void MainServer::GetActiveBackends(QStringList &hosts)
5061 {
5062  hosts.clear();
5063  hosts << gCoreContext->GetHostName();
5064 
5065  QString hostname;
5066  QReadLocker rlock(&m_sockListLock);
5067  for (auto & pbs : m_playbackList)
5068  {
5069  if (pbs->isMediaServer())
5070  {
5071  hostname = pbs->getHostname();
5072  if (!hosts.contains(hostname))
5073  hosts << hostname;
5074  }
5075  }
5076 }
5077 
5079 {
5080  QStringList retlist;
5081  GetActiveBackends(retlist);
5082  retlist.push_front(QString::number(retlist.size()));
5083  SendResponse(pbs->getSocket(), retlist);
5084 }
5085 
5087  PlaybackSock *pbs)
5088 {
5089  QStringList retlist;
5090  QString queryhostname = slist[1];
5091 
5092  if (gCoreContext->GetHostName() != queryhostname)
5093  {
5094  PlaybackSock *slave = GetSlaveByHostname(queryhostname);
5095  if (slave != nullptr)
5096  {
5097  retlist << "TRUE";
5098  slave->DecrRef();
5099  }
5100  else
5101  retlist << "FALSE";
5102  }
5103  else
5104  retlist << "TRUE";
5105 
5106  SendResponse(pbs->getSocket(), retlist);
5107 }
5108 
5109 int MainServer::GetfsID(const QList<FileSystemInfo>::iterator& fsInfo)
5110 {
5111  QString fskey = fsInfo->getHostname() + ":" + fsInfo->getPath();
5112  QMutexLocker lock(&m_fsIDcacheLock);
5113  if (!m_fsIDcache.contains(fskey))
5114  m_fsIDcache[fskey] = m_fsIDcache.count();
5115 
5116  return m_fsIDcache[fskey];
5117 }
5118 
5120 {
5121  size_t totalKBperMin = 0;
5122 
5123  TVRec::s_inputsLock.lockForRead();
5124  for (auto * enc : qAsConst(*m_encoderList))
5125  {
5126  if (!enc->IsConnected() || !enc->IsBusy())
5127  continue;
5128 
5129  long long maxBitrate = enc->GetMaxBitrate();
5130  if (maxBitrate<=0)
5131  maxBitrate = 19500000LL;
5132  long long thisKBperMin = (((size_t)maxBitrate)*((size_t)15))>>11;
5133  totalKBperMin += thisKBperMin;
5134  LOG(VB_FILE, LOG_INFO, LOC + QString("Cardid %1: max bitrate %2 KB/min")
5135  .arg(enc->GetInputID()).arg(thisKBperMin));
5136  }
5137  TVRec::s_inputsLock.unlock();
5138 
5139  LOG(VB_FILE, LOG_INFO, LOC +
5140  QString("Maximal bitrate of busy encoders is %1 KB/min")
5141  .arg(totalKBperMin));
5142 
5143  return totalKBperMin;
5144 }
5145 
5146 void MainServer::BackendQueryDiskSpace(QStringList &strlist, bool consolidated,
5147  bool allHosts)
5148 {
5149  QString allHostList = gCoreContext->GetHostName();
5150  int64_t totalKB = -1;
5151  int64_t usedKB = -1;
5152  QMap <QString, bool>foundDirs;
5153  QString driveKey;
5154  QString localStr = "1";
5155  struct statfs statbuf {};
5156  QStringList groups(StorageGroup::kSpecialGroups);
5157  groups.removeAll("LiveTV");
5158  QString specialGroups = groups.join("', '");
5159  QString sql = QString("SELECT MIN(id),dirname "
5160  "FROM storagegroup "
5161  "WHERE hostname = :HOSTNAME "
5162  "AND groupname NOT IN ( '%1' ) "
5163  "GROUP BY dirname;").arg(specialGroups);
5165  query.prepare(sql);
5166  query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
5167 
5168  if (query.exec())
5169  {
5170  // If we don't have any dirs of our own, fallback to list of Default
5171  // dirs since that is what StorageGroup::Init() does.
5172  if (!query.size())
5173  {
5174  query.prepare("SELECT MIN(id),dirname "
5175  "FROM storagegroup "
5176  "WHERE groupname = :GROUP "
5177  "GROUP BY dirname;");
5178  query.bindValue(":GROUP", "Default");
5179  if (!query.exec())
5180  MythDB::DBError("BackendQueryDiskSpace", query);
5181  }
5182 
5183  QDir checkDir("");
5184  QString dirID;
5185  QString currentDir;
5186  while (query.next())
5187  {
5188  dirID = query.value(0).toString();
5189  /* The storagegroup.dirname column uses utf8_bin collation, so Qt
5190  * uses QString::fromAscii() for toString(). Explicitly convert the
5191  * value using QString::fromUtf8() to prevent corruption. */
5192  currentDir = QString::fromUtf8(query.value(1)
5193  .toByteArray().constData());
5194  if (currentDir.endsWith("/"))
5195  currentDir.remove(currentDir.length() - 1, 1);
5196 
5197  checkDir.setPath(currentDir);
5198  if (!foundDirs.contains(currentDir))
5199  {
5200  if (checkDir.exists())
5201  {
5202  QByteArray cdir = currentDir.toLatin1();
5203  getDiskSpace(cdir.constData(), totalKB, usedKB);
5204  memset(&statbuf, 0, sizeof(statbuf));
5205  localStr = "1"; // Assume local
5206  int bSize = 0;
5207 
5208  if (statfs(currentDir.toLocal8Bit().constData(), &statbuf) == 0)
5209  {
5210 #if CONFIG_DARWIN
5211  char *fstypename = statbuf.f_fstypename;
5212  if ((!strcmp(fstypename, "nfs")) || // NFS|FTP
5213  (!strcmp(fstypename, "afpfs")) || // ApplShr
5214  (!strcmp(fstypename, "smbfs"))) // SMB
5215  localStr = "0";
5216 #elif __linux__
5217  long fstype = statbuf.f_type;
5218  if ((fstype == 0x6969) || // NFS
5219  (fstype == 0x517B) || // SMB
5220  (fstype == (long)0xFF534D42)) // CIFS
5221  localStr = "0";
5222 #endif
5223  bSize = statbuf.f_bsize;
5224  }
5225 
5226  strlist << gCoreContext->GetHostName();
5227  strlist << currentDir;
5228  strlist << localStr;
5229  strlist << "-1"; // Ignore fsID
5230  strlist << dirID;
5231  strlist << QString::number(bSize);
5232  strlist << QString::number(totalKB);
5233  strlist << QString::number(usedKB);
5234 
5235  foundDirs[currentDir] = true;
5236  }
5237  else
5238  foundDirs[currentDir] = false;
5239  }
5240  }
5241  }
5242 
5243  if (allHosts)
5244  {
5245  QMap <QString, bool> backendsCounted;
5246  QString pbsHost;
5247 
5248  list<PlaybackSock *> localPlaybackList;
5249 
5250  m_sockListLock.lockForRead();
5251 
5252  for (auto *pbs : m_playbackList)
5253  {
5254  if ((pbs->IsDisconnected()) ||
5255  (!pbs->isMediaServer()) ||
5256  (pbs->isLocal()) ||
5257  (backendsCounted.contains(pbs->getHostname())))
5258  continue;
5259 
5260  backendsCounted[pbs->getHostname()] = true;
5261  pbs->IncrRef();
5262  localPlaybackList.push_back(pbs);
5263  allHostList += "," + pbs->getHostname();
5264  }
5265 
5266  m_sockListLock.unlock();
5267 
5268  for (auto & pbs : localPlaybackList) {
5269  pbs->GetDiskSpace(strlist);
5270  pbs->DecrRef();
5271  }
5272  }
5273 
5274  if (!consolidated)
5275  return;
5276 
5277  QList<FileSystemInfo> fsInfos;
5278  QStringList::const_iterator it = strlist.begin();
5279  while (it != strlist.end())
5280  {
5281  FileSystemInfo fsInfo;
5282 
5283  fsInfo.setHostname(*(it++));
5284  fsInfo.setPath(*(it++));
5285  fsInfo.setLocal((*(it++)).toInt() > 0);
5286  fsInfo.setFSysID(-1);
5287  ++it; // Without this, the strlist gets out of whack
5288  fsInfo.setGroupID((*(it++)).toInt());
5289  fsInfo.setBlockSize((*(it++)).toInt());
5290  fsInfo.setTotalSpace((*(it++)).toLongLong());
5291  fsInfo.setUsedSpace((*(it++)).toLongLong());
5292  fsInfos.push_back(fsInfo);
5293  }
5294  strlist.clear();
5295 
5296  // Consolidate hosts sharing storage
5297  int64_t maxWriteFiveSec = GetCurrentMaxBitrate()/12 /*5 seconds*/;
5298  maxWriteFiveSec = max((int64_t)2048, maxWriteFiveSec); // safety for NFS mounted dirs
5299 
5300  for (auto it1 = fsInfos.begin(); it1 != fsInfos.end(); ++it1)
5301  {
5302  if (it1->getFSysID() == -1)
5303  {
5304  it1->setFSysID(GetfsID(it1));
5305  it1->setPath(
5306  it1->getHostname().section(".", 0, 0) + ":" + it1->getPath());
5307  }
5308 
5309  for (auto it2 = it1 + 1; it2 != fsInfos.end(); ++it2)
5310  {
5311  // our fuzzy comparison uses the maximum of the two block sizes
5312  // or 32, whichever is greater
5313  int bSize = max(32, max(it1->getBlockSize(), it2->getBlockSize()) / 1024);
5314  int64_t diffSize = it1->getTotalSpace() - it2->getTotalSpace();
5315  int64_t diffUsed = it1->getUsedSpace() - it2->getUsedSpace();
5316  if (diffSize < 0)
5317  diffSize = 0 - diffSize;
5318  if (diffUsed < 0)
5319  diffUsed = 0 - diffUsed;
5320 
5321  if (it2->getFSysID() == -1 && (diffSize <= bSize) &&
5322  (diffUsed <= maxWriteFiveSec))
5323  {
5324  if (!it1->getHostname().contains(it2->getHostname()))
5325  it1->setHostname(it1->getHostname() + "," + it2->getHostname());
5326  it1->setPath(it1->getPath() + "," +
5327  it2->getHostname().section(".", 0, 0) + ":" + it2->getPath());
5328  fsInfos.erase(it2);
5329  it2 = it1;
5330  }
5331  }
5332  }
5333 
5334  // Passed the cleaned list back
5335  totalKB = 0;
5336  usedKB = 0;
5337  for (const auto & fsInfo : qAsConst(fsInfos))
5338  {
5339  strlist << fsInfo.getHostname();
5340  strlist << fsInfo.getPath();
5341  strlist << QString::number(static_cast<int>(fsInfo.isLocal()));
5342  strlist << QString::number(fsInfo.getFSysID());
5343  strlist << QString::number(fsInfo.getGroupID());
5344  strlist << QString::number(fsInfo.getBlockSize());
5345  strlist << QString::number(fsInfo.getTotalSpace());
5346  strlist << QString::number(fsInfo.getUsedSpace());
5347 
5348  totalKB += fsInfo.getTotalSpace();
5349  usedKB += fsInfo.getUsedSpace();
5350  }
5351 
5352  if (allHosts)
5353  {
5354  strlist << allHostList;
5355  strlist << "TotalDiskSpace";
5356  strlist << "0";
5357  strlist << "-2";
5358  strlist << "-2";
5359  strlist << "0";
5360  strlist << QString::number(totalKB);
5361  strlist << QString::number(usedKB);
5362  }
5363 }
5364 
5365 void MainServer::GetFilesystemInfos(QList<FileSystemInfo> &fsInfos,
5366  bool useCache)
5367 {
5368  // Return cached information if requested.
5369  if (useCache)
5370  {
5371  QMutexLocker locker(&m_fsInfosCacheLock);
5372  fsInfos = m_fsInfosCache;
5373  return;
5374  }
5375 
5376  QStringList strlist;
5377  FileSystemInfo fsInfo;
5378 
5379  fsInfos.clear();
5380 
5381  BackendQueryDiskSpace(strlist, false, true);
5382 
5383  QStringList::const_iterator it = strlist.begin();
5384  while (it != strlist.end())
5385  {
5386  fsInfo.setHostname(*(it++));
5387  fsInfo.setPath(*(it++));
5388  fsInfo.setLocal((*(it++)).toInt() > 0);
5389  fsInfo.setFSysID(-1);
5390  ++it;
5391  fsInfo.setGroupID((*(it++)).toInt());
5392  fsInfo.setBlockSize((*(it++)).toInt());
5393  fsInfo.setTotalSpace((*(it++)).toLongLong());
5394  fsInfo.setUsedSpace((*(it++)).toLongLong());
5395  fsInfo.setWeight(0);
5396  fsInfos.push_back(fsInfo);
5397  }
5398 
5399  LOG(VB_SCHEDULE | VB_FILE, LOG_DEBUG, LOC +
5400  "Determining unique filesystems");
5401  size_t maxWriteFiveSec = GetCurrentMaxBitrate()/12 /*5 seconds*/;
5402  // safety for NFS mounted dirs
5403  maxWriteFiveSec = max((size_t)2048, maxWriteFiveSec);
5404 
5405  FileSystemInfo::Consolidate(fsInfos, false, maxWriteFiveSec);
5406 
5407  QList<FileSystemInfo>::iterator it1;
5408  if (VERBOSE_LEVEL_CHECK(VB_FILE | VB_SCHEDULE, LOG_INFO))
5409  {
5410  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5411  "--- GetFilesystemInfos directory list start ---");
5412  for (it1 = fsInfos.begin(); it1 != fsInfos.end(); ++it1)
5413  {
5414  QString msg =
5415  QString("Dir: %1:%2")
5416  .arg(it1->getHostname())
5417  .arg(it1->getPath());
5418  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC + msg) ;
5419  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5420  QString(" Location: %1")
5421  .arg(it1->isLocal() ? "Local" : "Remote"));
5422  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5423  QString(" fsID : %1")
5424  .arg(it1->getFSysID()));
5425  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5426  QString(" dirID : %1")
5427  .arg(it1->getGroupID()));
5428  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5429  QString(" BlkSize : %1")
5430  .arg(it1->getBlockSize()));
5431  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5432  QString(" TotalKB : %1")
5433  .arg(it1->getTotalSpace()));
5434  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5435  QString(" UsedKB : %1")
5436  .arg(it1->getUsedSpace()));
5437  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5438  QString(" FreeKB : %1")
5439  .arg(it1->getFreeSpace()));
5440  }
5441  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5442  "--- GetFilesystemInfos directory list end ---");
5443  }
5444 
5445  // Save these results to the cache.
5446  QMutexLocker locker(&m_fsInfosCacheLock);
5447  m_fsInfosCache = fsInfos;
5448 }
5449 
5450 void MainServer::HandleMoveFile(PlaybackSock *pbs, const QString &storagegroup,
5451  const QString &src, const QString &dst)
5452 {
5453  StorageGroup sgroup(storagegroup, "", false);
5454  QStringList retlist;
5455 
5456  if (src.isEmpty() || dst.isEmpty()
5457  || src.contains("..") || dst.contains(".."))
5458  {
5459  LOG(VB_GENERAL, LOG_ERR, LOC +
5460  QString("HandleMoveFile: ERROR moving file '%1' -> '%2', "
5461  "a path fails sanity checks").arg(src, dst));
5462  retlist << "0" << "Invalid path";
5463  SendResponse(pbs->getSocket(), retlist);
5464  return;
5465  }
5466 
5467  QString srcAbs = sgroup.FindFile(src);
5468  if (srcAbs.isEmpty())
5469  {
5470  LOG(VB_GENERAL, LOG_ERR, LOC +
5471  QString("HandleMoveFile: Unable to find %1").arg(src));
5472  retlist << "0" << "Source file not found";
5473  SendResponse(pbs->getSocket(), retlist);
5474  return;
5475  }
5476 
5477  // Path of files must be unique within SG. Rename will permit <sgdir1>/<dst>
5478  // even when <sgdir2>/<dst> already exists.
5479  // Directory paths do not have to be unique.
5480  QString dstAbs = sgroup.FindFile(dst);
5481  if (!dstAbs.isEmpty() && QFileInfo(dstAbs).isFile())
5482  {
5483  LOG(VB_GENERAL, LOG_ERR, LOC +
5484  QString("HandleMoveFile: Destination exists at %1").arg(dstAbs));
5485  retlist << "0" << "Destination file exists";
5486  SendResponse(pbs->getSocket(), retlist);
5487  return;
5488  }
5489 
5490  // Files never move filesystems, so use current SG dir
5491  int sgPathSize = srcAbs.size() - src.size();
5492  dstAbs = srcAbs.mid(0, sgPathSize) + dst;
5493 
5494  // Renaming on same filesystem should always be fast but is liable to delays
5495  // for unknowable reasons so we delegate to a separate thread for safety.
5496  auto *renamer = new RenameThread(*this, *pbs, srcAbs, dstAbs);
5497  MThreadPool::globalInstance()->start(renamer, "Rename");
5498 }
5499 
5501 
5503 {
5504  // Only permit one rename to run at any time
5505  QMutexLocker lock(&s_renamelock);
5506  LOG(VB_FILE, LOG_INFO, QString("MainServer::RenameThread: Renaming %1 -> %2")
5507  .arg(m_src, m_dst));
5508 
5509  QStringList retlist;
5510  QFileInfo fi(m_dst);
5511 
5512  if (QDir().mkpath(fi.path()) && QFile::rename(m_src, m_dst))
5513  {
5514  retlist << "1";
5515  }
5516  else
5517  {
5518  retlist << "0" << "Rename failed";
5519  LOG(VB_FILE, LOG_ERR, "MainServer::DoRenameThread: Rename failed");
5520  }
5521  m_ms.SendResponse(m_pbs.getSocket(), retlist);
5522 }
5523 
5525 {
5526  if (m_ms)
5527  m_ms->DoTruncateThread(this);
5528 }
5529 
5531 {
5532  if (gCoreContext->GetBoolSetting("TruncateDeletesSlowly", false))
5533  {
5534  TruncateAndClose(nullptr, ds->m_fd, ds->m_filename, ds->m_size);
5535  }
5536  else
5537  {
5538  QMutexLocker dl(&m_deletelock);
5539  close(ds->m_fd);
5540  }
5541 }
5542 
5544 {
5545  return HandleDeleteFile(slist[1], slist[2], pbs);
5546 }
5547 
5548 bool MainServer::HandleDeleteFile(const QString& filename, const QString& storagegroup,
5549  PlaybackSock *pbs)
5550 {
5551  StorageGroup sgroup(storagegroup, "", false);
5552  QStringList retlist;
5553 
5554  if ((filename.isEmpty()) ||
5555  (filename.contains("/../")) ||
5556  (filename.startsWith("../")))
5557  {
5558  LOG(VB_GENERAL, LOG_ERR, LOC +
5559  QString("ERROR deleting file, filename '%1' "
5560  "fails sanity checks").arg(filename));
5561  if (pbs)
5562  {
5563  retlist << "0";
5564  SendResponse(pbs->getSocket(), retlist);
5565  }
5566  return false;
5567  }
5568 
5569  QString fullfile = sgroup.FindFile(filename);
5570 
5571  if (fullfile.isEmpty()) {
5572  LOG(VB_GENERAL, LOG_ERR, LOC +
5573  QString("Unable to find %1 in HandleDeleteFile()") .arg(filename));
5574  if (pbs)
5575  {
5576  retlist << "0";
5577  SendResponse(pbs->getSocket(), retlist);
5578  }
5579  return false;
5580  }
5581 
5582  QFile checkFile(fullfile);
5583  bool followLinks = gCoreContext->GetBoolSetting("DeletesFollowLinks", false);
5584  off_t size = 0;
5585 
5586  // This will open the file and unlink the dir entry. The actual file
5587  // data will be deleted in the truncate thread spawned below.
5588  // Since stat fails after unlinking on some filesystems, get the size first
5589  const QFileInfo info(fullfile);
5590  size = info.size();
5591  int fd = DeleteFile(fullfile, followLinks);
5592 
5593  if ((fd < 0) && checkFile.exists())
5594  {
5595  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error deleting file: %1.")
5596  .arg(fullfile));
5597  if (pbs)
5598  {
5599  retlist << "0";
5600  SendResponse(pbs->getSocket(), retlist);
5601  }
5602  return false;
5603  }
5604 
5605  if (pbs)
5606  {
5607  retlist << "1";
5608  SendResponse(pbs->getSocket(), retlist);
5609  }
5610 
5611  // DeleteFile() opened up a file for us to delete
5612  if (fd >= 0)
5613  {
5614  // Thread off the actual file truncate
5615  auto *truncateThread = new TruncateThread(this, fullfile, fd, size);
5616  truncateThread->run();
5617  }
5618 
5619  return true;
5620 }
5621 
5622 // Helper function for the guts of HandleCommBreakQuery + HandleCutlistQuery
5623 void MainServer::HandleCutMapQuery(const QString &chanid,
5624  const QString &starttime,
5625  PlaybackSock *pbs, bool commbreak)
5626 {
5627  MythSocket *pbssock = nullptr;
5628  if (pbs)
5629  pbssock = pbs->getSocket();
5630 
5631  frm_dir_map_t markMap;
5632  frm_dir_map_t::const_iterator it;
5633  QDateTime recstartdt = MythDate::fromSecsSinceEpoch(starttime.toLongLong());
5634  QStringList retlist;
5635  int rowcnt = 0;
5636 
5637  const ProgramInfo pginfo(chanid.toUInt(), recstartdt);
5638 
5639  if (pginfo.GetChanID())
5640  {
5641  if (commbreak)
5642  pginfo.QueryCommBreakList(markMap);
5643  else
5644  pginfo.QueryCutList(markMap);
5645 
5646  for (it = markMap.begin(); it != markMap.end(); ++it)
5647  {
5648  rowcnt++;
5649  QString intstr = QString("%1").arg(*it);
5650  retlist << intstr;
5651  retlist << QString::number(it.key());
5652  }
5653  }
5654 
5655  if (rowcnt > 0)
5656  retlist.prepend(QString("%1").arg(rowcnt));
5657  else
5658  retlist << "-1";
5659 
5660  if (pbssock)
5661  SendResponse(pbssock, retlist);
5662 }
5663 
5664 void MainServer::HandleCommBreakQuery(const QString &chanid,
5665  const QString &starttime,
5666  PlaybackSock *pbs)
5667 {
5668 // Commercial break query
5669 // Format: QUERY_COMMBREAK <chanid> <starttime>
5670 // chanid is chanid, starttime is startime of program in
5671 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5672 // a ProgramInfo structure in a string list.
5673 // Return structure is [number of rows] followed by a triplet of values:
5674 // each triplet : [type] [long portion 1] [long portion 2]
5675 // type is the value in the map, right now 4 = commbreak start, 5= end
5676  return HandleCutMapQuery(chanid, starttime, pbs, true);
5677 }
5678 
5679 void MainServer::HandleCutlistQuery(const QString &chanid,
5680  const QString &starttime,
5681  PlaybackSock *pbs)
5682 {
5683 // Cutlist query
5684 // Format: QUERY_CUTLIST <chanid> <starttime>
5685 // chanid is chanid, starttime is startime of program in
5686 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5687 // a ProgramInfo structure in a string list.
5688 // Return structure is [number of rows] followed by a triplet of values:
5689 // each triplet : [type] [long portion 1] [long portion 2]
5690 // type is the value in the map, right now 0 = commbreak start, 1 = end
5691  return HandleCutMapQuery(chanid, starttime, pbs, false);
5692 }
5693 
5694 
5695 void MainServer::HandleBookmarkQuery(const QString &chanid,
5696  const QString &starttime,
5697  PlaybackSock *pbs)
5698 // Bookmark query
5699 // Format: QUERY_BOOKMARK <chanid> <starttime>
5700 // chanid is chanid, starttime is startime of program in
5701 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5702 // a ProgramInfo structure in a string list.
5703 // Return value is a long-long encoded as two separate values
5704 {
5705  MythSocket *pbssock = nullptr;
5706  if (pbs)
5707  pbssock = pbs->getSocket();
5708 
5709  QDateTime recstartts = MythDate::fromSecsSinceEpoch(starttime.toLongLong());
5710  uint64_t bookmark = ProgramInfo::QueryBookmark(
5711  chanid.toUInt(), recstartts);
5712 
5713  QStringList retlist;
5714  retlist << QString::number(bookmark);
5715 
5716  if (pbssock)
5717  SendResponse(pbssock, retlist);
5718 }
5719 
5720 
5721 void MainServer::HandleSetBookmark(QStringList &tokens,
5722  PlaybackSock *pbs)
5723 {
5724 // Bookmark query
5725 // Format: SET_BOOKMARK <chanid> <starttime> <position>
5726 // chanid is chanid, starttime is startime of program in
5727 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5728 // a ProgramInfo structure in a string list. The two longs are the two
5729 // portions of the bookmark value to set.
5730 
5731  MythSocket *pbssock = nullptr;
5732  if (pbs)
5733  pbssock = pbs->getSocket();
5734 
5735  QString chanid = tokens[1];
5736  QString starttime = tokens[2];
5737  long long bookmark = tokens[3].toLongLong();
5738 
5739  QDateTime recstartts = MythDate::fromSecsSinceEpoch(starttime.toLongLong());
5740  QStringList retlist;
5741 
5742  ProgramInfo pginfo(chanid.toUInt(), recstartts);
5743 
5744  if (pginfo.GetChanID())
5745  {
5746  pginfo.SaveBookmark(bookmark);
5747  retlist << "OK";
5748  }
5749  else
5750  retlist << "FAILED";
5751 
5752  if (pbssock)
5753  SendResponse(pbssock, retlist);
5754 }
5755 
5757 {
5758 // Format: QUERY_SETTING <hostname> <setting>
5759 // Returns setting value as a string
5760 
5761  MythSocket *pbssock = nullptr;
5762  if (pbs)
5763  pbssock = pbs->getSocket();
5764 
5765  QString hostname = tokens[1];
5766  QString setting = tokens[2];
5767  QStringList retlist;
5768 
5769  QString retvalue = gCoreContext->GetSettingOnHost(setting, hostname, "-1");
5770 
5771  retlist << retvalue;
5772  if (pbssock)
5773  SendResponse(pbssock, retlist);
5774 }
5775 
5776 void MainServer::HandleDownloadFile(const QStringList &command,
5777  PlaybackSock *pbs)
5778 {
5779  bool synchronous = (command[0] == "DOWNLOAD_FILE_NOW");
5780  QString srcURL = command[1];
5781  QString storageGroup = command[2];
5782  QString filename = command[3];
5783  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName(), false);
5784  QString outDir = sgroup.FindNextDirMostFree();
5785  QString outFile;
5786  QStringList retlist;
5787 
5788  MythSocket *pbssock = nullptr;
5789  if (pbs)
5790  pbssock = pbs->getSocket();
5791 
5792  if (filename.isEmpty())
5793  {
5794  QFileInfo finfo(srcURL);
5795  filename = finfo.fileName();
5796  }
5797 
5798  if (outDir.isEmpty())
5799  {
5800  LOG(VB_GENERAL, LOG_ERR, LOC +
5801  QString("Unable to determine directory "
5802  "to write to in %1 write command").arg(command[0]));
5803  retlist << "downloadfile_directory_not_found";
5804  if (pbssock)
5805  SendResponse(pbssock, retlist);
5806  return;
5807  }
5808 
5809  if ((filename.contains("/../")) ||
5810  (filename.startsWith("../")))
5811  {
5812  LOG(VB_GENERAL, LOG_ERR, LOC +
5813  QString("ERROR: %1 write filename '%2' does not pass "
5814  "sanity checks.") .arg(command[0]).arg(filename));
5815  retlist << "downloadfile_filename_dangerous";
5816  if (pbssock)
5817  SendResponse(pbssock, retlist);
5818  return;
5819  }
5820 
5821  outFile = outDir + "/" + filename;
5822 
5823  if (synchronous)
5824  {
5825  if (GetMythDownloadManager()->download(srcURL, outFile))
5826  {
5827  retlist << "OK";
5828  retlist << gCoreContext->GetMasterHostPrefix(storageGroup)
5829  + filename;
5830  }
5831  else
5832  retlist << "ERROR";
5833  }
5834  else
5835  {
5836  QMutexLocker locker(&m_downloadURLsLock);
5837  m_downloadURLs[outFile] =
5838  gCoreContext->GetMasterHostPrefix(storageGroup) +
5840 
5841  GetMythDownloadManager()->queueDownload(srcURL, outFile, this);
5842  retlist << "OK";
5843  retlist << gCoreContext->GetMasterHostPrefix(storageGroup) + filename;
5844  }
5845 
5846  if (pbssock)
5847  SendResponse(pbssock, retlist);
5848 }
5849 
5850 void MainServer::HandleSetSetting(QStringList &tokens,
5851  PlaybackSock *pbs)
5852 {
5853 // Format: SET_SETTING <hostname> <setting> <value>
5854  MythSocket *pbssock = nullptr;
5855  if (pbs)
5856  pbssock = pbs->getSocket();
5857 
5858  QString hostname = tokens[1];
5859  QString setting = tokens[2];
5860  QString svalue = tokens[3];
5861  QStringList retlist;
5862 
5863  if (gCoreContext->SaveSettingOnHost(setting, svalue, hostname))
5864  retlist << "OK";
5865  else
5866  retlist << "ERROR";
5867 
5868  if (pbssock)
5869  SendResponse(pbssock, retlist);
5870 }
5871 
5873 {
5874  MythSocket *pbssock = pbs->getSocket();
5875 
5876  QStringList retlist;
5877 
5878  if (m_metadatafactory)
5879  {
5880  QStringList hosts;
5881  GetActiveBackends(hosts);
5882  m_metadatafactory->VideoScan(hosts);
5883  retlist << "OK";
5884  }
5885  else
5886  retlist << "ERROR";
5887 
5888  if (pbssock)
5889  SendResponse(pbssock, retlist);
5890 }
5891 
5892 void MainServer::HandleScanMusic(const QStringList &slist, PlaybackSock *pbs)
5893 {
5894  MythSocket *pbssock = pbs->getSocket();
5895 
5896  QStringList strlist;
5897 
5898  if (m_ismaster)
5899  {
5900  // get a list of hosts with a directory defined for the 'Music' storage group
5902  QString sql = "SELECT DISTINCT hostname "
5903  "FROM storagegroup "
5904  "WHERE groupname = 'Music'";
5905  if (!query.exec(sql) || !query.isActive())
5906  MythDB::DBError("MainServer::HandleScanMusic get host list", query);
5907  else
5908  {
5909  while(query.next())
5910  {
5911  QString hostname = query.value(0).toString();
5912 
5913  if (hostname == gCoreContext->GetHostName())
5914  {
5915  // this is the master BE with a music storage group directory defined so run the file scanner
5916  LOG(VB_GENERAL, LOG_INFO, LOC +
5917  QString("HandleScanMusic: running filescanner on master BE '%1'").arg(hostname));
5918  QScopedPointer<MythSystem> cmd(MythSystem::Create(GetAppBinDir() + "mythutil --scanmusic",
5922  }
5923  else
5924  {
5925  // found a slave BE so ask it to run the file scanner
5927  if (slave)
5928  {
5929  LOG(VB_GENERAL, LOG_INFO, LOC +
5930  QString("HandleScanMusic: asking slave '%1' to run file scanner").arg(hostname));
5931  slave->ForwardRequest(slist);
5932  slave->DecrRef();
5933  }
5934  else
5935  {
5936  LOG(VB_GENERAL, LOG_INFO, LOC +
5937  QString("HandleScanMusic: Failed to grab slave socket on '%1'").arg(hostname));
5938  }
5939  }
5940  }
5941  }
5942  }
5943  else
5944  {
5945  // must be a slave with a music storage group directory defined so run the file scanner
5946  LOG(VB_GENERAL, LOG_INFO, LOC +
5947  QString("HandleScanMusic: running filescanner on slave BE '%1'")
5949  QScopedPointer<MythSystem> cmd(MythSystem::Create(GetAppBinDir() + "mythutil --scanmusic",
5953  }
5954 
5955  strlist << "OK";
5956 
5957  if (pbssock)
5958  SendResponse(pbssock, strlist);
5959 }
5960 
5962 {
5963 // format: MUSIC_TAG_UPDATE_VOLATILE <hostname> <songid> <rating> <playcount> <lastplayed>
5964 
5965  QStringList strlist;
5966 
5967  MythSocket *pbssock = pbs->getSocket();
5968 
5969  QString hostname = slist[1];
5970 
5972  {
5973  // forward the request to the slave BE
5975  if (slave)
5976  {
5977  LOG(VB_GENERAL, LOG_INFO, LOC +
5978  QString("HandleMusicTagUpdateVolatile: asking slave '%1' to update the metadata").arg(hostname));
5979  strlist = slave->ForwardRequest(slist);
5980  slave->DecrRef();
5981 
5982  if (pbssock)
5983  SendResponse(pbssock, strlist);
5984 
5985  return;
5986  }
5987 
5988  LOG(VB_GENERAL, LOG_INFO, LOC +
5989  QString("HandleMusicTagUpdateVolatile: Failed to grab slave socket on '%1'").arg(hostname));
5990 
5991  strlist << "ERROR: slave not found";
5992 
5993  if (pbssock)
5994  SendResponse(pbssock, strlist);
5995 
5996  return;
5997  }
5998 
5999  // run mythutil to update the metadata
6000  QStringList paramList;
6001  paramList.append(QString("--songid='%1'").arg(slist[2]));
6002  paramList.append(QString("--rating='%1'").arg(slist[3]));
6003  paramList.append(QString("--playcount='%1'").arg(slist[4]));
6004  paramList.append(QString("--lastplayed='%1'").arg(slist[5]));
6005 
6006  QString command = GetAppBinDir() + "mythutil --updatemeta " + paramList.join(" ");
6007 
6008  LOG(VB_GENERAL, LOG_INFO, LOC +
6009  QString("HandleMusicTagUpdateVolatile: running %1'").arg(command));
6010  QScopedPointer<MythSystem> cmd(MythSystem::Create(command,
6014 
6015  strlist << "OK";
6016 
6017  if (pbssock)
6018  SendResponse(pbssock, strlist);
6019 }
6020 
6021 void MainServer::HandleMusicCalcTrackLen(const QStringList &slist, PlaybackSock *pbs)
6022 {
6023 // format: MUSIC_CALC_TRACK_LENGTH <hostname> <songid>
6024 
6025  QStringList strlist;
6026 
6027  MythSocket *pbssock = pbs->getSocket();
6028 
6029  QString hostname = slist[1];
6030 
6032  {
6033  // forward the request to the slave BE
6035  if (slave)
6036  {
6037  LOG(VB_GENERAL, LOG_INFO, LOC +
6038  QString("HandleMusicCalcTrackLen: asking slave '%1' to update the track length").arg(hostname));
6039  strlist = slave->ForwardRequest(slist);
6040  slave->DecrRef();
6041 
6042  if (pbssock)
6043  SendResponse(pbssock, strlist);
6044 
6045  return;
6046  }
6047 
6048  LOG(VB_GENERAL, LOG_INFO, LOC +
6049  QString("HandleMusicCalcTrackLen: Failed to grab slave socket on '%1'").arg(hostname));
6050 
6051  strlist << "ERROR: slave not found";
6052 
6053  if (pbssock)
6054  SendResponse(pbssock, strlist);
6055 
6056  return;
6057  }
6058 
6059  // run mythutil to calc the tracks length
6060  QStringList paramList;
6061  paramList.append(QString("--songid='%1'").arg(slist[2]));
6062 
6063  QString command = GetAppBinDir() + "mythutil --calctracklen " + paramList.join(" ");
6064 
6065  LOG(VB_GENERAL, LOG