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