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