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