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 <QRegExp>
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).arg(linktext) + ENO);
122  }
123  }
124  }
125  if ((!checkexists || checkFile.exists()) &&
126  !(success2 = checkFile.remove()))
127  {
128  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error deleting '%1': %2")
129  .arg(filename).arg(strerror(errno)));
130  }
131  return success1 && success2;
132 }
133 
134 };
135 
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 
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  extra.push_back(
1169  QString::number(qChecksum(data.constData(), data.size())));
1170  extra.push_back(QString(data.toBase64()));
1171 
1172  for (uint i = 4 ; i < (uint) me->ExtraDataCount(); i++)
1173  {
1174  const QString& token = me->ExtraData(i);
1175  extra.push_back(token);
1176  RequestedBy::iterator it = m_previewRequestedBy.find(token);
1177  if (it != m_previewRequestedBy.end())
1178  {
1179  receivers.insert(*it);
1180  m_previewRequestedBy.erase(it);
1181  }
1182  }
1183 
1184  if (receivers.empty())
1185  {
1186  LOG(VB_GENERAL, LOG_ERR, LOC +
1187  "PREVIEW_SUCCESS but no receivers.");
1188  return;
1189  }
1190 
1191  broadcast.push_back("BACKEND_MESSAGE");
1192  broadcast.push_back("GENERATED_PIXMAP");
1193  broadcast += extra;
1194  }
1195  else
1196  {
1197  message = "PREVIEW_FAILED";
1198  error = QString("Failed to read '%1'").arg(filename);
1199  LOG(VB_GENERAL, LOG_ERR, LOC + error);
1200  }
1201  }
1202 
1203  if (message == "PREVIEW_FAILED" && me->ExtraDataCount() >= 5)
1204  {
1205  const QString& pginfokey = me->ExtraData(0); // pginfo->MakeUniqueKey()
1206  const QString& msg = me->ExtraData(2);
1207 
1208  QStringList extra("ERROR");
1209  extra.push_back(pginfokey);
1210  extra.push_back(msg);
1211  for (uint i = 4 ; i < (uint) me->ExtraDataCount(); i++)
1212  {
1213  const QString& token = me->ExtraData(i);
1214  extra.push_back(token);
1215  RequestedBy::iterator it = m_previewRequestedBy.find(token);
1216  if (it != m_previewRequestedBy.end())
1217  {
1218  receivers.insert(*it);
1219  m_previewRequestedBy.erase(it);
1220  }
1221  }
1222 
1223  if (receivers.empty())
1224  {
1225  LOG(VB_GENERAL, LOG_ERR, LOC +
1226  "PREVIEW_FAILED but no receivers.");
1227  return;
1228  }
1229 
1230  broadcast.push_back("BACKEND_MESSAGE");
1231  broadcast.push_back("GENERATED_PIXMAP");
1232  broadcast += extra;
1233  }
1234 
1235  if (me->Message().startsWith("AUTO_EXPIRE"))
1236  {
1237 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1238  QStringList tokens = me->Message()
1239  .split(" ", QString::SkipEmptyParts);
1240 #else
1241  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1242 #endif
1243 
1244  if (tokens.size() != 3)
1245  {
1246  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad AUTO_EXPIRE message");
1247  return;
1248  }
1249 
1250  QDateTime startts = MythDate::fromString(tokens[2]);
1251  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1252 
1253  if (recInfo.GetChanID())
1254  {
1255  SendMythSystemPlayEvent("REC_EXPIRED", &recInfo);
1256 
1257  // allow re-record if auto expired but not expired live
1258  // or already "deleted" programs
1259  if (recInfo.GetRecordingGroup() != "LiveTV" &&
1260  recInfo.GetRecordingGroup() != "Deleted" &&
1261  (gCoreContext->GetBoolSetting("RerecordWatched", false) ||
1262  !recInfo.IsWatched()))
1263  {
1264  recInfo.ForgetHistory();
1265  }
1266  DoHandleDeleteRecording(recInfo, nullptr, false, true, false);
1267  }
1268  else
1269  {
1270  QString msg = QString("Cannot find program info for '%1', "
1271  "while attempting to Auto-Expire.")
1272  .arg(me->Message());
1273  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
1274  }
1275 
1276  return;
1277  }
1278 
1279  if (me->Message().startsWith("QUERY_NEXT_LIVETV_DIR") && m_sched)
1280  {
1281 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1282  QStringList tokens = me->Message()
1283  .split(" ", QString::SkipEmptyParts);
1284 #else
1285  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1286 #endif
1287 
1288  if (tokens.size() != 2)
1289  {
1290  LOG(VB_GENERAL, LOG_ERR, LOC +
1291  QString("Bad %1 message").arg(tokens[0]));
1292  return;
1293  }
1294 
1295  m_sched->GetNextLiveTVDir(tokens[1].toInt());
1296  return;
1297  }
1298 
1299  if (me->Message().startsWith("STOP_RECORDING"))
1300  {
1301 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1302  QStringList tokens = me->Message().split(" ",
1303  QString::SkipEmptyParts);
1304 #else
1305  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1306 #endif
1307 
1308 
1309  if (tokens.size() < 3 || tokens.size() > 3)
1310  {
1311  LOG(VB_GENERAL, LOG_ERR, LOC +
1312  QString("Bad STOP_RECORDING message: %1")
1313  .arg(me->Message()));
1314  return;
1315  }
1316 
1317  QDateTime startts = MythDate::fromString(tokens[2]);
1318  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1319 
1320  if (recInfo.GetChanID())
1321  {
1322  DoHandleStopRecording(recInfo, nullptr);
1323  }
1324  else
1325  {
1326  LOG(VB_GENERAL, LOG_ERR, LOC +
1327  QString("Cannot find program info for '%1' while "
1328  "attempting to stop recording.").arg(me->Message()));
1329  }
1330 
1331  return;
1332  }
1333 
1334  if ((me->Message().startsWith("DELETE_RECORDING")) ||
1335  (me->Message().startsWith("FORCE_DELETE_RECORDING")))
1336  {
1337 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1338  QStringList tokens = me->Message()
1339  .split(" ", QString::SkipEmptyParts);
1340 #else
1341  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1342 #endif
1343 
1344 
1345  if (tokens.size() < 3 || tokens.size() > 5)
1346  {
1347  LOG(VB_GENERAL, LOG_ERR, LOC +
1348  QString("Bad %1 message").arg(tokens[0]));
1349  return;
1350  }
1351 
1352  bool force = (tokens.size() >= 4) && (tokens[3] == "FORCE");
1353  bool forget = (tokens.size() >= 5) && (tokens[4] == "FORGET");
1354 
1355  QDateTime startts = MythDate::fromString(tokens[2]);
1356  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1357 
1358  if (recInfo.GetChanID())
1359  {
1360  if (tokens[0] == "FORCE_DELETE_RECORDING")
1361  DoHandleDeleteRecording(recInfo, nullptr, true, false, forget);
1362  else
1363  DoHandleDeleteRecording(recInfo, nullptr, force, false, forget);
1364  }
1365  else
1366  {
1367  LOG(VB_GENERAL, LOG_ERR, LOC +
1368  QString("Cannot find program info for '%1' while "
1369  "attempting to delete.").arg(me->Message()));
1370  }
1371 
1372  return;
1373  }
1374 
1375  if (me->Message().startsWith("UNDELETE_RECORDING"))
1376  {
1377 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1378  QStringList tokens = me->Message().split(" ",
1379  QString::SkipEmptyParts);
1380 #else
1381  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1382 #endif
1383 
1384 
1385  if (tokens.size() < 3 || tokens.size() > 3)
1386  {
1387  LOG(VB_GENERAL, LOG_ERR, LOC +
1388  QString("Bad UNDELETE_RECORDING message: %1")
1389  .arg(me->Message()));
1390  return;
1391  }
1392 
1393  QDateTime startts = MythDate::fromString(tokens[2]);
1394  RecordingInfo recInfo(tokens[1].toUInt(), startts);
1395 
1396  if (recInfo.GetChanID())
1397  {
1398  DoHandleUndeleteRecording(recInfo, nullptr);
1399  }
1400  else
1401  {
1402  LOG(VB_GENERAL, LOG_ERR, LOC +
1403  QString("Cannot find program info for '%1' while "
1404  "attempting to undelete.").arg(me->Message()));
1405  }
1406 
1407  return;
1408  }
1409 
1410  if (me->Message().startsWith("ADD_CHILD_INPUT"))
1411  {
1412 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1413  QStringList tokens = me->Message()
1414  .split(" ", QString::SkipEmptyParts);
1415 #else
1416  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1417 #endif
1418  if (!m_ismaster)
1419  {
1420  LOG(VB_GENERAL, LOG_ERR, LOC +
1421  "ADD_CHILD_INPUT event received in slave context");
1422  }
1423  else if (tokens.size() != 2)
1424  {
1425  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad ADD_CHILD_INPUT message");
1426  }
1427  else
1428  {
1429  HandleAddChildInput(tokens[1].toUInt());
1430  }
1431  return;
1432  }
1433 
1434  if (me->Message().startsWith("RESCHEDULE_RECORDINGS") && m_sched)
1435  {
1436  const QStringList& request = me->ExtraDataList();
1437  m_sched->Reschedule(request);
1438  return;
1439  }
1440 
1441  if (me->Message().startsWith("SCHEDULER_ADD_RECORDING") && m_sched)
1442  {
1443  ProgramInfo pi(me->ExtraDataList());
1444  if (!pi.GetChanID())
1445  {
1446  LOG(VB_GENERAL, LOG_ERR, LOC +
1447  "Bad SCHEDULER_ADD_RECORDING message");
1448  return;
1449  }
1450 
1451  m_sched->AddRecording(pi);
1452  return;
1453  }
1454 
1455  if (me->Message().startsWith("UPDATE_RECORDING_STATUS") && m_sched)
1456  {
1457 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1458  QStringList tokens = me->Message()
1459  .split(" ", QString::SkipEmptyParts);
1460 #else
1461  QStringList tokens = me->Message().split(" ", Qt::SkipEmptyParts);
1462 #endif
1463 
1464  if (tokens.size() != 6)
1465  {
1466  LOG(VB_GENERAL, LOG_ERR, LOC +
1467  "Bad UPDATE_RECORDING_STATUS message");
1468  return;
1469  }
1470 
1471  uint cardid = tokens[1].toUInt();
1472  uint chanid = tokens[2].toUInt();
1473  QDateTime startts = MythDate::fromString(tokens[3]);
1474  auto recstatus = RecStatus::Type(tokens[4].toInt());
1475  QDateTime recendts = MythDate::fromString(tokens[5]);
1476  m_sched->UpdateRecStatus(cardid, chanid, startts,
1477  recstatus, recendts);
1478 
1480  return;
1481  }
1482 
1483  if (me->Message().startsWith("LIVETV_EXITED"))
1484  {
1485  const QString& chainid = me->ExtraData();
1486  LiveTVChain *chain = GetExistingChain(chainid);
1487  if (chain)
1488  DeleteChain(chain);
1489 
1490  return;
1491  }
1492 
1493  if (me->Message() == "CLEAR_SETTINGS_CACHE")
1495 
1496  if (me->Message().startsWith("RESET_IDLETIME") && m_sched)
1498 
1499  if (me->Message() == "LOCAL_RECONNECT_TO_MASTER")
1501 
1502  if (me->Message() == "LOCAL_SLAVE_BACKEND_ENCODERS_OFFLINE")
1504 
1505  if (me->Message().startsWith("LOCAL_"))
1506  return;
1507 
1508  if (me->Message() == "CREATE_THUMBNAILS")
1509  ImageManagerBe::getInstance()->HandleCreateThumbnails(me->ExtraDataList());
1510 
1511  if (me->Message() == "IMAGE_GET_METADATA")
1512  ImageManagerBe::getInstance()->HandleGetMetadata(me->ExtraData());
1513 
1514  MythEvent mod_me("");
1515  if (me->Message().startsWith("MASTER_UPDATE_REC_INFO"))
1516  {
1517  QStringList tokens = me->Message().simplified().split(" ");
1518  uint recordedid = 0;
1519  if (tokens.size() >= 2)
1520  recordedid = tokens[1].toUInt();
1521 
1522  ProgramInfo evinfo(recordedid);
1523  if (evinfo.GetChanID())
1524  {
1525  QDateTime rectime = MythDate::current().addSecs(
1526  -gCoreContext->GetNumSetting("RecordOverTime"));
1527 
1528  if (m_sched && evinfo.GetRecordingEndTime() > rectime)
1529  evinfo.SetRecordingStatus(m_sched->GetRecStatus(evinfo));
1530 
1531  QStringList list;
1532  evinfo.ToStringList(list);
1533  mod_me = MythEvent("RECORDING_LIST_CHANGE UPDATE", list);
1534  me = &mod_me;
1535  }
1536  else
1537  {
1538  return;
1539  }
1540  }
1541 
1542  if (me->Message().startsWith("DOWNLOAD_FILE"))
1543  {
1544  QStringList extraDataList = me->ExtraDataList();
1545  QString localFile = extraDataList[1];
1546  QFile file(localFile);
1547  QStringList tokens = me->Message().simplified().split(" ");
1548  QMutexLocker locker(&m_downloadURLsLock);
1549 
1550  if (!m_downloadURLs.contains(localFile))
1551  return;
1552 
1553  extraDataList[1] = m_downloadURLs[localFile];
1554 
1555  if ((tokens.size() >= 2) && (tokens[1] == "FINISHED"))
1556  m_downloadURLs.remove(localFile);
1557 
1558  mod_me = MythEvent(me->Message(), extraDataList);
1559  me = &mod_me;
1560  }
1561 
1562  if (broadcast.empty())
1563  {
1564  broadcast.push_back("BACKEND_MESSAGE");
1565  broadcast.push_back(me->Message());
1566  broadcast += me->ExtraDataList();
1567  }
1568  }
1569 
1570  if (!broadcast.empty())
1571  {
1572  // Make a local copy of the list, upping the refcount as we go..
1573  vector<PlaybackSock *> localPBSList;
1574  m_sockListLock.lockForRead();
1575  for (auto & pbs : m_playbackList)
1576  {
1577  pbs->IncrRef();
1578  localPBSList.push_back(pbs);
1579  }
1580  m_sockListLock.unlock();
1581 
1582  bool sendGlobal = false;
1583  if (m_ismaster && broadcast[1].startsWith("GLOBAL_"))
1584  {
1585  broadcast[1].replace("GLOBAL_", "LOCAL_");
1586  MythEvent me(broadcast[1], broadcast[2]);
1587  gCoreContext->dispatch(me);
1588 
1589  sendGlobal = true;
1590  }
1591 
1592  QSet<PlaybackSock*> sentSet;
1593 
1594  bool isSystemEvent = broadcast[1].startsWith("SYSTEM_EVENT ");
1595  QStringList sentSetSystemEvent(gCoreContext->GetHostName());
1596 
1597  vector<PlaybackSock*>::const_iterator iter;
1598  for (iter = localPBSList.begin(); iter != localPBSList.end(); ++iter)
1599  {
1600  PlaybackSock *pbs = *iter;
1601 
1602  if (sentSet.contains(pbs) || pbs->IsDisconnected())
1603  continue;
1604 
1605  if (!receivers.empty() && !receivers.contains(pbs->getHostname()))
1606  continue;
1607 
1608  sentSet.insert(pbs);
1609 
1610  bool reallysendit = false;
1611 
1612  if (broadcast[1] == "CLEAR_SETTINGS_CACHE")
1613  {
1614  if ((m_ismaster) &&
1615  (pbs->isSlaveBackend() || pbs->wantsEvents()))
1616  reallysendit = true;
1617  }
1618  else if (sendGlobal)
1619  {
1620  if (pbs->isSlaveBackend())
1621  reallysendit = true;
1622  }
1623  else if (pbs->wantsEvents())
1624  {
1625  reallysendit = true;
1626  }
1627 
1628  if (reallysendit)
1629  {
1630  if (isSystemEvent)
1631  {
1632  if (!pbs->wantsSystemEvents())
1633  {
1634  continue;
1635  }
1636  if (!pbs->wantsOnlySystemEvents())
1637  {
1638  if (sentSetSystemEvent.contains(pbs->getHostname()))
1639  continue;
1640 
1641  sentSetSystemEvent << pbs->getHostname();
1642  }
1643  }
1644  else if (pbs->wantsOnlySystemEvents())
1645  continue;
1646  }
1647 
1648  MythSocket *sock = pbs->getSocket();
1649  if (reallysendit && sock->IsConnected())
1650  sock->WriteStringList(broadcast);
1651  }
1652 
1653  // Done with the pbs list, so decrement all the instances..
1654  for (iter = localPBSList.begin(); iter != localPBSList.end(); ++iter)
1655  {
1656  PlaybackSock *pbs = *iter;
1657  pbs->DecrRef();
1658  }
1659  }
1660 }
1661 
1670 void MainServer::HandleVersion(MythSocket *socket, const QStringList &slist)
1671 {
1672  QStringList retlist;
1673  QString version = slist[1];
1674  if (version != MYTH_PROTO_VERSION)
1675  {
1676  LOG(VB_GENERAL, LOG_CRIT, LOC +
1677  "MainServer::HandleVersion - Client speaks protocol version " +
1678  version + " but we speak " + MYTH_PROTO_VERSION + '!');
1679  retlist << "REJECT" << MYTH_PROTO_VERSION;
1680  socket->WriteStringList(retlist);
1681  HandleDone(socket);
1682  return;
1683  }
1684 
1685  if (slist.size() < 3)
1686  {
1687  LOG(VB_GENERAL, LOG_CRIT, LOC +
1688  "MainServer::HandleVersion - Client did not pass protocol "
1689  "token. Refusing connection!");
1690  retlist << "REJECT" << MYTH_PROTO_VERSION;
1691  socket->WriteStringList(retlist);
1692  HandleDone(socket);
1693  return;
1694  }
1695 
1696  QString token = slist[2];
1697  if (token != QString::fromUtf8(MYTH_PROTO_TOKEN))
1698  {
1699  LOG(VB_GENERAL, LOG_CRIT, LOC +
1700  QString("MainServer::HandleVersion - Client sent incorrect "
1701  "protocol token \"%1\" for protocol version. Refusing "
1702  "connection!").arg(token));
1703  retlist << "REJECT" << MYTH_PROTO_VERSION;
1704  socket->WriteStringList(retlist);
1705  HandleDone(socket);
1706  return;
1707  }
1708 
1709  retlist << "ACCEPT" << MYTH_PROTO_VERSION;
1710  socket->WriteStringList(retlist);
1711 }
1712 
1735 void MainServer::HandleAnnounce(QStringList &slist, QStringList commands,
1736  MythSocket *socket)
1737 {
1738  QStringList retlist( "OK" );
1739  QStringList errlist( "ERROR" );
1740 
1741  if (commands.size() < 3 || commands.size() > 6)
1742  {
1743  QString info = "";
1744  if (commands.size() == 2)
1745  info = QString(" %1").arg(commands[1]);
1746 
1747  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Received malformed ANN%1 query")
1748  .arg(info));
1749 
1750  errlist << "malformed_ann_query";
1751  socket->WriteStringList(errlist);
1752  return;
1753  }
1754 
1755  m_sockListLock.lockForRead();
1756  for (auto *pbs : m_playbackList)
1757  {
1758  if (pbs->getSocket() == socket)
1759  {
1760  LOG(VB_GENERAL, LOG_WARNING, LOC +
1761  QString("Client %1 is trying to announce a socket "
1762  "multiple times.")
1763  .arg(commands[2]));
1764  socket->WriteStringList(retlist);
1765  m_sockListLock.unlock();
1766  return;
1767  }
1768  }
1769  m_sockListLock.unlock();
1770 
1771  if (commands[1] == "Playback" || commands[1] == "Monitor" ||
1772  commands[1] == "Frontend")
1773  {
1774  if (commands.size() < 4)
1775  {
1776  LOG(VB_GENERAL, LOG_ERR, LOC +
1777  QString("Received malformed ANN %1 query")
1778  .arg(commands[1]));
1779 
1780  errlist << "malformed_ann_query";
1781  socket->WriteStringList(errlist);
1782  return;
1783  }
1784 
1785  // Monitor connections are same as Playback but they don't
1786  // block shutdowns. See the Scheduler event loop for more.
1787 
1788  auto eventsMode = (PlaybackSockEventsMode)commands[3].toInt();
1789 
1790  QWriteLocker lock(&m_sockListLock);
1791  if (!m_controlSocketList.remove(socket))
1792  return; // socket was disconnected
1793  auto *pbs = new PlaybackSock(this, socket, commands[2], eventsMode);
1794  m_playbackList.push_back(pbs);
1795  lock.unlock();
1796 
1797  LOG(VB_GENERAL, LOG_INFO, LOC + QString("MainServer::ANN %1")
1798  .arg(commands[1]));
1799  LOG(VB_GENERAL, LOG_INFO, LOC +
1800  QString("adding: %1(%2) as a client (events: %3)")
1801  .arg(commands[2])
1802  .arg(quintptr(socket),0,16)
1803  .arg(eventsMode));
1804  pbs->setBlockShutdown((commands[1] == "Playback") ||
1805  (commands[1] == "Frontend"));
1806 
1807  if (commands[1] == "Frontend")
1808  {
1809  pbs->SetAsFrontend();
1810  auto *frontend = new Frontend();
1811  frontend->m_name = commands[2];
1812  // On a combined mbe/fe the frontend will connect using the localhost
1813  // address, we need the external IP which happily will be the same as
1814  // the backend's external IP
1815  if (frontend->m_name == gCoreContext->GetMasterHostName())
1816  frontend->m_ip = QHostAddress(gCoreContext->GetBackendServerIP());
1817  else
1818  frontend->m_ip = socket->GetPeerAddress();
1819  if (gBackendContext)
1821  else
1822  delete frontend;
1823  }
1824 
1825  }
1826  else if (commands[1] == "MediaServer")
1827  {
1828  if (commands.size() < 3)
1829  {
1830  LOG(VB_GENERAL, LOG_ERR, LOC +
1831  "Received malformed ANN MediaServer query");
1832  errlist << "malformed_ann_query";
1833  socket->WriteStringList(errlist);
1834  return;
1835  }
1836 
1837  QWriteLocker lock(&m_sockListLock);
1838  if (!m_controlSocketList.remove(socket))
1839  return; // socket was disconnected
1840  auto *pbs = new PlaybackSock(this, socket, commands[2],
1842  pbs->setAsMediaServer();
1843  pbs->setBlockShutdown(false);
1844  m_playbackList.push_back(pbs);
1845  lock.unlock();
1846 
1848  QString("CLIENT_CONNECTED HOSTNAME %1").arg(commands[2]));
1849  }
1850  else if (commands[1] == "SlaveBackend")
1851  {
1852  if (commands.size() < 4)
1853  {
1854  LOG(VB_GENERAL, LOG_ERR, LOC +
1855  QString("Received malformed ANN %1 query")
1856  .arg(commands[1]));
1857  errlist << "malformed_ann_query";
1858  socket->WriteStringList(errlist);
1859  return;
1860  }
1861 
1862  QWriteLocker lock(&m_sockListLock);
1863  if (!m_controlSocketList.remove(socket))
1864  return; // socket was disconnected
1865  auto *pbs = new PlaybackSock(this, socket, commands[2],
1866  kPBSEvents_None);
1867  m_playbackList.push_back(pbs);
1868  lock.unlock();
1869 
1870  LOG(VB_GENERAL, LOG_INFO, LOC +
1871  QString("adding: %1 as a slave backend server")
1872  .arg(commands[2]));
1873  pbs->setAsSlaveBackend();
1874  pbs->setIP(commands[3]);
1875 
1876  if (m_sched)
1877  {
1878  RecordingList slavelist;
1879  QStringList::const_iterator sit = slist.cbegin()+1;
1880  while (sit != slist.cend())
1881  {
1882  auto *recinfo = new RecordingInfo(sit, slist.cend());
1883  if (!recinfo->GetChanID())
1884  {
1885  delete recinfo;
1886  break;
1887  }
1888  slavelist.push_back(recinfo);
1889  }
1890  m_sched->SlaveConnected(slavelist);
1891  }
1892 
1893  bool wasAsleep = true;
1894  TVRec::s_inputsLock.lockForRead();
1895  for (auto * elink : qAsConst(*m_encoderList))
1896  {
1897  if (elink->GetHostName() == commands[2])
1898  {
1899  if (! (elink->IsWaking() || elink->IsAsleep()))
1900  wasAsleep = false;
1901  elink->SetSocket(pbs);
1902  }
1903  }
1904  TVRec::s_inputsLock.unlock();
1905 
1906  if (!wasAsleep && m_sched)
1907  m_sched->ReschedulePlace("SlaveConnected");
1908 
1909  QString message = QString("LOCAL_SLAVE_BACKEND_ONLINE %2")
1910  .arg(commands[2]);
1911  MythEvent me(message);
1912  gCoreContext->dispatch(me);
1913 
1914  pbs->setBlockShutdown(false);
1915 
1916  m_autoexpireUpdateTimer->start(1s);
1917 
1919  QString("SLAVE_CONNECTED HOSTNAME %1").arg(commands[2]));
1920  }
1921  else if (commands[1] == "FileTransfer")
1922  {
1923  if (slist.size() < 3)
1924  {
1925  LOG(VB_GENERAL, LOG_ERR, LOC +
1926  "Received malformed FileTransfer command");
1927  errlist << "malformed_filetransfer_command";
1928  socket->WriteStringList(errlist);
1929  return;
1930  }
1931 
1932  LOG(VB_NETWORK, LOG_INFO, LOC +
1933  "MainServer::HandleAnnounce FileTransfer");
1934  LOG(VB_NETWORK, LOG_INFO, LOC +
1935  QString("adding: %1 as a remote file transfer") .arg(commands[2]));
1936  QStringList::const_iterator it = slist.cbegin();
1937  QString path = *(++it);
1938  QString wantgroup = *(++it);
1939  QString filename;
1940  QStringList checkfiles;
1941 
1942  for (++it; it != slist.cend(); ++it)
1943  checkfiles += *it;
1944 
1945  FileTransfer *ft = nullptr;
1946  bool writemode = false;
1947  bool usereadahead = true;
1948  std::chrono::milliseconds timeout_ms = 2s;
1949  if (commands.size() > 3)
1950  writemode = (commands[3].toInt() != 0);
1951 
1952  if (commands.size() > 4)
1953  usereadahead = (commands[4].toInt() != 0);
1954 
1955  if (commands.size() > 5)
1956  timeout_ms = std::chrono::milliseconds(commands[5].toInt());
1957 
1958  if (writemode)
1959  {
1960  if (wantgroup.isEmpty())
1961  wantgroup = "Default";
1962 
1963  StorageGroup sgroup(wantgroup, gCoreContext->GetHostName(), false);
1964  QString dir = sgroup.FindNextDirMostFree();
1965  if (dir.isEmpty())
1966  {
1967  LOG(VB_GENERAL, LOG_ERR, LOC + "Unable to determine directory "
1968  "to write to in FileTransfer write command");
1969  errlist << "filetransfer_directory_not_found";
1970  socket->WriteStringList(errlist);
1971  return;
1972  }
1973 
1974  if (path.isEmpty())
1975  {
1976  LOG(VB_GENERAL, LOG_ERR, LOC +
1977  QString("FileTransfer write filename is empty in path '%1'.")
1978  .arg(path));
1979  errlist << "filetransfer_filename_empty";
1980  socket->WriteStringList(errlist);
1981  return;
1982  }
1983 
1984  if ((path.contains("/../")) ||
1985  (path.startsWith("../")))
1986  {
1987  LOG(VB_GENERAL, LOG_ERR, LOC +
1988  QString("FileTransfer write filename '%1' does not pass "
1989  "sanity checks.") .arg(path));
1990  errlist << "filetransfer_filename_dangerous";
1991  socket->WriteStringList(errlist);
1992  return;
1993  }
1994 
1995  filename = dir + "/" + path;
1996  }
1997  else
1998  filename = LocalFilePath(path, wantgroup);
1999 
2000  if (filename.isEmpty())
2001  {
2002  LOG(VB_GENERAL, LOG_ERR, LOC + "Empty filename, cowardly aborting!");
2003  errlist << "filetransfer_filename_empty";
2004  socket->WriteStringList(errlist);
2005  return;
2006  }
2007 
2008 
2009  QFileInfo finfo(filename);
2010  if (finfo.isDir())
2011  {
2012  LOG(VB_GENERAL, LOG_ERR, LOC +
2013  QString("FileTransfer filename '%1' is actually a directory, "
2014  "cannot transfer.") .arg(filename));
2015  errlist << "filetransfer_filename_is_a_directory";
2016  socket->WriteStringList(errlist);
2017  return;
2018  }
2019 
2020  if (writemode)
2021  {
2022  QString dirPath = finfo.absolutePath();
2023  QDir qdir(dirPath);
2024  if (!qdir.exists())
2025  {
2026  if (!qdir.mkpath(dirPath))
2027  {
2028  LOG(VB_GENERAL, LOG_ERR, LOC +
2029  QString("FileTransfer filename '%1' is in a "
2030  "subdirectory which does not exist, and can "
2031  "not be created.") .arg(filename));
2032  errlist << "filetransfer_unable_to_create_subdirectory";
2033  socket->WriteStringList(errlist);
2034  return;
2035  }
2036  }
2037  QWriteLocker lock(&m_sockListLock);
2038  if (!m_controlSocketList.remove(socket))
2039  return; // socket was disconnected
2040  ft = new FileTransfer(filename, socket, writemode);
2041  }
2042  else
2043  {
2044  QWriteLocker lock(&m_sockListLock);
2045  if (!m_controlSocketList.remove(socket))
2046  return; // socket was disconnected
2047  ft = new FileTransfer(filename, socket, usereadahead, timeout_ms);
2048  }
2049 
2050  if (!ft->isOpen())
2051  {
2052  LOG(VB_GENERAL, LOG_ERR, LOC +
2053  QString("Can't open %1").arg(filename));
2054  errlist << "filetransfer_unable_to_open_file";
2055  socket->WriteStringList(errlist);
2056  socket->IncrRef(); // FileTransfer took ownership of the socket, take it back
2057  ft->DecrRef();
2058  return;
2059  }
2060  ft->IncrRef();
2061  LOG(VB_GENERAL, LOG_INFO, LOC +
2062  QString("adding: %1(%2) as a file transfer")
2063  .arg(commands[2])
2064  .arg(quintptr(socket),0,16));
2065  m_sockListLock.lockForWrite();
2066  m_fileTransferList.push_back(ft);
2067  m_sockListLock.unlock();
2068 
2069  retlist << QString::number(socket->GetSocketDescriptor());
2070  retlist << QString::number(ft->GetFileSize());
2071 
2072  ft->DecrRef();
2073 
2074  if (!checkfiles.empty())
2075  {
2076  QFileInfo fi(filename);
2077  QDir dir = fi.absoluteDir();
2078  for (const auto & file : qAsConst(checkfiles))
2079  {
2080  if (dir.exists(file) &&
2081  ((file).endsWith(".srt") ||
2082  QFileInfo(dir, file).size() >= kReadTestSize))
2083  {
2084  retlist<<file;
2085  }
2086  }
2087  }
2088  }
2089 
2090  socket->WriteStringList(retlist);
2092 }
2093 
2100 {
2101  socket->DisconnectFromHost();
2103 }
2104 
2106 {
2107  SendErrorResponse(pbs->getSocket(), error);
2108 }
2109 
2111 {
2112  LOG(VB_GENERAL, LOG_ERR, LOC + error);
2113 
2114  QStringList strList("ERROR");
2115  strList << error;
2116 
2117  SendResponse(sock, strList);
2118 }
2119 
2120 void MainServer::SendResponse(MythSocket *socket, QStringList &commands)
2121 {
2122  // Note: this method assumes that the playback or filetransfer
2123  // handler has already been uprefed and the socket as well.
2124 
2125  // These checks are really just to check if the socket has
2126  // been remotely disconnected while we were working on the
2127  // response.
2128 
2129  bool do_write = false;
2130  if (socket)
2131  {
2132  m_sockListLock.lockForRead();
2133  do_write = (GetPlaybackBySock(socket) ||
2134  GetFileTransferBySock(socket));
2135  m_sockListLock.unlock();
2136  }
2137 
2138  if (do_write)
2139  {
2140  socket->WriteStringList(commands);
2141  }
2142  else
2143  {
2144  LOG(VB_GENERAL, LOG_ERR, LOC +
2145  "SendResponse: Unable to write to client socket, as it's no "
2146  "longer there");
2147  }
2148 }
2149 
2159 {
2160  MythSocket *pbssock = pbs->getSocket();
2161  QString playbackhost = pbs->getHostname();
2162 
2163  QMap<QString,ProgramInfo*> recMap;
2164  if (m_sched)
2165  recMap = m_sched->GetRecording();
2166 
2167  QMap<QString,uint32_t> inUseMap = ProgramInfo::QueryInUseMap();
2168  QMap<QString,bool> isJobRunning =
2170 
2171  int sort = 0;
2172  // Allow "Play" and "Delete" for backwards compatibility with protocol
2173  // version 56 and below.
2174  if ((type == "Ascending") || (type == "Play"))
2175  sort = 1;
2176  else if ((type == "Descending") || (type == "Delete"))
2177  sort = -1;
2178 
2179  ProgramList destination;
2181  destination, (type == "Recording"),
2182  inUseMap, isJobRunning, recMap, sort);
2183 
2184  QMap<QString,ProgramInfo*>::iterator mit = recMap.begin();
2185  for (; mit != recMap.end(); mit = recMap.erase(mit))
2186  delete *mit;
2187 
2188  QStringList outputlist(QString::number(destination.size()));
2189  QMap<QString, int> backendPortMap;
2190  int port = gCoreContext->GetBackendServerPort();
2191  QString host = gCoreContext->GetHostName();
2192 
2193  for (auto* proginfo : destination)
2194  {
2195  PlaybackSock *slave = nullptr;
2196 
2197  if (proginfo->GetHostname() != gCoreContext->GetHostName())
2198  slave = GetSlaveByHostname(proginfo->GetHostname());
2199 
2200  if ((proginfo->GetHostname() == gCoreContext->GetHostName()) ||
2201  (!slave && m_masterBackendOverride))
2202  {
2203  proginfo->SetPathname(MythCoreContext::GenMythURL(host,port,
2204  proginfo->GetBasename()));
2205  if (!proginfo->GetFilesize())
2206  {
2207  QString tmpURL = GetPlaybackURL(proginfo);
2208  if (tmpURL.startsWith('/'))
2209  {
2210  QFile checkFile(tmpURL);
2211  if (!tmpURL.isEmpty() && checkFile.exists())
2212  {
2213  proginfo->SetFilesize(checkFile.size());
2214  if (proginfo->GetRecordingEndTime() <
2216  {
2217  proginfo->SaveFilesize(proginfo->GetFilesize());
2218  }
2219  }
2220  }
2221  }
2222  }
2223  else if (!slave)
2224  {
2225  proginfo->SetPathname(GetPlaybackURL(proginfo));
2226  if (proginfo->GetPathname().isEmpty())
2227  {
2228  LOG(VB_GENERAL, LOG_ERR, LOC +
2229  QString("HandleQueryRecordings() "
2230  "Couldn't find backend for:\n\t\t\t%1")
2231  .arg(proginfo->toString(ProgramInfo::kTitleSubtitle)));
2232 
2233  proginfo->SetFilesize(0);
2234  proginfo->SetPathname("file not found");
2235  }
2236  }
2237  else
2238  {
2239  if (!proginfo->GetFilesize())
2240  {
2241  if (!slave->FillProgramInfo(*proginfo, playbackhost))
2242  {
2243  LOG(VB_GENERAL, LOG_ERR, LOC +
2244  "MainServer::HandleQueryRecordings()"
2245  "\n\t\t\tCould not fill program info "
2246  "from backend");
2247  }
2248  else
2249  {
2250  if (proginfo->GetRecordingEndTime() <
2252  {
2253  proginfo->SaveFilesize(proginfo->GetFilesize());
2254  }
2255  }
2256  }
2257  else
2258  {
2259  ProgramInfo *p = proginfo;
2260  QString hostname = p->GetHostname();
2261 
2262  if (!backendPortMap.contains(hostname))
2263  backendPortMap[hostname] = gCoreContext->GetBackendServerPort(hostname);
2264 
2265  p->SetPathname(MythCoreContext::GenMythURL(hostname,
2266  backendPortMap[hostname],
2267  p->GetBasename()));
2268  }
2269  }
2270 
2271  if (slave)
2272  slave->DecrRef();
2273 
2274  proginfo->ToStringList(outputlist);
2275  }
2276 
2277  SendResponse(pbssock, outputlist);
2278 }
2279 
2286 {
2287  if (slist.size() < 3)
2288  {
2289  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad QUERY_RECORDING query");
2290  return;
2291  }
2292 
2293  MythSocket *pbssock = pbs->getSocket();
2294  QString command = slist[1].toUpper();
2295  ProgramInfo *pginfo = nullptr;
2296 
2297  if (command == "BASENAME")
2298  {
2299  pginfo = new ProgramInfo(slist[2]);
2300  }
2301  else if (command == "TIMESLOT")
2302  {
2303  if (slist.size() < 4)
2304  {
2305  LOG(VB_GENERAL, LOG_ERR, LOC + "Bad QUERY_RECORDING query");
2306  return;
2307  }
2308 
2309  QDateTime recstartts = MythDate::fromString(slist[3]);
2310  pginfo = new ProgramInfo(slist[2].toUInt(), recstartts);
2311  }
2312 
2313  QStringList strlist;
2314 
2315  if (pginfo && pginfo->GetChanID())
2316  {
2317  strlist << "OK";
2318  pginfo->ToStringList(strlist);
2319  }
2320  else
2321  {
2322  strlist << "ERROR";
2323  }
2324 
2325  delete pginfo;
2326 
2327  SendResponse(pbssock, strlist);
2328 }
2329 
2331 {
2332  MythSocket *pbssock = pbs->getSocket();
2333 
2334  QString playbackhost = slist[1];
2335 
2336  QStringList::const_iterator it = slist.cbegin() + 2;
2337  ProgramInfo pginfo(it, slist.cend());
2338 
2339  if (pginfo.HasPathname())
2340  {
2341  QString lpath = GetPlaybackURL(&pginfo);
2342  int port = gCoreContext->GetBackendServerPort();
2343  QString host = gCoreContext->GetHostName();
2344 
2345  if (playbackhost == gCoreContext->GetHostName())
2346  pginfo.SetPathname(lpath);
2347  else
2348  pginfo.SetPathname(MythCoreContext::GenMythURL(host,port,
2349  pginfo.GetBasename()));
2350 
2351  const QFileInfo info(lpath);
2352  pginfo.SetFilesize(info.size());
2353  }
2354 
2355  QStringList strlist;
2356 
2357  pginfo.ToStringList(strlist);
2358 
2359  SendResponse(pbssock, strlist);
2360 }
2361 
2362 
2363 void DeleteThread::run(void)
2364 {
2365  if (m_ms)
2366  m_ms->DoDeleteThread(this);
2367 }
2368 
2370 {
2371  // sleep a little to let frontends reload the recordings list
2372  // after deleting a recording, then we can hammer the DB and filesystem
2373  std::this_thread::sleep_for(3s);
2374  std::this_thread::sleep_for(std::chrono::milliseconds(MythRandom()%2));
2375 
2376  m_deletelock.lock();
2377 
2378 #if 0
2379  QString logInfo = QString("recording id %1 (chanid %2 at %3)")
2380  .arg(ds->m_recordedid)
2381  .arg(ds->m_chanid)
2382  .arg(ds->m_recstartts.toString(Qt::ISODate));
2383 
2384  QString name = QString("deleteThread%1%2").arg(getpid()).arg(random());
2385 #endif
2386  QFile checkFile(ds->m_filename);
2387 
2389  {
2390  QString msg = QString("ERROR opening database connection for Delete "
2391  "Thread for chanid %1 recorded at %2. Program "
2392  "will NOT be deleted.")
2393  .arg(ds->m_chanid)
2394  .arg(ds->m_recstartts.toString(Qt::ISODate));
2395  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2396 
2397  m_deletelock.unlock();
2398  return;
2399  }
2400 
2401  ProgramInfo pginfo(ds->m_chanid, ds->m_recstartts);
2402 
2403  if (!pginfo.GetChanID())
2404  {
2405  QString msg = QString("ERROR retrieving program info when trying to "
2406  "delete program for chanid %1 recorded at %2. "
2407  "Recording will NOT be deleted.")
2408  .arg(ds->m_chanid)
2409  .arg(ds->m_recstartts.toString(Qt::ISODate));
2410  LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2411 
2412  m_deletelock.unlock();
2413  return;
2414  }
2415 
2416  // Don't allow deleting files where filesize != 0 and we can't find
2417  // the file, unless forceMetadataDelete has been set. This allows
2418  // deleting failed recordings without fuss, but blocks accidental
2419  // deletion of metadata for files where the filesystem has gone missing.
2420  if ((!checkFile.exists()) && pginfo.GetFilesize() &&
2421  (!ds->m_forceMetadataDelete))
2422  {
2423  LOG(VB_GENERAL, LOG_ERR, LOC +
2424  QString("ERROR when trying to delete file: %1. File "
2425  "doesn't exist. Database metadata will not be removed.")
2426  .arg(ds->m_filename));
2427 
2428  pginfo.SaveDeletePendingFlag(false);
2429  m_deletelock.unlock();
2430  return;
2431  }
2432 
2434 
2435  LiveTVChain *tvchain = GetChainWithRecording(pginfo);
2436  if (tvchain)
2437  tvchain->DeleteProgram(&pginfo);
2438 
2439  bool followLinks = gCoreContext->GetBoolSetting("DeletesFollowLinks", false);
2440  bool slowDeletes = gCoreContext->GetBoolSetting("TruncateDeletesSlowly", false);
2441  int fd = -1;
2442  off_t size = 0;
2443  bool errmsg = false;
2444 
2445  //-----------------------------------------------------------------------
2446  // TODO Move the following into DeleteRecordedFiles
2447  //-----------------------------------------------------------------------
2448 
2449  // Delete recording.
2450  if (slowDeletes)
2451  {
2452  // Since stat fails after unlinking on some filesystems,
2453  // get the filesize first
2454  const QFileInfo info(ds->m_filename);
2455  size = info.size();
2456  fd = DeleteFile(ds->m_filename, followLinks, ds->m_forceMetadataDelete);
2457 
2458  if ((fd < 0) && checkFile.exists())
2459  errmsg = true;
2460  }
2461  else
2462  {
2463  delete_file_immediately(ds->m_filename, followLinks, false);
2464  std::this_thread::sleep_for(2s);
2465  if (checkFile.exists())
2466  errmsg = true;
2467  }
2468 
2469  if (errmsg)
2470  {
2471  LOG(VB_GENERAL, LOG_ERR, LOC +
2472  QString("Error deleting file: %1. Keeping metadata in database.")
2473  .arg(ds->m_filename));
2474 
2475  pginfo.SaveDeletePendingFlag(false);
2476  m_deletelock.unlock();
2477  return;
2478  }
2479 
2480  // Delete all related files, though not the recording itself
2481  // i.e. preview thumbnails, srt subtitles, orphaned transcode temporary
2482  // files
2483  //
2484  // TODO: Delete everything with this basename to catch stray
2485  // .tmp and .old files, and future proof it
2486  QFileInfo fInfo( ds->m_filename );
2487  QStringList nameFilters;
2488  nameFilters.push_back(fInfo.fileName() + "*.png");
2489  nameFilters.push_back(fInfo.fileName() + "*.jpg");
2490  nameFilters.push_back(fInfo.fileName() + ".tmp");
2491  nameFilters.push_back(fInfo.fileName() + ".old");
2492  nameFilters.push_back(fInfo.fileName() + ".map");
2493  nameFilters.push_back(fInfo.fileName() + ".tmp.map");
2494  nameFilters.push_back(fInfo.baseName() + ".srt"); // e.g. 1234_20150213165800.srt
2495 
2496  QDir dir (fInfo.path());
2497  QFileInfoList miscFiles = dir.entryInfoList(nameFilters);
2498 
2499  for (const auto & file : qAsConst(miscFiles))
2500  {
2501  QString sFileName = file.absoluteFilePath();
2502  delete_file_immediately( sFileName, followLinks, true);
2503  }
2504  // -----------------------------------------------------------------------
2505 
2506  // TODO Have DeleteRecordedFiles do the deletion of all associated files
2507  DeleteRecordedFiles(ds);
2508 
2509  DoDeleteInDB(ds);
2510 
2511  m_deletelock.unlock();
2512 
2513  if (slowDeletes && fd >= 0)
2514  TruncateAndClose(&pginfo, fd, ds->m_filename, size);
2515 }
2516 
2518 {
2519  QString logInfo = QString("recording id %1 filename %2")
2520  .arg(ds->m_recordedid).arg(ds->m_filename);
2521 
2522  LOG(VB_GENERAL, LOG_NOTICE, "DeleteRecordedFiles - " + logInfo);
2523 
2524  MSqlQuery update(MSqlQuery::InitCon());
2526  query.prepare("SELECT basename, hostname, storagegroup FROM recordedfile "
2527  "WHERE recordedid = :RECORDEDID;");
2528  query.bindValue(":RECORDEDID", ds->m_recordedid);
2529 
2530  if (!query.exec() || !query.size())
2531  {
2532  MythDB::DBError("RecordedFiles deletion", query);
2533  LOG(VB_GENERAL, LOG_ERR, LOC +
2534  QString("Error querying recordedfiles for %1.") .arg(logInfo));
2535  }
2536 
2537  while (query.next())
2538  {
2539  QString basename = query.value(0).toString();
2540  //QString hostname = query.value(1).toString();
2541  //QString storagegroup = query.value(2).toString();
2542  bool deleteInDB = false;
2543 
2544  if (basename == QFileInfo(ds->m_filename).fileName())
2545  deleteInDB = true;
2546  else
2547  {
2548 // LOG(VB_FILE, LOG_INFO, LOC +
2549 // QString("DeleteRecordedFiles(%1), deleting '%2'")
2550 // .arg(logInfo).arg(query.value(0).toString()));
2551 //
2552 // StorageGroup sgroup(storagegroup);
2553 // QString localFile = sgroup.FindFile(basename);
2554 //
2555 // QString url = gCoreContext->GenMythURL(hostname,
2556 // gCoreContext->GetBackendServerPort(hostname),
2557 // basename,
2558 // storagegroup);
2559 //
2560 // if ((((hostname == gCoreContext->GetHostName()) ||
2561 // (!localFile.isEmpty())) &&
2562 // (HandleDeleteFile(basename, storagegroup))) ||
2563 // (((hostname != gCoreContext->GetHostName()) ||
2564 // (localFile.isEmpty())) &&
2565 // (RemoteFile::DeleteFile(url))))
2566 // {
2567 // deleteInDB = true;
2568 // }
2569  }
2570 
2571  if (deleteInDB)
2572  {
2573  update.prepare("DELETE FROM recordedfile "
2574  "WHERE recordedid = :RECORDEDID "
2575  "AND basename = :BASENAME ;");
2576  update.bindValue(":RECORDEDID", ds->m_recordedid);
2577  update.bindValue(":BASENAME", basename);
2578  if (!update.exec())
2579  {
2580  MythDB::DBError("RecordedFiles deletion", update);
2581  LOG(VB_GENERAL, LOG_ERR, LOC +
2582  QString("Error querying recordedfile (%1) for %2.")
2583  .arg(query.value(1).toString())
2584  .arg(logInfo));
2585  }
2586  }
2587  }
2588 }
2589 
2591 {
2592  QString logInfo = QString("recording id %1 (chanid %2 at %3)")
2593  .arg(ds->m_recordedid)
2594  .arg(ds->m_chanid).arg(ds->m_recstartts.toString(Qt::ISODate));
2595 
2596  LOG(VB_GENERAL, LOG_NOTICE, "DoDeleteINDB - " + logInfo);
2597 
2599  query.prepare("DELETE FROM recorded WHERE recordedid = :RECORDEDID AND "
2600  "title = :TITLE;");
2601  query.bindValue(":RECORDEDID", ds->m_recordedid);
2602  query.bindValue(":TITLE", ds->m_title);
2603 
2604  if (!query.exec() || !query.size())
2605  {
2606  MythDB::DBError("Recorded program deletion", query);
2607  LOG(VB_GENERAL, LOG_ERR, LOC +
2608  QString("Error deleting recorded entry for %1.") .arg(logInfo));
2609  }
2610 
2611  std::this_thread::sleep_for(1s);
2612 
2613  // Notify the frontend so it can requery for Free Space
2614  QString msg = QString("RECORDING_LIST_CHANGE DELETE %1")
2615  .arg(ds->m_recordedid);
2617 
2618  // sleep a little to let frontends reload the recordings list
2619  std::this_thread::sleep_for(3s);
2620 
2621  query.prepare("DELETE FROM recordedmarkup "
2622  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
2623  query.bindValue(":CHANID", ds->m_chanid);
2624  query.bindValue(":STARTTIME", ds->m_recstartts);
2625 
2626  if (!query.exec())
2627  {
2628  MythDB::DBError("Recorded program delete recordedmarkup", query);
2629  LOG(VB_GENERAL, LOG_ERR, LOC +
2630  QString("Error deleting recordedmarkup for %1.") .arg(logInfo));
2631  }
2632 
2633  query.prepare("DELETE FROM recordedseek "
2634  "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
2635  query.bindValue(":CHANID", ds->m_chanid);
2636  query.bindValue(":STARTTIME", ds->m_recstartts);
2637 
2638  if (!query.exec())
2639  {
2640  MythDB::DBError("Recorded program delete recordedseek", query);
2641  LOG(VB_GENERAL, LOG_ERR, LOC +
2642  QString("Error deleting recordedseek for %1.")
2643  .arg(logInfo));
2644  }
2645 }
2646 
2656 int MainServer::DeleteFile(const QString &filename, bool followLinks,
2657  bool deleteBrokenSymlinks)
2658 {
2659  QFileInfo finfo(filename);
2660  int fd = -1;
2661  QString linktext = "";
2662  QByteArray fname = filename.toLocal8Bit();
2663 
2664  LOG(VB_FILE, LOG_INFO, LOC +
2665  QString("About to unlink/delete file: '%1'")
2666  .arg(fname.constData()));
2667 
2668  QString errmsg = QString("Delete Error '%1'").arg(fname.constData());
2669  if (finfo.isSymLink())
2670  {
2671  linktext = getSymlinkTarget(filename);
2672  QByteArray alink = linktext.toLocal8Bit();
2673  errmsg += QString(" -> '%2'").arg(alink.constData());
2674  }
2675 
2676  if (followLinks && finfo.isSymLink())
2677  {
2678  if (!finfo.exists() && deleteBrokenSymlinks)
2679  unlink(fname.constData());
2680  else
2681  {
2682  fd = OpenAndUnlink(linktext);
2683  if (fd >= 0)
2684  unlink(fname.constData());
2685  }
2686  }
2687  else if (!finfo.isSymLink())
2688  {
2689  fd = OpenAndUnlink(filename);
2690  }
2691  else // just delete symlinks immediately
2692  {
2693  int err = unlink(fname.constData());
2694  if (err == 0)
2695  return -2; // valid result, not an error condition
2696  }
2697 
2698  if (fd < 0)
2699  LOG(VB_GENERAL, LOG_ERR, LOC + errmsg + ENO);
2700 
2701  return fd;
2702 }
2703 
2714 {
2715  QByteArray fname = filename.toLocal8Bit();
2716  QString msg = QString("Error deleting '%1'").arg(fname.constData());
2717  int fd = open(fname.constData(), O_WRONLY);
2718 
2719  if (fd == -1)
2720  {
2721  LOG(VB_GENERAL, LOG_ERR, LOC + msg + " could not open " + ENO);
2722  return -1;
2723  }
2724 
2725  if (unlink(fname.constData()))
2726  {
2727  LOG(VB_GENERAL, LOG_ERR, LOC + msg + " could not unlink " + ENO);
2728  close(fd);
2729  return -1;
2730  }
2731 
2732  return fd;
2733 }
2734 
2744  const QString &filename, off_t fsize)
2745 {
2746  QMutexLocker locker(&s_truncate_and_close_lock);
2747 
2748  if (pginfo)
2749  {
2750  pginfo->SetPathname(filename);
2751  pginfo->MarkAsInUse(true, kTruncatingDeleteInUseID);
2752  }
2753 
2754  int cards = 5;
2755  {
2757  query.prepare("SELECT COUNT(cardid) FROM capturecard;");
2758  if (query.exec() && query.next())
2759  cards = query.value(0).toInt();
2760  }
2761 
2762  // Time between truncation steps in milliseconds
2763  constexpr std::chrono::milliseconds sleep_time = 500ms;
2764  const size_t min_tps = 8 * 1024 * 1024;
2765  const auto calc_tps = (size_t) (cards * 1.2 * (22200000LL / 8.0));
2766  const size_t tps = std::max(min_tps, calc_tps);
2767  const auto increment = (size_t) (tps * (sleep_time.count() * 0.001F));
2768 
2769  LOG(VB_FILE, LOG_INFO, LOC +
2770  QString("Truncating '%1' by %2 MB every %3 milliseconds")
2771  .arg(filename)
2772  .arg(increment / (1024.0 * 1024.0), 0, 'f', 2)
2773  .arg(sleep_time.count()));
2774 
2775  GetMythDB()->GetDBManager()->PurgeIdleConnections(false);
2776 
2777  int count = 0;
2778  while (fsize > 0)
2779  {
2780 #if 0
2781  LOG(VB_FILE, LOG_DEBUG, LOC + QString("Truncating '%1' to %2 MB")
2782  .arg(filename).arg(fsize / (1024.0 * 1024.0), 0, 'f', 2));
2783 #endif
2784 
2785  int err = ftruncate(fd, fsize);
2786  if (err)
2787  {
2788  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error truncating '%1'")
2789  .arg(filename) + ENO);
2790  if (pginfo)
2791  pginfo->MarkAsInUse(false, kTruncatingDeleteInUseID);
2792  return 0 == close(fd);
2793  }
2794 
2795  fsize -= increment;
2796 
2797  if (pginfo && ((count % 100) == 0))
2798  pginfo->UpdateInUseMark(true);
2799 
2800  count++;
2801 
2802  std::this_thread::sleep_for(sleep_time);
2803  }
2804 
2805  bool ok = (0 == close(fd));
2806 
2807  if (pginfo)
2808  pginfo->MarkAsInUse(false, kTruncatingDeleteInUseID);
2809 
2810  LOG(VB_FILE, LOG_INFO, LOC +
2811  QString("Finished truncating '%1'").arg(filename));
2812 
2813  return ok;
2814 }
2815 
2817  PlaybackSock *pbs)
2818 {
2819  MythSocket *pbssock = nullptr;
2820  if (pbs)
2821  pbssock = pbs->getSocket();
2822 
2823  QStringList::const_iterator it = slist.cbegin() + 1;
2824  ProgramInfo pginfo(it, slist.cend());
2825 
2826  int result = 0;
2827 
2828  if (m_ismaster && pginfo.GetHostname() != gCoreContext->GetHostName())
2829  {
2830  PlaybackSock *slave = GetSlaveByHostname(pginfo.GetHostname());
2831  if (slave)
2832  {
2833  result = slave->CheckRecordingActive(&pginfo);
2834  slave->DecrRef();
2835  }
2836  }
2837  else
2838  {
2839  TVRec::s_inputsLock.lockForRead();
2840  for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter)
2841  {
2842  EncoderLink *elink = *iter;
2843 
2844  if (elink->IsLocal() && elink->MatchesRecording(&pginfo))
2845  result = iter.key();
2846  }
2847  TVRec::s_inputsLock.unlock();
2848  }
2849 
2850  QStringList outputlist( QString::number(result) );
2851  if (pbssock)
2852  SendResponse(pbssock, outputlist);
2853 }
2854 
2856 {
2857  QStringList::const_iterator it = slist.cbegin() + 1;
2858  RecordingInfo recinfo(it, slist.cend());
2859  if (recinfo.GetChanID())
2860  {
2861  if (m_ismaster)
2862  {
2863  // Stop recording may have been called for the same program on
2864  // different channel in the guide, we need to find the actual channel
2865  // that the recording is occurring on. This only needs doing once
2866  // on the master backend, as the correct chanid will then be sent
2867  // to the slave
2868  ProgramList schedList;
2869  bool hasConflicts = false;
2870  LoadFromScheduler(schedList, hasConflicts);
2871  for (auto *pInfo : schedList)
2872  {
2873  if ((pInfo->GetRecordingStatus() == RecStatus::Tuning ||
2874  pInfo->GetRecordingStatus() == RecStatus::Failing ||
2875  pInfo->GetRecordingStatus() == RecStatus::Recording)
2876  && recinfo.IsSameProgram(*pInfo))
2877  recinfo.SetChanID(pInfo->GetChanID());
2878  }
2879  }
2880  DoHandleStopRecording(recinfo, pbs);
2881  }
2882 }
2883 
2885  RecordingInfo &recinfo, PlaybackSock *pbs)
2886 {
2887  MythSocket *pbssock = nullptr;
2888  if (pbs)
2889  pbssock = pbs->getSocket();
2890 
2891  // FIXME! We don't know what state the recorder is in at this
2892  // time. Simply set the recstatus to RecStatus::Unknown and let the
2893  // scheduler do the best it can with it. The proper long term fix
2894  // is probably to have the recorder return the actual recstatus as
2895  // part of the stop recording response. That's a more involved
2896  // change than I care to make during the 0.25 code freeze.
2898 
2899  if (m_ismaster && recinfo.GetHostname() != gCoreContext->GetHostName())
2900  {
2901  PlaybackSock *slave = GetSlaveByHostname(recinfo.GetHostname());
2902 
2903  if (slave)
2904  {
2905  int num = slave->StopRecording(&recinfo);
2906 
2907  if (num > 0)
2908  {
2909  TVRec::s_inputsLock.lockForRead();
2910  if (m_encoderList->contains(num))
2911  {
2912  (*m_encoderList)[num]->StopRecording();
2913  }
2914  TVRec::s_inputsLock.unlock();
2915  if (m_sched)
2916  m_sched->UpdateRecStatus(&recinfo);
2917  }
2918  if (pbssock)
2919  {
2920  QStringList outputlist( "0" );
2921  SendResponse(pbssock, outputlist);
2922  }
2923 
2924  slave->DecrRef();
2925  return;
2926  }
2927 
2928  // If the slave is unreachable, we can assume that the
2929  // recording has stopped and the status should be updated.
2930  // Continue so that the master can try to update the endtime
2931  // of the file is in a shared directory.
2932  if (m_sched)
2933  m_sched->UpdateRecStatus(&recinfo);
2934  }
2935 
2936  int recnum = -1;
2937 
2938  TVRec::s_inputsLock.lockForRead();
2939  for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter)
2940  {
2941  EncoderLink *elink = *iter;
2942 
2943  if (elink->IsLocal() && elink->MatchesRecording(&recinfo))
2944  {
2945  recnum = iter.key();
2946 
2947  elink->StopRecording();
2948 
2949  while (elink->IsBusyRecording() ||
2950  elink->GetState() == kState_ChangingState)
2951  {
2952  std::this_thread::sleep_for(100us);
2953  }
2954 
2955  if (m_ismaster)
2956  {
2957  if (m_sched)
2958  m_sched->UpdateRecStatus(&recinfo);
2959  }
2960 
2961  break;
2962  }
2963  }
2964  TVRec::s_inputsLock.unlock();
2965 
2966  if (pbssock)
2967  {
2968  QStringList outputlist( QString::number(recnum) );
2969  SendResponse(pbssock, outputlist);
2970  }
2971 }
2972 
2973 void MainServer::HandleDeleteRecording(QString &chanid, QString &starttime,
2974  PlaybackSock *pbs,
2975  bool forceMetadataDelete,
2976  bool forgetHistory)
2977 {
2978  QDateTime recstartts = MythDate::fromString(starttime);
2979  RecordingInfo recinfo(chanid.toUInt(), recstartts);
2980 
2981  if (!recinfo.GetRecordingID())
2982  {
2983  qDebug() << "HandleDeleteRecording(chanid, starttime) Empty Recording ID";
2984  }
2985 
2986  if (!recinfo.GetChanID()) // !recinfo.GetRecordingID()
2987  {
2988  MythSocket *pbssock = nullptr;
2989  if (pbs)
2990  pbssock = pbs->getSocket();
2991 
2992  QStringList outputlist( QString::number(0) );
2993 
2994  SendResponse(pbssock, outputlist);
2995  return;
2996  }
2997 
2998  DoHandleDeleteRecording(recinfo, pbs, forceMetadataDelete, false, forgetHistory);
2999 }
3000 
3002  bool forceMetadataDelete)
3003 {
3004  QStringList::const_iterator it = slist.cbegin() + 1;
3005  RecordingInfo recinfo(it, slist.cend());
3006 
3007  if (!recinfo.GetRecordingID())
3008  {
3009  qDebug() << "HandleDeleteRecording(QStringList) Empty Recording ID";
3010  }
3011 
3012  if (recinfo.GetChanID()) // !recinfo.GetRecordingID()
3013  DoHandleDeleteRecording(recinfo, pbs, forceMetadataDelete, false, false);
3014 }
3015 
3017  RecordingInfo &recinfo, PlaybackSock *pbs,
3018  bool forceMetadataDelete, bool lexpirer, bool forgetHistory)
3019 {
3020  int resultCode = -1;
3021  MythSocket *pbssock = nullptr;
3022  if (pbs)
3023  pbssock = pbs->getSocket();
3024 
3025  bool justexpire = lexpirer ? false :
3026  ( //gCoreContext->GetNumSetting("AutoExpireInsteadOfDelete") &&
3027  (recinfo.GetRecordingGroup() != "Deleted") &&
3028  (recinfo.GetRecordingGroup() != "LiveTV"));
3029 
3030  QString filename = GetPlaybackURL(&recinfo, false);
3031  if (filename.isEmpty())
3032  {
3033  LOG(VB_GENERAL, LOG_ERR, LOC +
3034  QString("ERROR when trying to delete file for %1. Unable "
3035  "to determine filename of recording.")
3037 
3038  if (pbssock)
3039  {
3040  resultCode = -2;
3041  QStringList outputlist(QString::number(resultCode));
3042  SendResponse(pbssock, outputlist);
3043  }
3044 
3045  return;
3046  }
3047 
3048  // Stop the recording if it's still in progress.
3049  DoHandleStopRecording(recinfo, nullptr);
3050 
3051  if (justexpire && !forceMetadataDelete &&
3052  recinfo.GetFilesize() > (1024 * 1024) )
3053  {
3054  recinfo.ApplyRecordRecGroupChange("Deleted");
3055  recinfo.SaveAutoExpire(kDeletedAutoExpire, true);
3056  if (forgetHistory)
3057  recinfo.ForgetHistory();
3058  else if (m_sched)
3059  m_sched->RescheduleCheck(recinfo, "DoHandleDelete1");
3060  QStringList outputlist( QString::number(0) );
3061  SendResponse(pbssock, outputlist);
3062  return;
3063  }
3064 
3065  // If this recording was made by a another recorder, and that
3066  // recorder is available, tell it to do the deletion.
3067  if (m_ismaster && recinfo.GetHostname() != gCoreContext->GetHostName())
3068  {
3069  PlaybackSock *slave = GetSlaveByHostname(recinfo.GetHostname());
3070 
3071  if (slave)
3072  {
3073  int num = slave->DeleteRecording(&recinfo, forceMetadataDelete);
3074 
3075  if (forgetHistory)
3076  recinfo.ForgetHistory();
3077  else if (m_sched &&
3078  recinfo.GetRecordingGroup() != "Deleted" &&
3079  recinfo.GetRecordingGroup() != "LiveTV")
3080  m_sched->RescheduleCheck(recinfo, "DoHandleDelete2");
3081 
3082  if (pbssock)
3083  {
3084  QStringList outputlist( QString::number(num) );
3085  SendResponse(pbssock, outputlist);
3086  }
3087 
3088  slave->DecrRef();
3089  return;
3090  }
3091  }
3092 
3093  QFile checkFile(filename);
3094  bool fileExists = checkFile.exists();
3095  if (!fileExists)
3096  {
3097  QFile checkFileUTF8(QString::fromUtf8(filename.toLatin1().constData()));
3098  fileExists = checkFileUTF8.exists();
3099  if (fileExists)
3100  filename = QString::fromUtf8(filename.toLatin1().constData());
3101  }
3102 
3103  // Allow deleting of files where the recording failed meaning size == 0
3104  // But do not allow deleting of files that appear to be completely absent.
3105  // The latter condition indicates the filesystem containing the file is
3106  // most likely absent and deleting the file metadata is unsafe.
3107  if (fileExists || !recinfo.GetFilesize() || forceMetadataDelete)
3108  {
3109  recinfo.SaveDeletePendingFlag(true);
3110 
3111  if (!recinfo.GetRecordingID())
3112  {
3113  qDebug() << "DoHandleDeleteRecording() Empty Recording ID";
3114  }
3115 
3116  auto *deleteThread = new DeleteThread(this, filename,
3117  recinfo.GetTitle(), recinfo.GetChanID(),
3118  recinfo.GetRecordingStartTime(), recinfo.GetRecordingEndTime(),
3119  recinfo.GetRecordingID(),
3120  forceMetadataDelete);
3121  deleteThread->start();
3122  }
3123  else
3124  {
3125 #if 0
3126  QString logInfo = QString("chanid %1")
3127  .arg(recinfo.toString(ProgramInfo::kRecordingKey));
3128 #endif
3129 
3130  LOG(VB_GENERAL, LOG_ERR, LOC +
3131  QString("ERROR when trying to delete file: %1. File doesn't "
3132  "exist. Database metadata will not be removed.")
3133  .arg(filename));
3134  resultCode = -2;
3135  }
3136 
3137  if (pbssock)
3138  {
3139  QStringList outputlist( QString::number(resultCode) );
3140  SendResponse(pbssock, outputlist);
3141  }
3142 
3143  if (forgetHistory)
3144  recinfo.ForgetHistory();
3145  else if (m_sched &&
3146  recinfo.GetRecordingGroup() != "Deleted" &&
3147  recinfo.GetRecordingGroup() != "LiveTV")
3148  m_sched->RescheduleCheck(recinfo, "DoHandleDelete3");
3149 
3150  // Tell MythTV frontends that the recording list needs to be updated.
3151  if (fileExists || !recinfo.GetFilesize() || forceMetadataDelete)
3152  {
3154  QString("REC_DELETED CHANID %1 STARTTIME %2")
3155  .arg(recinfo.GetChanID())
3156  .arg(recinfo.GetRecordingStartTime(MythDate::ISODate)));
3157 
3158  recinfo.SendDeletedEvent();
3159  }
3160 }
3161 
3163 {
3164  if (slist.size() == 3)
3165  {
3166  RecordingInfo recinfo(
3167  slist[1].toUInt(), MythDate::fromString(slist[2]));
3168  if (recinfo.GetChanID())
3169  DoHandleUndeleteRecording(recinfo, pbs);
3170  }
3171  else if (slist.size() >= (1 + NUMPROGRAMLINES))
3172  {
3173  QStringList::const_iterator it = slist.cbegin()+1;
3174  RecordingInfo recinfo(it, slist.cend());
3175  if (recinfo.GetChanID())
3176  DoHandleUndeleteRecording(recinfo, pbs);
3177  }
3178 }
3179 
3181  RecordingInfo &recinfo, PlaybackSock *pbs)
3182 {
3183  int ret = -1;
3184 
3185  MythSocket *pbssock = nullptr;
3186  if (pbs)
3187  pbssock = pbs->getSocket();
3188 
3189 #if 0
3190  if (gCoreContext->GetNumSetting("AutoExpireInsteadOfDelete", 0))
3191 #endif
3192  {
3193  recinfo.ApplyRecordRecGroupChange("Default");
3194  recinfo.UpdateLastDelete(false);
3196  if (m_sched)
3197  m_sched->RescheduleCheck(recinfo, "DoHandleUndelete");
3198  ret = 0;
3199  }
3200 
3201  QStringList outputlist( QString::number(ret) );
3202  SendResponse(pbssock, outputlist);
3203 }
3204 
3231 void MainServer::HandleRescheduleRecordings(const QStringList &request,
3232  PlaybackSock *pbs)
3233 {
3234  QStringList result;
3235  if (m_sched)
3236  {
3237  m_sched->Reschedule(request);
3238  result = QStringList(QString::number(1));
3239  }
3240  else
3241  result = QStringList(QString::number(0));
3242 
3243  if (pbs)
3244  {
3245  MythSocket *pbssock = pbs->getSocket();
3246  if (pbssock)
3247  SendResponse(pbssock, result);
3248  }
3249 }
3250 
3252 {
3253  // If we're already trying to add a child input, ignore this
3254  // attempt. The scheduler will keep asking until it gets added.
3255  // This makes the whole operation asynchronous and allows the
3256  // scheduler to continue servicing other recordings.
3257  if (!m_addChildInputLock.tryLock())
3258  {
3259  LOG(VB_GENERAL, LOG_INFO, LOC + "HandleAddChildInput: Already locked");
3260  return false;
3261  }
3262 
3263  LOG(VB_GENERAL, LOG_INFO, LOC +
3264  QString("HandleAddChildInput: Handling input %1").arg(inputid));
3265 
3266  TVRec::s_inputsLock.lockForWrite();
3267 
3268  if (m_ismaster)
3269  {
3270  // First, add the new input to the database.
3271  uint childid = CardUtil::AddChildInput(inputid);
3272  if (!childid)
3273  {
3274  LOG(VB_GENERAL, LOG_ERR, LOC +
3275  QString("HandleAddChildInput: "
3276  "Failed to add child to input %1").arg(inputid));
3277  TVRec::s_inputsLock.unlock();
3278  m_addChildInputLock.unlock();
3279  return false;
3280  }
3281 
3282  LOG(VB_GENERAL, LOG_INFO, LOC +
3283  QString("HandleAddChildInput: Added child input %1").arg(childid));
3284 
3285  // Next, create the master TVRec and/or EncoderLink.
3286  QString localhostname = gCoreContext->GetHostName();
3287  QString hostname = CardUtil::GetHostname(childid);
3288 
3289  if (hostname == localhostname)
3290  {
3291  auto *tv = new TVRec(childid);
3292  if (!tv || !tv->Init())
3293  {
3294  LOG(VB_GENERAL, LOG_ERR, LOC +
3295  QString("HandleAddChildInput: "
3296  "Failed to initialize input %1").arg(childid));
3297  delete tv;
3298  CardUtil::DeleteInput(childid);
3299  TVRec::s_inputsLock.unlock();
3300  m_addChildInputLock.unlock();
3301  return false;
3302  }
3303 
3304  auto *enc = new EncoderLink(childid, tv);
3305  (*m_encoderList)[childid] = enc;
3306  }
3307  else
3308  {
3309  EncoderLink *enc = (*m_encoderList)[inputid];
3310  if (!enc->AddChildInput(childid))
3311  {
3312  LOG(VB_GENERAL, LOG_ERR, LOC +
3313  QString("HandleAddChildInput: "
3314  "Failed to add remote input %1").arg(childid));
3315  CardUtil::DeleteInput(childid);
3316  TVRec::s_inputsLock.unlock();
3317  m_addChildInputLock.unlock();
3318  return false;
3319  }
3320 
3321  PlaybackSock *pbs = enc->GetSocket();
3322  enc = new EncoderLink(childid, nullptr, hostname);
3323  enc->SetSocket(pbs);
3324  (*m_encoderList)[childid] = enc;
3325  }
3326 
3327  // Finally, add the new input to the Scheduler.
3328  m_sched->AddChildInput(inputid, childid);
3329  }
3330  else
3331  {
3332  // Create the slave TVRec and EncoderLink.
3333  auto *tv = new TVRec(inputid);
3334  if (!tv || !tv->Init())
3335  {
3336  LOG(VB_GENERAL, LOG_ERR, LOC +
3337  QString("HandleAddChildInput: "
3338  "Failed to initialize input %1").arg(inputid));
3339  delete tv;
3340  TVRec::s_inputsLock.unlock();
3341  m_addChildInputLock.unlock();
3342  return false;
3343  }
3344 
3345  auto *enc = new EncoderLink(inputid, tv);
3346  (*m_encoderList)[inputid] = enc;
3347  }
3348 
3349  TVRec::s_inputsLock.unlock();
3350  m_addChildInputLock.unlock();
3351 
3352  LOG(VB_GENERAL, LOG_INFO, LOC +
3353  QString("HandleAddChildInput: "
3354  "Successfully handled input %1").arg(inputid));
3355 
3356  return true;
3357 }
3358 
3360 {
3361  QStringList::const_iterator it = slist.cbegin() + 1;
3362  RecordingInfo recinfo(it, slist.cend());
3363  if (recinfo.GetChanID())
3364  recinfo.ForgetHistory();
3365 
3366  MythSocket *pbssock = nullptr;
3367  if (pbs)
3368  pbssock = pbs->getSocket();
3369  if (pbssock)
3370  {
3371  QStringList outputlist( QString::number(0) );
3372  SendResponse(pbssock, outputlist);
3373  }
3374 }
3375 
3382 {
3383  QStringList strlist;
3384 
3385  QString sleepCmd = gCoreContext->GetSetting("SleepCommand");
3386  if (!sleepCmd.isEmpty())
3387  {
3388  strlist << "OK";
3389  SendResponse(pbs->getSocket(), strlist);
3390  LOG(VB_GENERAL, LOG_NOTICE, LOC +
3391  "Received GO_TO_SLEEP command from master, running SleepCommand.");
3392  myth_system(sleepCmd);
3393  }
3394  else
3395  {
3396  strlist << "ERROR: SleepCommand is empty";
3397  LOG(VB_GENERAL, LOG_ERR, LOC +
3398  "ERROR: in HandleGoToSleep(), but no SleepCommand found!");
3399  SendResponse(pbs->getSocket(), strlist);
3400  }
3401 }
3402 
3413 {
3414  QStringList strlist;
3415 
3416  if (allHosts)
3417  {
3418  QMutexLocker locker(&m_masterFreeSpaceListLock);
3419  strlist = m_masterFreeSpaceList;
3422  {
3424  {
3426  m_masterFreeSpaceListWait.wait(locker.mutex());
3427  }
3430  m_masterFreeSpaceListUpdater, "FreeSpaceUpdater");
3431  }
3432  }
3433  else
3434  BackendQueryDiskSpace(strlist, allHosts, allHosts);
3435 
3436  SendResponse(pbs->getSocket(), strlist);
3437 }
3438 
3445 {
3446  QStringList strlist;
3447  {
3448  QMutexLocker locker(&m_masterFreeSpaceListLock);
3449  strlist = m_masterFreeSpaceList;
3452  {
3454  {
3456  m_masterFreeSpaceListWait.wait(locker.mutex());
3457  }
3460  m_masterFreeSpaceListUpdater, "FreeSpaceUpdater");
3461  }
3462  }
3463 
3464  // The TotalKB and UsedKB are the last two numbers encoded in the list
3465  QStringList shortlist;
3466  if (strlist.size() < 4)
3467  {
3468  shortlist << QString("0");
3469  shortlist << QString("0");
3470  }
3471  else
3472  {
3473  unsigned int index = (uint)(strlist.size()) - 2;
3474  shortlist << strlist[index++];
3475  shortlist << strlist[index++];
3476  }
3477 
3478  SendResponse(pbs->getSocket(), shortlist);
3479 }
3480 
3488 {
3489  MythSocket *pbssock = pbs->getSocket();
3490 
3491  QStringList strlist;
3492 
3493 #if defined(_WIN32) || defined(Q_OS_ANDROID)
3494  strlist << "0" << "0" << "0";
3495 #else
3496  loadArray loads = getLoadAvgs();
3497  if (loads[0] == -1)
3498  {
3499  strlist << "ERROR";
3500  strlist << "getloadavg() failed";
3501  }
3502  else
3503  {
3504  strlist << QString::number(loads[0])
3505  << QString::number(loads[1])
3506  << QString::number(loads[2]);
3507  }
3508 #endif
3509 
3510  SendResponse(pbssock, strlist);
3511 }
3512 
3519 {
3520  MythSocket *pbssock = pbs->getSocket();
3521  QStringList strlist;
3522  std::chrono::seconds uptime = 0s;
3523 
3524  if (getUptime(uptime))
3525  strlist << QString::number(uptime.count());
3526  else
3527  {
3528  strlist << "ERROR";
3529  strlist << "Could not determine uptime.";
3530  }
3531 
3532  SendResponse(pbssock, strlist);
3533 }
3534 
3541 {
3542  MythSocket *pbssock = pbs->getSocket();
3543  QStringList strlist;
3544 
3545  strlist << gCoreContext->GetHostName();
3546 
3547  SendResponse(pbssock, strlist);
3548 }
3549 
3556 {
3557  MythSocket *pbssock = pbs->getSocket();
3558  QStringList strlist;
3559  int totalMB = 0;
3560  int freeMB = 0;
3561  int totalVM = 0;
3562  int freeVM = 0;
3563 
3564  if (getMemStats(totalMB, freeMB, totalVM, freeVM))
3565  {
3566  strlist << QString::number(totalMB) << QString::number(freeMB)
3567  << QString::number(totalVM) << QString::number(freeVM);
3568  }
3569  else
3570  {
3571  strlist << "ERROR";
3572  strlist << "Could not determine memory stats.";
3573  }
3574 
3575  SendResponse(pbssock, strlist);
3576 }
3577 
3584 {
3585  MythSocket *pbssock = pbs->getSocket();
3586  QStringList strlist;
3587  strlist << MythTZ::getTimeZoneID()
3588  << QString::number(MythTZ::calc_utc_offset())
3590 
3591  SendResponse(pbssock, strlist);
3592 }
3593 
3599 {
3600  MythSocket *pbssock = pbs->getSocket();
3601  bool checkSlaves = slist[1].toInt() != 0;
3602 
3603  QStringList::const_iterator it = slist.cbegin() + 2;
3604  RecordingInfo recinfo(it, slist.cend());
3605 
3606  bool exists = false;
3607 
3608  if (recinfo.HasPathname() && (m_ismaster) &&
3609  (recinfo.GetHostname() != gCoreContext->GetHostName()) &&
3610  (checkSlaves))
3611  {
3613 
3614  if (slave)
3615  {
3616  exists = slave->CheckFile(&recinfo);
3617  slave->DecrRef();
3618 
3619  QStringList outputlist( QString::number(static_cast<int>(exists)) );
3620  if (exists)
3621  outputlist << recinfo.GetPathname();
3622  else
3623  outputlist << "";
3624 
3625  SendResponse(pbssock, outputlist);
3626  return;
3627  }
3628  }
3629 
3630  QString pburl;
3631  if (recinfo.HasPathname())
3632  {
3633  pburl = GetPlaybackURL(&recinfo);
3634  exists = QFileInfo::exists(pburl);
3635  if (!exists)
3636  pburl.clear();
3637  }
3638 
3639  QStringList strlist( QString::number(static_cast<int>(exists)) );
3640  strlist << pburl;
3641  SendResponse(pbssock, strlist);
3642 }
3643 
3644 
3650 {
3651  QString storageGroup = "Default";
3652  QString hostname = gCoreContext->GetHostName();
3653  QString filename = "";
3654  QStringList res;
3655 
3656  switch (slist.size()) {
3657  case 4:
3658  if (!slist[3].isEmpty())
3659  hostname = slist[3];
3660  [[clang::fallthrough]];
3661  case 3:
3662  if (slist[2].isEmpty())
3663  storageGroup = slist[2];
3664  [[clang::fallthrough]];
3665  case 2:
3666  filename = slist[1];
3667  if (filename.isEmpty() ||
3668  filename.contains("/../") ||
3669  filename.startsWith("../"))
3670  {
3671  LOG(VB_GENERAL, LOG_ERR, LOC +
3672  QString("ERROR checking for file, filename '%1' "
3673  "fails sanity checks").arg(filename));
3674  res << "";
3675  SendResponse(pbs->getSocket(), res);
3676  return;
3677  }
3678  break;
3679  default:
3680  LOG(VB_GENERAL, LOG_ERR, LOC +
3681  "ERROR, invalid input count for QUERY_FILE_HASH");
3682  res << "";
3683  SendResponse(pbs->getSocket(), res);
3684  return;
3685  }
3686 
3687  QString hash = "";
3688 
3690  {
3691  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName());
3692  QString fullname = sgroup.FindFile(filename);
3693  hash = FileHash(fullname);
3694  }
3695  else
3696  {
3698  if (slave)
3699  {
3700  hash = slave->GetFileHash(filename, storageGroup);
3701  slave->DecrRef();
3702  }
3703  // I deleted the incorrect SQL select that was supposed to get
3704  // host name from ip address. Since it cannot work and has
3705  // been there 6 years I assume it is not important.
3706  }
3707 
3708  res << hash;
3709  SendResponse(pbs->getSocket(), res);
3710 }
3711 
3717 {
3718  QString filename = slist[1];
3719  QString storageGroup = "Default";
3720  QStringList retlist;
3721 
3722  if (slist.size() > 2)
3723  storageGroup = slist[2];
3724 
3725  if ((filename.isEmpty()) ||
3726  (filename.contains("/../")) ||
3727  (filename.startsWith("../")))
3728  {
3729  LOG(VB_GENERAL, LOG_ERR, LOC +
3730  QString("ERROR checking for file, filename '%1' "
3731  "fails sanity checks").arg(filename));
3732  retlist << "0";
3733  SendResponse(pbs->getSocket(), retlist);
3734  return;
3735  }
3736 
3737  if (storageGroup.isEmpty())
3738  storageGroup = "Default";
3739 
3740  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName());
3741 
3742  QString fullname = sgroup.FindFile(filename);
3743 
3744  if (!fullname.isEmpty())
3745  {
3746  retlist << "1";
3747  retlist << fullname;
3748 
3749  struct stat fileinfo {};
3750  if (stat(fullname.toLocal8Bit().constData(), &fileinfo) >= 0)
3751  {
3752  retlist << QString::number(fileinfo.st_dev);
3753  retlist << QString::number(fileinfo.st_ino);
3754  retlist << QString::number(fileinfo.st_mode);
3755  retlist << QString::number(fileinfo.st_nlink);
3756  retlist << QString::number(fileinfo.st_uid);
3757  retlist << QString::number(fileinfo.st_gid);
3758  retlist << QString::number(fileinfo.st_rdev);
3759  retlist << QString::number(fileinfo.st_size);
3760 #ifdef _WIN32
3761  retlist << "0"; // st_blksize
3762  retlist << "0"; // st_blocks
3763 #else
3764  retlist << QString::number(fileinfo.st_blksize);
3765  retlist << QString::number(fileinfo.st_blocks);
3766 #endif
3767  retlist << QString::number(fileinfo.st_atime);
3768  retlist << QString::number(fileinfo.st_mtime);
3769  retlist << QString::number(fileinfo.st_ctime);
3770  }
3771  }
3772  else
3773  retlist << "0";
3774 
3775  SendResponse(pbs->getSocket(), retlist);
3776 }
3777 
3778 void MainServer::getGuideDataThrough(QDateTime &GuideDataThrough)
3779 {
3781  query.prepare("SELECT MAX(endtime) FROM program WHERE manualid = 0;");
3782 
3783  if (query.exec() && query.next())
3784  {
3785  GuideDataThrough = MythDate::fromString(query.value(0).toString());
3786  }
3787 }
3788 
3790 {
3791  QDateTime GuideDataThrough;
3792  MythSocket *pbssock = pbs->getSocket();
3793  QStringList strlist;
3794 
3795  getGuideDataThrough(GuideDataThrough);
3796 
3797  if (GuideDataThrough.isNull())
3798  strlist << QString("0000-00-00 00:00");
3799  else
3800  strlist << GuideDataThrough.toString("yyyy-MM-dd hh:mm");
3801 
3802  SendResponse(pbssock, strlist);
3803 }
3804 
3806  const QString& tmptable, int recordid)
3807 {
3808  MythSocket *pbssock = pbs->getSocket();
3809 
3810  QStringList strList;
3811 
3812  if (m_sched)
3813  {
3814  if (tmptable.isEmpty())
3815  m_sched->GetAllPending(strList);
3816  else
3817  {
3818  auto *sched = new Scheduler(false, m_encoderList, tmptable, m_sched);
3819  sched->FillRecordListFromDB(recordid);
3820  sched->GetAllPending(strList);
3821  delete sched;
3822 
3823  if (recordid > 0)
3824  {
3826  query.prepare("SELECT NULL FROM record "
3827  "WHERE recordid = :RECID;");
3828  query.bindValue(":RECID", recordid);
3829 
3830  if (query.exec() && query.size())
3831  {
3832  auto *record = new RecordingRule();
3833  record->m_recordID = recordid;
3834  if (record->Load() &&
3835  record->m_searchType == kManualSearch)
3836  m_sched->RescheduleMatch(recordid, 0, 0, QDateTime(),
3837  "Speculation");
3838  delete record;
3839  }
3840  query.prepare("DELETE FROM program WHERE manualid = :RECID;");
3841  query.bindValue(":RECID", recordid);
3842  if (!query.exec())
3843  MythDB::DBError("MainServer::HandleGetPendingRecordings "
3844  "- delete", query);
3845  }
3846  }
3847  }
3848  else
3849  {
3850  strList << QString::number(0);
3851  strList << QString::number(0);
3852  }
3853 
3854  SendResponse(pbssock, strList);
3855 }
3856 
3858 {
3859  MythSocket *pbssock = pbs->getSocket();
3860 
3861  QStringList strList;
3862 
3863  if (m_sched)
3864  Scheduler::GetAllScheduled(strList);
3865  else
3866  strList << QString::number(0);
3867 
3868  SendResponse(pbssock, strList);
3869 }
3870 
3872  PlaybackSock *pbs)
3873 {
3874  MythSocket *pbssock = pbs->getSocket();
3875 
3876  QStringList::const_iterator it = slist.cbegin() + 1;
3877  RecordingInfo recinfo(it, slist.cend());
3878 
3879  QStringList strlist;
3880 
3881  if (m_sched && recinfo.GetChanID())
3882  m_sched->getConflicting(&recinfo, strlist);
3883  else
3884  strlist << QString::number(0);
3885 
3886  SendResponse(pbssock, strlist);
3887 }
3888 
3890 {
3891  MythSocket *pbssock = pbs->getSocket();
3892 
3893  QStringList strList;
3894 
3895  if (m_expirer)
3896  m_expirer->GetAllExpiring(strList);
3897  else
3898  strList << QString::number(0);
3899 
3900  SendResponse(pbssock, strList);
3901 }
3902 
3903 void MainServer::HandleSGGetFileList(QStringList &sList,
3904  PlaybackSock *pbs)
3905 {
3906  MythSocket *pbssock = pbs->getSocket();
3907  QStringList strList;
3908 
3909  if ((sList.size() < 4) || (sList.size() > 5))
3910  {
3911  LOG(VB_GENERAL, LOG_ERR, LOC +
3912  QString("HandleSGGetFileList: Invalid Request. %1")
3913  .arg(sList.join("[]:[]")));
3914  strList << "EMPTY LIST";
3915  SendResponse(pbssock, strList);
3916  return;
3917  }
3918 
3919  QString host = gCoreContext->GetHostName();
3920  const QString& wantHost = sList.at(1);
3921  QHostAddress wantHostaddr(wantHost);
3922  const QString& groupname = sList.at(2);
3923  const QString& path = sList.at(3);
3924  bool fileNamesOnly = false;
3925 
3926  if (sList.size() >= 5)
3927  fileNamesOnly = (sList.at(4).toInt() != 0);
3928 
3929  bool slaveUnreachable = false;
3930 
3931  LOG(VB_FILE, LOG_INFO, LOC +
3932  QString("HandleSGGetFileList: group = %1 host = %2 "
3933  " path = %3 wanthost = %4")
3934  .arg(groupname).arg(host).arg(path).arg(wantHost));
3935 
3936  QString addr = gCoreContext->GetBackendServerIP();
3937 
3938  if ((host.toLower() == wantHost.toLower()) ||
3939  (!addr.isEmpty() && addr == wantHostaddr.toString()))
3940  {
3941  StorageGroup sg(groupname, host);
3942  LOG(VB_FILE, LOG_INFO, LOC + "HandleSGGetFileList: Getting local info");
3943  if (fileNamesOnly)
3944  strList = sg.GetFileList(path);
3945  else
3946  strList = sg.GetFileInfoList(path);
3947  }
3948  else
3949  {
3950  PlaybackSock *slave = GetMediaServerByHostname(wantHost);
3951  if (slave)
3952  {
3953  LOG(VB_FILE, LOG_INFO, LOC +
3954  "HandleSGGetFileList: Getting remote info");
3955  strList = slave->GetSGFileList(wantHost, groupname, path,
3956  fileNamesOnly);
3957  slave->DecrRef();
3958  slaveUnreachable = false;
3959  }
3960  else
3961  {
3962  LOG(VB_FILE, LOG_INFO, LOC +
3963  QString("HandleSGGetFileList: Failed to grab slave socket "
3964  ": %1 :").arg(wantHost));
3965  slaveUnreachable = true;
3966  }
3967 
3968  }
3969 
3970  if (slaveUnreachable)
3971  strList << "SLAVE UNREACHABLE: " << host;
3972 
3973  if (strList.isEmpty() || (strList.at(0) == "0"))
3974  strList << "EMPTY LIST";
3975 
3976  SendResponse(pbssock, strList);
3977 }
3978 
3980 {
3981 //format: QUERY_FINDFILE <host> <storagegroup> <filename> <useregex (optional)> <allowfallback (optional)>
3982 
3983  QString hostname = slist[1];
3984  QString storageGroup = slist[2];
3985  QString filename = slist[3];
3986  bool allowFallback = true;
3987  bool useRegex = false;
3988  QStringList fileList;
3989 
3990  if (!QHostAddress(hostname).isNull())
3991  {
3992  LOG(VB_GENERAL, LOG_ERR, QString("Mainserver: QUERY_FINDFILE called "
3993  "with IP (%1) instead of hostname. "
3994  "This is invalid.").arg(hostname));
3995  }
3996 
3997  if (hostname.isEmpty())
3999 
4000  if (storageGroup.isEmpty())
4001  storageGroup = "Default";
4002 
4003  if (filename.isEmpty() || filename.contains("/../") ||
4004  filename.startsWith("../"))
4005  {
4006  LOG(VB_GENERAL, LOG_ERR, LOC +
4007  QString("ERROR QueryFindFile, filename '%1' "
4008  "fails sanity checks").arg(filename));
4009  fileList << "ERROR: Bad/Missing Filename";
4010  SendResponse(pbs->getSocket(), fileList);
4011  return;
4012  }
4013 
4014  if (slist.size() >= 5)
4015  useRegex = (slist[4].toInt() > 0);
4016 
4017  if (slist.size() >= 6)
4018  allowFallback = (slist[5].toInt() > 0);
4019 
4020  LOG(VB_FILE, LOG_INFO, LOC +
4021  QString("Looking for file '%1' on host '%2' in group '%3' (useregex: %4, allowfallback: %5")
4022  .arg(filename).arg(hostname).arg(storageGroup).arg(useRegex).arg(allowFallback));
4023 
4024  // first check the given host
4026  {
4027  LOG(VB_FILE, LOG_INFO, LOC + QString("Checking local host '%1' for file").arg(gCoreContext->GetHostName()));
4028 
4029  // check the local storage group
4030  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName(), false);
4031 
4032  if (useRegex)
4033  {
4034  QFileInfo fi(filename);
4035  QStringList files = sgroup.GetFileList('/' + fi.path());
4036 
4037  LOG(VB_FILE, LOG_INFO, LOC + QString("Looking in dir '%1' for '%2'").arg(fi.path()).arg(fi.fileName()));
4038 
4039  for (int x = 0; x < files.size(); x++)
4040  {
4041  LOG(VB_FILE, LOG_INFO, LOC + QString("Found '%1 - %2'").arg(x).arg(files[x]));
4042  }
4043 
4044  QStringList filteredFiles = files.filter(QRegExp(fi.fileName()));
4045  for (int x = 0; x < filteredFiles.size(); x++)
4046  {
4049  fi.path() + '/' + filteredFiles[x],
4050  storageGroup);
4051  }
4052  }
4053  else
4054  {
4055  if (!sgroup.FindFile(filename).isEmpty())
4056  {
4059  filename, storageGroup);
4060  }
4061  }
4062  }
4063  else
4064  {
4065  LOG(VB_FILE, LOG_INFO, LOC + QString("Checking remote host '%1' for file").arg(hostname));
4066 
4067  // check the given slave hostname
4069  if (slave)
4070  {
4071  QStringList slaveFiles = slave->GetFindFile(hostname, filename, storageGroup, useRegex);
4072 
4073  if (!slaveFiles.isEmpty() && slaveFiles[0] != "NOT FOUND" && !slaveFiles[0].startsWith("ERROR: "))
4074  fileList += slaveFiles;
4075 
4076  slave->DecrRef();
4077  }
4078  else
4079  {
4080  LOG(VB_FILE, LOG_INFO, LOC + QString("Slave '%1' was unreachable").arg(hostname));
4081  fileList << QString("ERROR: SLAVE UNREACHABLE: %1").arg(hostname);
4082  SendResponse(pbs->getSocket(), fileList);
4083  return;
4084  }
4085  }
4086 
4087  // if we still haven't found it and this is the master and fallback is enabled
4088  // check all other slaves that have a directory in the storagegroup
4089  if (m_ismaster && fileList.isEmpty() && allowFallback)
4090  {
4091  // get a list of hosts
4093 
4094  QString sql = "SELECT DISTINCT hostname "
4095  "FROM storagegroup "
4096  "WHERE groupname = :GROUP "
4097  "AND hostname != :HOSTNAME";
4098  query.prepare(sql);
4099  query.bindValue(":GROUP", storageGroup);
4100  query.bindValue(":HOSTNAME", hostname);
4101 
4102  if (!query.exec() || !query.isActive())
4103  {
4104  MythDB::DBError(LOC + "FindFile() get host list", query);
4105  fileList << "ERROR: failed to get host list";
4106  SendResponse(pbs->getSocket(), fileList);
4107  return;
4108  }
4109 
4110  while(query.next())
4111  {
4112  hostname = query.value(0).toString();
4113 
4115  {
4116  StorageGroup sgroup(storageGroup, hostname);
4117 
4118  if (useRegex)
4119  {
4120  QFileInfo fi(filename);
4121  QStringList files = sgroup.GetFileList('/' + fi.path());
4122 
4123  LOG(VB_FILE, LOG_INFO, LOC + QString("Looking in dir '%1' for '%2'").arg(fi.path()).arg(fi.fileName()));
4124 
4125  for (int x = 0; x < files.size(); x++)
4126  {
4127  LOG(VB_FILE, LOG_INFO, LOC + QString("Found '%1 - %2'").arg(x).arg(files[x]));
4128  }
4129 
4130  QStringList filteredFiles = files.filter(QRegExp(fi.fileName()));
4131 
4132  for (int x = 0; x < filteredFiles.size(); x++)
4133  {
4136  fi.path() + '/' + filteredFiles[x],
4137  storageGroup);
4138  }
4139  }
4140  else
4141  {
4142  QString fname = sgroup.FindFile(filename);
4143  if (!fname.isEmpty())
4144  {
4147  filename, storageGroup);
4148  }
4149  }
4150  }
4151  else
4152  {
4153  // check the slave host
4155  if (slave)
4156  {
4157  QStringList slaveFiles = slave->GetFindFile(hostname, filename, storageGroup, useRegex);
4158  if (!slaveFiles.isEmpty() && slaveFiles[0] != "NOT FOUND" && !slaveFiles[0].startsWith("ERROR: "))
4159  fileList += slaveFiles;
4160 
4161  slave->DecrRef();
4162  }
4163  }
4164 
4165  if (!fileList.isEmpty())
4166  break;
4167  }
4168  }
4169 
4170  if (fileList.isEmpty())
4171  {
4172  fileList << "NOT FOUND";
4173  LOG(VB_FILE, LOG_INFO, LOC + QString("File was not found"));
4174  }
4175  else
4176  {
4177  for (int x = 0; x < fileList.size(); x++)
4178  {
4179  LOG(VB_FILE, LOG_INFO, LOC + QString("File %1 was found at: '%2'").arg(x).arg(fileList[0]));
4180  }
4181  }
4182 
4183  SendResponse(pbs->getSocket(), fileList);
4184 }
4185 
4186 void MainServer::HandleSGFileQuery(QStringList &sList,
4187  PlaybackSock *pbs)
4188 {
4189 //format: QUERY_SG_FILEQUERY <host> <storagegroup> <filename> <allowfallback (optional)>
4190 
4191  MythSocket *pbssock = pbs->getSocket();
4192  QStringList strList;
4193 
4194  if (sList.size() < 4)
4195  {
4196  LOG(VB_GENERAL, LOG_ERR, LOC +
4197  QString("HandleSGFileQuery: Invalid Request. %1")
4198  .arg(sList.join("[]:[]")));
4199  strList << "EMPTY LIST";
4200  SendResponse(pbssock, strList);
4201  return;
4202  }
4203 
4204  QString host = gCoreContext->GetHostName();
4205  const QString& wantHost = sList.at(1);
4206  QHostAddress wantHostaddr(wantHost);
4207  const QString& groupname = sList.at(2);
4208  const QString& filename = sList.at(3);
4209 
4210  bool allowFallback = true;
4211  if (sList.size() >= 5)
4212  allowFallback = (sList.at(4).toInt() > 0);
4213  LOG(VB_FILE, LOG_ERR, QString("HandleSGFileQuery - allowFallback: %1").arg(allowFallback));
4214 
4215  bool slaveUnreachable = false;
4216 
4217  LOG(VB_FILE, LOG_INFO, LOC + QString("HandleSGFileQuery: %1")
4218  .arg(gCoreContext->GenMythURL(wantHost, 0, filename, groupname)));
4219 
4220  QString addr = gCoreContext->GetBackendServerIP();
4221 
4222  if ((host.toLower() == wantHost.toLower()) ||
4223  (!addr.isEmpty() && addr == wantHostaddr.toString()))
4224  {
4225  LOG(VB_FILE, LOG_INFO, LOC + "HandleSGFileQuery: Getting local info");
4226  StorageGroup sg(groupname, gCoreContext->GetHostName(), allowFallback);
4227  strList = sg.GetFileInfo(filename);
4228  }
4229  else
4230  {
4231  PlaybackSock *slave = GetMediaServerByHostname(wantHost);
4232  if (slave)
4233  {
4234  LOG(VB_FILE, LOG_INFO, LOC +
4235  "HandleSGFileQuery: Getting remote info");
4236  strList = slave->GetSGFileQuery(wantHost, groupname, filename);
4237  slave->DecrRef();
4238  slaveUnreachable = false;
4239  }
4240  else
4241  {
4242  LOG(VB_FILE, LOG_INFO, LOC +
4243  QString("HandleSGFileQuery: Failed to grab slave socket : %1 :")
4244  .arg(wantHost));
4245  slaveUnreachable = true;
4246  }
4247 
4248  }
4249 
4250  if (slaveUnreachable)
4251  strList << "SLAVE UNREACHABLE: " << wantHost;
4252 
4253  if (strList.count() == 0 || (strList.at(0) == "0"))
4254  strList << "EMPTY LIST";
4255 
4256  SendResponse(pbssock, strList);
4257 }
4258 
4260 {
4261  MythSocket *pbssock = pbs->getSocket();
4262  QString pbshost = pbs->getHostname();
4263 
4264  QStringList strlist;
4265 
4266  EncoderLink *encoder = nullptr;
4267  QString enchost;
4268 
4269  TVRec::s_inputsLock.lockForRead();
4270  for (auto * elink : qAsConst(*m_encoderList))
4271  {
4272  // we're looking for a specific card but this isn't the one we want
4273  if ((cardid != -1) && (cardid != elink->GetInputID()))
4274  continue;
4275 
4276  if (elink->IsLocal())
4277  enchost = gCoreContext->GetHostName();
4278  else
4279  enchost = elink->GetHostName();
4280 
4281  if ((enchost == pbshost) &&
4282  (elink->IsConnected()) &&
4283  (!elink->IsBusy()) &&
4284  (!elink->IsTunerLocked()))
4285  {
4286  encoder = elink;
4287  break;
4288  }
4289  }
4290  TVRec::s_inputsLock.unlock();
4291 
4292  if (encoder)
4293  {
4294  int retval = encoder->LockTuner();
4295 
4296  if (retval != -1)
4297  {
4298  QString msg = QString("Cardid %1 LOCKed for external use on %2.")
4299  .arg(retval).arg(pbshost);
4300  LOG(VB_GENERAL, LOG_INFO, LOC + msg);
4301 
4303  query.prepare("SELECT videodevice, audiodevice, "
4304  "vbidevice "
4305  "FROM capturecard "
4306  "WHERE cardid = :CARDID ;");
4307  query.bindValue(":CARDID", retval);
4308 
4309  if (query.exec() && query.next())
4310  {
4311  // Success
4312  strlist << QString::number(retval)
4313  << query.value(0).toString()
4314  << query.value(1).toString()
4315  << query.value(2).toString();
4316 
4317  if (m_sched)
4318  m_sched->ReschedulePlace("LockTuner");
4319 
4320  SendResponse(pbssock, strlist);
4321  return;
4322  }
4323  LOG(VB_GENERAL, LOG_ERR, LOC +
4324  "MainServer::LockTuner(): Could not find "
4325  "card info in database");
4326  }
4327  else
4328  {
4329  // Tuner already locked
4330  strlist << "-2" << "" << "" << "";
4331  SendResponse(pbssock, strlist);
4332  return;
4333  }
4334  }
4335 
4336  strlist << "-1" << "" << "" << "";
4337  SendResponse(pbssock, strlist);
4338 }
4339 
4341 {
4342  MythSocket *pbssock = pbs->getSocket();
4343  QStringList strlist;
4344  EncoderLink *encoder = nullptr;
4345 
4346  TVRec::s_inputsLock.lockForRead();
4347  auto iter = m_encoderList->constFind(cardid);
4348  if (iter == m_encoderList->constEnd())
4349  {
4350  LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleFreeTuner() " +
4351  QString("Unknown encoder: %1").arg(cardid));
4352  strlist << "FAILED";
4353  }
4354  else
4355  {
4356  encoder = *iter;
4357  encoder->FreeTuner();
4358 
4359  QString msg = QString("Cardid %1 FREED from external use on %2.")
4360  .arg(cardid).arg(pbs->getHostname());
4361  LOG(VB_GENERAL, LOG_INFO, LOC + msg);
4362 
4363  if (m_sched)
4364  m_sched->ReschedulePlace("FreeTuner");
4365 
4366  strlist << "OK";
4367  }
4368  TVRec::s_inputsLock.unlock();
4369 
4370  SendResponse(pbssock, strlist);
4371 }
4372 
4373 static bool comp_livetvorder(const InputInfo &a, const InputInfo &b)
4374 {
4375  if (a.m_liveTvOrder != b.m_liveTvOrder)
4376  return a.m_liveTvOrder < b.m_liveTvOrder;
4377  return a.m_inputId < b.m_inputId;
4378 }
4379 
4381  uint excluded_input)
4382 {
4383  LOG(VB_CHANNEL, LOG_INFO,
4384  LOC + QString("Excluding input %1")
4385  .arg(excluded_input));
4386 
4387  MythSocket *pbssock = pbs->getSocket();
4388  vector<InputInfo> busyinputs;
4389  vector<InputInfo> freeinputs;
4390  QMap<uint, QSet<uint> > groupids;
4391 
4392  // Lopp over each encoder and divide the inputs into busy and free
4393  // lists.
4394  TVRec::s_inputsLock.lockForRead();
4395  for (auto * elink : qAsConst(*m_encoderList))
4396  {
4397  InputInfo info;
4398  info.m_inputId = elink->GetInputID();
4399 
4400  if (!elink->IsConnected() || elink->IsTunerLocked())
4401  {
4402  LOG(VB_CHANNEL, LOG_INFO,
4403  LOC + QString("Input %1 is locked or not connected")
4404  .arg(info.m_inputId));
4405  continue;
4406  }
4407 
4408  vector<uint> infogroups;
4409  CardUtil::GetInputInfo(info, &infogroups);
4410  for (uint group : infogroups)
4411  groupids[info.m_inputId].insert(group);
4412 
4413  InputInfo busyinfo;
4414  if (info.m_inputId != excluded_input && elink->IsBusy(&busyinfo))
4415  {
4416  LOG(VB_CHANNEL, LOG_DEBUG,
4417  LOC + QString("Input %1 is busy on %2/%3")
4418  .arg(info.m_inputId).arg(busyinfo.m_chanId).arg(busyinfo.m_mplexId));
4419  info.m_chanId = busyinfo.m_chanId;
4420  info.m_mplexId = busyinfo.m_mplexId;
4421  busyinputs.push_back(info);
4422  }
4423  else if (info.m_liveTvOrder)
4424  {
4425  LOG(VB_CHANNEL, LOG_DEBUG,
4426  LOC + QString("Input %1 is free")
4427  .arg(info.m_inputId));
4428  freeinputs.push_back(info);
4429  }
4430  }
4431  TVRec::s_inputsLock.unlock();
4432 
4433  // Loop over each busy input and restrict or delete any free
4434  // inputs that are in the same group.
4435  for (auto & busyinfo : busyinputs)
4436  {
4437  auto freeiter = freeinputs.begin();
4438  while (freeiter != freeinputs.end())
4439  {
4440  InputInfo &freeinfo = *freeiter;
4441 
4442  if ((groupids[busyinfo.m_inputId] & groupids[freeinfo.m_inputId])
4443  .isEmpty())
4444  {
4445  ++freeiter;
4446  continue;
4447  }
4448 
4449  if (busyinfo.m_sourceId == freeinfo.m_sourceId)
4450  {
4451  LOG(VB_CHANNEL, LOG_DEBUG,
4452  LOC + QString("Input %1 is limited to %2/%3 by input %4")
4453  .arg(freeinfo.m_inputId).arg(busyinfo.m_chanId)
4454  .arg(busyinfo.m_mplexId).arg(busyinfo.m_inputId));
4455  freeinfo.m_chanId = busyinfo.m_chanId;
4456  freeinfo.m_mplexId = busyinfo.m_mplexId;
4457  ++freeiter;
4458  continue;
4459  }
4460 
4461  LOG(VB_CHANNEL, LOG_DEBUG,
4462  LOC + QString("Input %1 is unavailable by input %2")
4463  .arg(freeinfo.m_inputId).arg(busyinfo.m_inputId));
4464  freeiter = freeinputs.erase(freeiter);
4465  }
4466  }
4467 
4468  // Return the results in livetvorder.
4469  stable_sort(freeinputs.begin(), freeinputs.end(), comp_livetvorder);
4470  QStringList strlist;
4471  for (auto & input : freeinputs)
4472  {
4473  LOG(VB_CHANNEL, LOG_INFO,
4474  LOC + QString("Input %1 is available on %2/%3")
4475  .arg(input.m_inputId).arg(input.m_chanId)
4476  .arg(input.m_mplexId));
4477  input.ToStringList(strlist);
4478  }
4479 
4480  if (strlist.empty())
4481  strlist << "OK";
4482 
4483  SendResponse(pbssock, strlist);
4484 }
4485 
4486 static QString cleanup(const QString &str)
4487 {
4488  if (str == " ")
4489  return "";
4490  return str;
4491 }
4492 
4493 static QString make_safe(const QString &str)
4494 {
4495  if (str.isEmpty())
4496  return " ";
4497  return str;
4498 }
4499 
4500 void MainServer::HandleRecorderQuery(QStringList &slist, QStringList &commands,
4501  PlaybackSock *pbs)
4502 {
4503  MythSocket *pbssock = pbs->getSocket();
4504 
4505  if (commands.size() < 2 || slist.size() < 2)
4506  return;
4507 
4508  int recnum = commands[1].toInt();
4509 
4510  TVRec::s_inputsLock.lockForRead();
4511  auto iter = m_encoderList->constFind(recnum);
4512  if (iter == m_encoderList->constEnd())
4513  {
4514  TVRec::s_inputsLock.unlock();
4515  LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleRecorderQuery() " +
4516  QString("Unknown encoder: %1").arg(recnum));
4517  QStringList retlist( "bad" );
4518  SendResponse(pbssock, retlist);
4519  return;
4520  }
4521  TVRec::s_inputsLock.unlock();
4522 
4523  QString command = slist[1];
4524 
4525  QStringList retlist;
4526 
4527  EncoderLink *enc = *iter;
4528  if (!enc->IsConnected())
4529  {
4530  LOG(VB_GENERAL, LOG_ERR, LOC + " MainServer::HandleRecorderQuery() " +
4531  QString("Command %1 for unconnected encoder %2")
4532  .arg(command).arg(recnum));
4533  retlist << "bad";
4534  SendResponse(pbssock, retlist);
4535  return;
4536  }
4537 
4538  if (command == "IS_RECORDING")
4539  {
4540  retlist << QString::number((int)enc->IsReallyRecording());
4541  }
4542  else if (command == "GET_FRAMERATE")
4543  {
4544  retlist << QString::number(enc->GetFramerate());
4545  }
4546  else if (command == "GET_FRAMES_WRITTEN")
4547  {
4548  retlist << QString::number(enc->GetFramesWritten());
4549  }
4550  else if (command == "GET_FILE_POSITION")
4551  {
4552  retlist << QString::number(enc->GetFilePosition());
4553  }
4554  else if (command == "GET_MAX_BITRATE")
4555  {
4556  retlist << QString::number(enc->GetMaxBitrate());
4557  }
4558  else if (command == "GET_CURRENT_RECORDING")
4559  {
4560  ProgramInfo *info = enc->GetRecording();
4561  if (info)
4562  {
4563  info->ToStringList(retlist);
4564  delete info;
4565  }
4566  else
4567  {
4568  ProgramInfo dummy;
4569  dummy.SetInputID(enc->GetInputID());
4570  dummy.ToStringList(retlist);
4571  }
4572  }
4573  else if (command == "GET_KEYFRAME_POS")
4574  {
4575  long long desired = slist[2].toLongLong();
4576  retlist << QString::number(enc->GetKeyframePosition(desired));
4577  }
4578  else if (command == "FILL_POSITION_MAP")
4579  {
4580  int64_t start = slist[2].toLongLong();
4581  int64_t end = slist[3].toLongLong();
4582  frm_pos_map_t map;
4583 
4584  if (!enc->GetKeyframePositions(start, end, map))
4585  {
4586  retlist << "error";
4587  }
4588  else
4589  {
4590  for (auto it = map.cbegin(); it != map.cend(); ++it)
4591  {
4592  retlist += QString::number(it.key());
4593  retlist += QString::number(*it);
4594  }
4595  if (retlist.empty())
4596  retlist << "OK";
4597  }
4598  }
4599  else if (command == "FILL_DURATION_MAP")
4600  {
4601  int64_t start = slist[2].toLongLong();
4602  int64_t end = slist[3].toLongLong();
4603  frm_pos_map_t map;
4604 
4605  if (!enc->GetKeyframeDurations(start, end, map))
4606  {
4607  retlist << "error";
4608  }
4609  else
4610  {
4611  for (auto it = map.cbegin(); it != map.cend(); ++it)
4612  {
4613  retlist += QString::number(it.key());
4614  retlist += QString::number(*it);
4615  }
4616  if (retlist.empty())
4617  retlist << "OK";
4618  }
4619  }
4620  else if (command == "GET_RECORDING")
4621  {
4622  ProgramInfo *pginfo = enc->GetRecording();
4623  if (pginfo)
4624  {
4625  pginfo->ToStringList(retlist);
4626  delete pginfo;
4627  }
4628  else
4629  {
4630  ProgramInfo dummy;
4631  dummy.SetInputID(enc->GetInputID());
4632  dummy.ToStringList(retlist);
4633  }
4634  }
4635  else if (command == "FRONTEND_READY")
4636  {
4637  enc->FrontendReady();
4638  retlist << "OK";
4639  }
4640  else if (command == "CANCEL_NEXT_RECORDING")
4641  {
4642  QString cancel = slist[2];
4643  LOG(VB_GENERAL, LOG_NOTICE, LOC +
4644  QString("Received: CANCEL_NEXT_RECORDING %1").arg(cancel));
4645  enc->CancelNextRecording(cancel == "1");
4646  retlist << "OK";
4647  }
4648  else if (command == "SPAWN_LIVETV")
4649  {
4650  QString chainid = slist[2];
4651  LiveTVChain *chain = GetExistingChain(chainid);
4652  if (!chain)
4653  {
4654  chain = new LiveTVChain();
4655  chain->LoadFromExistingChain(chainid);
4656  AddToChains(chain);
4657  }
4658 
4659  chain->SetHostSocket(pbssock);
4660 
4661  enc->SpawnLiveTV(chain, slist[3].toInt() != 0, slist[4]);
4662  retlist << "OK";
4663  }
4664  else if (command == "STOP_LIVETV")
4665  {
4666  QString chainid = enc->GetChainID();
4667  enc->StopLiveTV();
4668 
4669  LiveTVChain *chain = GetExistingChain(chainid);
4670  if (chain)
4671  {
4672  chain->DelHostSocket(pbssock);
4673  if (chain->HostSocketCount() == 0)
4674  {
4675  DeleteChain(chain);
4676  }
4677  }
4678 
4679  retlist << "OK";
4680  }
4681  else if (command == "PAUSE")
4682  {
4683  enc->PauseRecorder();
4684  retlist << "OK";
4685  }
4686  else if (command == "FINISH_RECORDING")
4687  {
4688  enc->FinishRecording();
4689  retlist << "OK";
4690  }
4691  else if (command == "SET_LIVE_RECORDING")
4692  {
4693  int recording = slist[2].toInt();
4694  enc->SetLiveRecording(recording);
4695  retlist << "OK";
4696  }
4697  else if (command == "GET_INPUT")
4698  {
4699  QString ret = enc->GetInput();
4700  ret = (ret.isEmpty()) ? "UNKNOWN" : ret;
4701  retlist << ret;
4702  }
4703  else if (command == "SET_INPUT")
4704  {
4705  QString input = slist[2];
4706  QString ret = enc->SetInput(input);
4707  ret = (ret.isEmpty()) ? "UNKNOWN" : ret;
4708  retlist << ret;
4709  }
4710  else if (command == "TOGGLE_CHANNEL_FAVORITE")
4711  {
4712  QString changroup = slist[2];
4713  enc->ToggleChannelFavorite(changroup);
4714  retlist << "OK";
4715  }
4716  else if (command == "CHANGE_CHANNEL")
4717  {
4718  auto direction = (ChannelChangeDirection) slist[2].toInt();
4719  enc->ChangeChannel(direction);
4720  retlist << "OK";
4721  }
4722  else if (command == "SET_CHANNEL")
4723  {
4724  QString name = slist[2];
4725  enc->SetChannel(name);
4726  retlist << "OK";
4727  }
4728  else if (command == "SET_SIGNAL_MONITORING_RATE")
4729  {
4730  auto rate = std::chrono::milliseconds(slist[2].toInt());
4731  int notifyFrontend = slist[3].toInt();
4732  auto oldrate = enc->SetSignalMonitoringRate(rate, notifyFrontend);
4733  retlist << QString::number(oldrate.count());
4734  }
4735  else if (command == "GET_COLOUR")
4736  {
4738  retlist << QString::number(ret);
4739  }
4740  else if (command == "GET_CONTRAST")
4741  {
4743  retlist << QString::number(ret);
4744  }
4745  else if (command == "GET_BRIGHTNESS")
4746  {
4748  retlist << QString::number(ret);
4749  }
4750  else if (command == "GET_HUE")
4751  {
4753  retlist << QString::number(ret);
4754  }
4755  else if (command == "CHANGE_COLOUR")
4756  {
4757  int type = slist[2].toInt();
4758  bool up = slist[3].toInt() != 0;
4759  int ret = enc->ChangePictureAttribute(
4761  retlist << QString::number(ret);
4762  }
4763  else if (command == "CHANGE_CONTRAST")
4764  {
4765  int type = slist[2].toInt();
4766  bool up = slist[3].toInt() != 0;
4767  int ret = enc->ChangePictureAttribute(
4769  retlist << QString::number(ret);
4770  }
4771  else if (command == "CHANGE_BRIGHTNESS")
4772  {
4773  int type= slist[2].toInt();
4774  bool up = slist[3].toInt() != 0;
4775  int ret = enc->ChangePictureAttribute(
4777  retlist << QString::number(ret);
4778  }
4779  else if (command == "CHANGE_HUE")
4780  {
4781  int type= slist[2].toInt();
4782  bool up = slist[3].toInt() != 0;
4783  int ret = enc->ChangePictureAttribute(
4785  retlist << QString::number(ret);
4786  }
4787  else if (command == "CHECK_CHANNEL")
4788  {
4789  QString name = slist[2];
4790  retlist << QString::number((int)(enc->CheckChannel(name)));
4791  }
4792  else if (command == "SHOULD_SWITCH_CARD")
4793  {
4794  QString chanid = slist[2];
4795  retlist << QString::number((int)(enc->ShouldSwitchToAnotherInput(chanid)));
4796  }
4797  else if (command == "CHECK_CHANNEL_PREFIX")
4798  {
4799  QString needed_spacer;
4800  QString prefix = slist[2];
4801  uint complete_valid_channel_on_rec = 0;
4802  bool is_extra_char_useful = false;
4803 
4804  bool match = enc->CheckChannelPrefix(
4805  prefix, complete_valid_channel_on_rec,
4806  is_extra_char_useful, needed_spacer);
4807 
4808  retlist << QString::number((int)match);
4809  retlist << QString::number(complete_valid_channel_on_rec);
4810  retlist << QString::number((int)is_extra_char_useful);
4811  retlist << ((needed_spacer.isEmpty()) ? QString("X") : needed_spacer);
4812  }
4813  else if (command == "GET_NEXT_PROGRAM_INFO" && (slist.size() >= 6))
4814  {
4815  QString channelname = slist[2];
4816  uint chanid = slist[3].toUInt();
4817  auto direction = (BrowseDirection)slist[4].toInt();
4818  QString starttime = slist[5];
4819 
4820  QString title = "";
4821  QString subtitle = "";
4822  QString desc = "";
4823  QString category = "";
4824  QString endtime = "";
4825  QString callsign = "";
4826  QString iconpath = "";
4827  QString seriesid = "";
4828  QString programid = "";
4829 
4830  enc->GetNextProgram(direction,
4831  title, subtitle, desc, category, starttime,
4832  endtime, callsign, iconpath, channelname, chanid,
4833  seriesid, programid);
4834 
4835  retlist << make_safe(title);
4836  retlist << make_safe(subtitle);
4837  retlist << make_safe(desc);
4838  retlist << make_safe(category);
4839  retlist << make_safe(starttime);
4840  retlist << make_safe(endtime);
4841  retlist << make_safe(callsign);
4842  retlist << make_safe(iconpath);
4843  retlist << make_safe(channelname);
4844  retlist << QString::number(chanid);
4845  retlist << make_safe(seriesid);
4846  retlist << make_safe(programid);
4847  }
4848  else if (command == "GET_CHANNEL_INFO")
4849  {
4850  uint chanid = slist[2].toUInt();
4851  uint sourceid = 0;
4852  QString callsign = "";
4853  QString channum = "";
4854  QString channame = "";
4855  QString xmltv = "";
4856 
4857  enc->GetChannelInfo(chanid, sourceid,
4858  callsign, channum, channame, xmltv);
4859 
4860  retlist << QString::number(chanid);
4861  retlist << QString::number(sourceid);
4862  retlist << make_safe(callsign);
4863  retlist << make_safe(channum);
4864  retlist << make_safe(channame);
4865  retlist << make_safe(xmltv);
4866  }
4867  else
4868  {
4869  LOG(VB_GENERAL, LOG_ERR, LOC +
4870  QString("Unknown command: %1").arg(command));
4871  retlist << "OK";
4872  }
4873 
4874  SendResponse(pbssock, retlist);
4875 }
4876 
4877 void MainServer::HandleSetNextLiveTVDir(QStringList &commands,
4878  PlaybackSock *pbs)
4879 {
4880  MythSocket *pbssock = pbs->getSocket();
4881 
4882  int recnum = commands[1].toInt();
4883 
4884  TVRec::s_inputsLock.lockForRead();
4885  auto iter = m_encoderList->constFind(recnum);
4886  if (iter == m_encoderList->constEnd())
4887  {
4888  TVRec::s_inputsLock.unlock();
4889  LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleSetNextLiveTVDir() " +
4890  QString("Unknown encoder: %1").arg(recnum));
4891  QStringList retlist( "bad" );
4892  SendResponse(pbssock, retlist);
4893  return;
4894  }
4895  TVRec::s_inputsLock.unlock();
4896 
4897  EncoderLink *enc = *iter;
4898  enc->SetNextLiveTVDir(commands[2]);
4899 
4900  QStringList retlist( "OK" );
4901  SendResponse(pbssock, retlist);
4902 }
4903 
4905 {
4906  bool ok = true;
4907  MythSocket *pbssock = pbs->getSocket();
4908  uint chanid = slist[1].toUInt();
4909  uint sourceid = slist[2].toUInt();
4910  QString oldcnum = cleanup(slist[3]);
4911  QString callsign = cleanup(slist[4]);
4912  QString channum = cleanup(slist[5]);
4913  QString channame = cleanup(slist[6]);
4914  QString xmltv = cleanup(slist[7]);
4915 
4916  QStringList retlist;
4917  if (!chanid || !sourceid)
4918  {
4919  retlist << "0";
4920  SendResponse(pbssock, retlist);
4921  return;
4922  }
4923 
4924  TVRec::s_inputsLock.lockForRead();
4925  for (auto * encoder : qAsConst(*m_encoderList))
4926  {
4927  if (encoder)
4928  {
4929  ok &= encoder->SetChannelInfo(chanid, sourceid, oldcnum,
4930  callsign, channum, channame, xmltv);
4931  }
4932  }
4933  TVRec::s_inputsLock.unlock();
4934 
4935  retlist << ((ok) ? "1" : "0");
4936  SendResponse(pbssock, retlist);
4937 }
4938 
4939 void MainServer::HandleRemoteEncoder(QStringList &slist, QStringList &commands,
4940  PlaybackSock *pbs)
4941 {
4942  MythSocket *pbssock = pbs->getSocket();
4943 
4944  int recnum = commands[1].toInt();
4945  QStringList retlist;
4946 
4947  TVRec::s_inputsLock.lockForRead();
4948  auto iter = m_encoderList->constFind(recnum);
4949  if (iter == m_encoderList->constEnd())
4950  {
4951  TVRec::s_inputsLock.unlock();
4952  LOG(VB_GENERAL, LOG_ERR, LOC +
4953  QString("HandleRemoteEncoder(cmd %1) ").arg(slist[1]) +
4954  QString("Unknown encoder: %1").arg(recnum));
4955  retlist << QString::number((int) kState_Error);
4956  SendResponse(pbssock, retlist);
4957  return;
4958  }
4959  TVRec::s_inputsLock.unlock();
4960 
4961  EncoderLink *enc = *iter;
4962 
4963  QString command = slist[1];
4964 
4965  if (command == "GET_STATE")
4966  {
4967  retlist << QString::number((int)enc->GetState());
4968  }
4969  else if (command == "GET_SLEEPSTATUS")
4970  {
4971  retlist << QString::number(enc->GetSleepStatus());
4972  }
4973  else if (command == "GET_FLAGS")
4974  {
4975  retlist << QString::number(enc->GetFlags());
4976  }
4977  else if (command == "IS_BUSY")
4978  {
4979  auto arg2 = std::chrono::seconds(slist[2].toInt());
4980  std::chrono::seconds time_buffer = (slist.size() >= 3) ? arg2 : 5s;
4981  InputInfo busy_input;
4982  retlist << QString::number((int)enc->IsBusy(&busy_input, time_buffer));
4983  busy_input.ToStringList(retlist);
4984  }
4985  else if (command == "MATCHES_RECORDING" &&
4986  slist.size() >= (2 + NUMPROGRAMLINES))
4987  {
4988  QStringList::const_iterator it = slist.cbegin() + 2;
4989  ProgramInfo pginfo(it, slist.cend());
4990 
4991  retlist << QString::number((int)enc->MatchesRecording(&pginfo));
4992  }
4993  else if (command == "START_RECORDING" &&
4994  slist.size() >= (2 + NUMPROGRAMLINES))
4995  {
4996  QStringList::const_iterator it = slist.cbegin() + 2;
4997  ProgramInfo pginfo(it, slist.cend());
4998 
4999  retlist << QString::number(enc->StartRecording(&pginfo));
5000  retlist << QString::number(pginfo.GetRecordingID());
5001  retlist << QString::number(pginfo.GetRecordingStartTime().toSecsSinceEpoch());
5002  }
5003  else if (command == "GET_RECORDING_STATUS")
5004  {
5005  retlist << QString::number((int)enc->GetRecordingStatus());
5006  }
5007  else if (command == "RECORD_PENDING" &&
5008  (slist.size() >= 4 + NUMPROGRAMLINES))
5009  {
5010  auto secsleft = std::chrono::seconds(slist[2].toInt());
5011  int haslater = slist[3].toInt();
5012  QStringList::const_iterator it = slist.cbegin() + 4;
5013  ProgramInfo pginfo(it, slist.cend());
5014 
5015  enc->RecordPending(&pginfo, secsleft, haslater != 0);
5016 
5017  retlist << "OK";
5018  }
5019  else if (command == "CANCEL_NEXT_RECORDING" &&
5020  (slist.size() >= 3))
5021  {
5022  bool cancel = (bool) slist[2].toInt();
5023  enc->CancelNextRecording(cancel);
5024  retlist << "OK";
5025  }
5026  else if (command == "STOP_RECORDING")
5027  {
5028  enc->StopRecording();
5029  retlist << "OK";
5030  }
5031  else if (command == "GET_MAX_BITRATE")
5032  {
5033  retlist << QString::number(enc->GetMaxBitrate());
5034  }
5035  else if (command == "GET_CURRENT_RECORDING")
5036  {
5037  ProgramInfo *info = enc->GetRecording();
5038  if (info)
5039  {
5040  info->ToStringList(retlist);
5041  delete info;
5042  }
5043  else
5044  {
5045  ProgramInfo dummy;
5046  dummy.SetInputID(enc->GetInputID());
5047  dummy.ToStringList(retlist);
5048  }
5049  }
5050 
5051  SendResponse(pbssock, retlist);
5052 }
5053 
5054 void MainServer::GetActiveBackends(QStringList &hosts)
5055 {
5056  hosts.clear();
5057  hosts << gCoreContext->GetHostName();
5058 
5059  QString hostname;
5060  QReadLocker rlock(&m_sockListLock);
5061  for (auto & pbs : m_playbackList)
5062  {
5063  if (pbs->isMediaServer())
5064  {
5065  hostname = pbs->getHostname();
5066  if (!hosts.contains(hostname))
5067  hosts << hostname;
5068  }
5069  }
5070 }
5071 
5073 {
5074  QStringList retlist;
5075  GetActiveBackends(retlist);
5076  retlist.push_front(QString::number(retlist.size()));
5077  SendResponse(pbs->getSocket(), retlist);
5078 }
5079 
5080 void MainServer::HandleIsActiveBackendQuery(const QStringList &slist,
5081  PlaybackSock *pbs)
5082 {
5083  QStringList retlist;
5084  QString queryhostname = slist[1];
5085 
5086  if (gCoreContext->GetHostName() != queryhostname)
5087  {
5088  PlaybackSock *slave = GetSlaveByHostname(queryhostname);
5089  if (slave != nullptr)
5090  {
5091  retlist << "TRUE";
5092  slave->DecrRef();
5093  }
5094  else
5095  retlist << "FALSE";
5096  }
5097  else
5098  retlist << "TRUE";
5099 
5100  SendResponse(pbs->getSocket(), retlist);
5101 }
5102 
5103 int MainServer::GetfsID(const QList<FileSystemInfo>::iterator& fsInfo)
5104 {
5105  QString fskey = fsInfo->getHostname() + ":" + fsInfo->getPath();
5106  QMutexLocker lock(&m_fsIDcacheLock);
5107  if (!m_fsIDcache.contains(fskey))
5108  m_fsIDcache[fskey] = m_fsIDcache.count();
5109 
5110  return m_fsIDcache[fskey];
5111 }
5112 
5114 {
5115  size_t totalKBperMin = 0;
5116 
5117  TVRec::s_inputsLock.lockForRead();
5118  for (auto * enc : qAsConst(*m_encoderList))
5119  {
5120  if (!enc->IsConnected() || !enc->IsBusy())
5121  continue;
5122 
5123  long long maxBitrate = enc->GetMaxBitrate();
5124  if (maxBitrate<=0)
5125  maxBitrate = 19500000LL;
5126  long long thisKBperMin = (((size_t)maxBitrate)*((size_t)15))>>11;
5127  totalKBperMin += thisKBperMin;
5128  LOG(VB_FILE, LOG_INFO, LOC + QString("Cardid %1: max bitrate %2 KB/min")
5129  .arg(enc->GetInputID()).arg(thisKBperMin));
5130  }
5131  TVRec::s_inputsLock.unlock();
5132 
5133  LOG(VB_FILE, LOG_INFO, LOC +
5134  QString("Maximal bitrate of busy encoders is %1 KB/min")
5135  .arg(totalKBperMin));
5136 
5137  return totalKBperMin;
5138 }
5139 
5140 void MainServer::BackendQueryDiskSpace(QStringList &strlist, bool consolidated,
5141  bool allHosts)
5142 {
5143  QString allHostList = gCoreContext->GetHostName();
5144  int64_t totalKB = -1;
5145  int64_t usedKB = -1;
5146  QMap <QString, bool>foundDirs;
5147  QString localStr = "1";
5148  struct statfs statbuf {};
5149  QStringList groups(StorageGroup::kSpecialGroups);
5150  groups.removeAll("LiveTV");
5151  QString specialGroups = groups.join("', '");
5152  QString sql = QString("SELECT MIN(id),dirname "
5153  "FROM storagegroup "
5154  "WHERE hostname = :HOSTNAME "
5155  "AND groupname NOT IN ( '%1' ) "
5156  "GROUP BY dirname;").arg(specialGroups);
5158  query.prepare(sql);
5159  query.bindValue(":HOSTNAME", gCoreContext->GetHostName());
5160 
5161  if (query.exec())
5162  {
5163  // If we don't have any dirs of our own, fallback to list of Default
5164  // dirs since that is what StorageGroup::Init() does.
5165  if (!query.size())
5166  {
5167  query.prepare("SELECT MIN(id),dirname "
5168  "FROM storagegroup "
5169  "WHERE groupname = :GROUP "
5170  "GROUP BY dirname;");
5171  query.bindValue(":GROUP", "Default");
5172  if (!query.exec())
5173  MythDB::DBError("BackendQueryDiskSpace", query);
5174  }
5175 
5176  QDir checkDir("");
5177  QString dirID;
5178  QString currentDir;
5179  while (query.next())
5180  {
5181  dirID = query.value(0).toString();
5182  /* The storagegroup.dirname column uses utf8_bin collation, so Qt
5183  * uses QString::fromAscii() for toString(). Explicitly convert the
5184  * value using QString::fromUtf8() to prevent corruption. */
5185  currentDir = QString::fromUtf8(query.value(1)
5186  .toByteArray().constData());
5187  if (currentDir.endsWith("/"))
5188  currentDir.remove(currentDir.length() - 1, 1);
5189 
5190  checkDir.setPath(currentDir);
5191  if (!foundDirs.contains(currentDir))
5192  {
5193  if (checkDir.exists())
5194  {
5195  QByteArray cdir = currentDir.toLatin1();
5196  getDiskSpace(cdir.constData(), totalKB, usedKB);
5197  memset(&statbuf, 0, sizeof(statbuf));
5198  localStr = "1"; // Assume local
5199  int bSize = 0;
5200 
5201  if (statfs(currentDir.toLocal8Bit().constData(), &statbuf) == 0)
5202  {
5203 #if CONFIG_DARWIN
5204  char *fstypename = statbuf.f_fstypename;
5205  if ((!strcmp(fstypename, "nfs")) || // NFS|FTP
5206  (!strcmp(fstypename, "afpfs")) || // ApplShr
5207  (!strcmp(fstypename, "smbfs"))) // SMB
5208  localStr = "0";
5209 #elif __linux__
5210  long fstype = statbuf.f_type;
5211  if ((fstype == 0x6969) || // NFS
5212  (fstype == 0x517B) || // SMB
5213  (fstype == (long)0xFF534D42)) // CIFS
5214  localStr = "0";
5215 #endif
5216  bSize = statbuf.f_bsize;
5217  }
5218 
5219  strlist << gCoreContext->GetHostName();
5220  strlist << currentDir;
5221  strlist << localStr;
5222  strlist << "-1"; // Ignore fsID
5223  strlist << dirID;
5224  strlist << QString::number(bSize);
5225  strlist << QString::number(totalKB);
5226  strlist << QString::number(usedKB);
5227 
5228  foundDirs[currentDir] = true;
5229  }
5230  else
5231  foundDirs[currentDir] = false;
5232  }
5233  }
5234  }
5235 
5236  if (allHosts)
5237  {
5238  QMap <QString, bool> backendsCounted;
5239  std::list<PlaybackSock *> localPlaybackList;
5240 
5241  m_sockListLock.lockForRead();
5242 
5243  for (auto *pbs : m_playbackList)
5244  {
5245  if ((pbs->IsDisconnected()) ||
5246  (!pbs->isMediaServer()) ||
5247  (pbs->isLocal()) ||
5248  (backendsCounted.contains(pbs->getHostname())))
5249  continue;
5250 
5251  backendsCounted[pbs->getHostname()] = true;
5252  pbs->IncrRef();
5253  localPlaybackList.push_back(pbs);
5254  allHostList += "," + pbs->getHostname();
5255  }
5256 
5257  m_sockListLock.unlock();
5258 
5259  for (auto & pbs : localPlaybackList) {
5260  pbs->GetDiskSpace(strlist);
5261  pbs->DecrRef();
5262  }
5263  }
5264 
5265  if (!consolidated)
5266  return;
5267 
5268  QList<FileSystemInfo> fsInfos;
5269  QStringList::const_iterator it = strlist.cbegin();
5270  while (it != strlist.cend())
5271  {
5272  FileSystemInfo fsInfo;
5273 
5274  fsInfo.setHostname(*(it++));
5275  fsInfo.setPath(*(it++));
5276  fsInfo.setLocal((*(it++)).toInt() > 0);
5277  fsInfo.setFSysID(-1);
5278  ++it; // Without this, the strlist gets out of whack
5279  fsInfo.setGroupID((*(it++)).toInt());
5280  fsInfo.setBlockSize((*(it++)).toInt());
5281  fsInfo.setTotalSpace((*(it++)).toLongLong());
5282  fsInfo.setUsedSpace((*(it++)).toLongLong());
5283  fsInfos.push_back(fsInfo);
5284  }
5285  strlist.clear();
5286 
5287  // Consolidate hosts sharing storage
5288  int64_t maxWriteFiveSec = GetCurrentMaxBitrate()/12 /*5 seconds*/;
5289  maxWriteFiveSec = std::max((int64_t)2048, maxWriteFiveSec); // safety for NFS mounted dirs
5290 
5291  for (auto it1 = fsInfos.begin(); it1 != fsInfos.end(); ++it1)
5292  {
5293  if (it1->getFSysID() == -1)
5294  {
5295  it1->setFSysID(GetfsID(it1));
5296  it1->setPath(
5297  it1->getHostname().section(".", 0, 0) + ":" + it1->getPath());
5298  }
5299 
5300  for (auto it2 = it1 + 1; it2 != fsInfos.end(); ++it2)
5301  {
5302  // our fuzzy comparison uses the maximum of the two block sizes
5303  // or 32, whichever is greater
5304  int bSize = std::max(32, std::max(it1->getBlockSize(), it2->getBlockSize()) / 1024);
5305  int64_t diffSize = it1->getTotalSpace() - it2->getTotalSpace();
5306  int64_t diffUsed = it1->getUsedSpace() - it2->getUsedSpace();
5307  if (diffSize < 0)
5308  diffSize = 0 - diffSize;
5309  if (diffUsed < 0)
5310  diffUsed = 0 - diffUsed;
5311 
5312  if (it2->getFSysID() == -1 && (diffSize <= bSize) &&
5313  (diffUsed <= maxWriteFiveSec))
5314  {
5315  if (!it1->getHostname().contains(it2->getHostname()))
5316  it1->setHostname(it1->getHostname() + "," + it2->getHostname());
5317  it1->setPath(it1->getPath() + "," +
5318  it2->getHostname().section(".", 0, 0) + ":" + it2->getPath());
5319  fsInfos.erase(it2);
5320  it2 = it1;
5321  }
5322  }
5323  }
5324 
5325  // Passed the cleaned list back
5326  totalKB = 0;
5327  usedKB = 0;
5328  for (const auto & fsInfo : qAsConst(fsInfos))
5329  {
5330  strlist << fsInfo.getHostname();
5331  strlist << fsInfo.getPath();
5332  strlist << QString::number(static_cast<int>(fsInfo.isLocal()));
5333  strlist << QString::number(fsInfo.getFSysID());
5334  strlist << QString::number(fsInfo.getGroupID());
5335  strlist << QString::number(fsInfo.getBlockSize());
5336  strlist << QString::number(fsInfo.getTotalSpace());
5337  strlist << QString::number(fsInfo.getUsedSpace());
5338 
5339  totalKB += fsInfo.getTotalSpace();
5340  usedKB += fsInfo.getUsedSpace();
5341  }
5342 
5343  if (allHosts)
5344  {
5345  strlist << allHostList;
5346  strlist << "TotalDiskSpace";
5347  strlist << "0";
5348  strlist << "-2";
5349  strlist << "-2";
5350  strlist << "0";
5351  strlist << QString::number(totalKB);
5352  strlist << QString::number(usedKB);
5353  }
5354 }
5355 
5356 void MainServer::GetFilesystemInfos(QList<FileSystemInfo> &fsInfos,
5357  bool useCache)
5358 {
5359  // Return cached information if requested.
5360  if (useCache)
5361  {
5362  QMutexLocker locker(&m_fsInfosCacheLock);
5363  fsInfos = m_fsInfosCache;
5364  return;
5365  }
5366 
5367  QStringList strlist;
5368  FileSystemInfo fsInfo;
5369 
5370  fsInfos.clear();
5371 
5372  BackendQueryDiskSpace(strlist, false, true);
5373 
5374  QStringList::const_iterator it = strlist.cbegin();
5375  while (it != strlist.cend())
5376  {
5377  fsInfo.setHostname(*(it++));
5378  fsInfo.setPath(*(it++));
5379  fsInfo.setLocal((*(it++)).toInt() > 0);
5380  fsInfo.setFSysID(-1);
5381  ++it;
5382  fsInfo.setGroupID((*(it++)).toInt());
5383  fsInfo.setBlockSize((*(it++)).toInt());
5384  fsInfo.setTotalSpace((*(it++)).toLongLong());
5385  fsInfo.setUsedSpace((*(it++)).toLongLong());
5386  fsInfo.setWeight(0);
5387  fsInfos.push_back(fsInfo);
5388  }
5389 
5390  LOG(VB_SCHEDULE | VB_FILE, LOG_DEBUG, LOC +
5391  "Determining unique filesystems");
5392  size_t maxWriteFiveSec = GetCurrentMaxBitrate()/12 /*5 seconds*/;
5393  // safety for NFS mounted dirs
5394  maxWriteFiveSec = std::max((size_t)2048, maxWriteFiveSec);
5395 
5396  FileSystemInfo::Consolidate(fsInfos, false, maxWriteFiveSec);
5397 
5398  QList<FileSystemInfo>::iterator it1;
5399  if (VERBOSE_LEVEL_CHECK(VB_FILE | VB_SCHEDULE, LOG_INFO))
5400  {
5401  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5402  "--- GetFilesystemInfos directory list start ---");
5403  for (it1 = fsInfos.begin(); it1 != fsInfos.end(); ++it1)
5404  {
5405  QString msg =
5406  QString("Dir: %1:%2")
5407  .arg(it1->getHostname())
5408  .arg(it1->getPath());
5409  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC + msg) ;
5410  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5411  QString(" Location: %1")
5412  .arg(it1->isLocal() ? "Local" : "Remote"));
5413  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5414  QString(" fsID : %1")
5415  .arg(it1->getFSysID()));
5416  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5417  QString(" dirID : %1")
5418  .arg(it1->getGroupID()));
5419  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5420  QString(" BlkSize : %1")
5421  .arg(it1->getBlockSize()));
5422  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5423  QString(" TotalKB : %1")
5424  .arg(it1->getTotalSpace()));
5425  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5426  QString(" UsedKB : %1")
5427  .arg(it1->getUsedSpace()));
5428  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5429  QString(" FreeKB : %1")
5430  .arg(it1->getFreeSpace()));
5431  }
5432  LOG(VB_FILE | VB_SCHEDULE, LOG_INFO, LOC +
5433  "--- GetFilesystemInfos directory list end ---");
5434  }
5435 
5436  // Save these results to the cache.
5437  QMutexLocker locker(&m_fsInfosCacheLock);
5438  m_fsInfosCache = fsInfos;
5439 }
5440 
5441 void MainServer::HandleMoveFile(PlaybackSock *pbs, const QString &storagegroup,
5442  const QString &src, const QString &dst)
5443 {
5444  StorageGroup sgroup(storagegroup, "", false);
5445  QStringList retlist;
5446 
5447  if (src.isEmpty() || dst.isEmpty()
5448  || src.contains("..") || dst.contains(".."))
5449  {
5450  LOG(VB_GENERAL, LOG_ERR, LOC +
5451  QString("HandleMoveFile: ERROR moving file '%1' -> '%2', "
5452  "a path fails sanity checks").arg(src, dst));
5453  retlist << "0" << "Invalid path";
5454  SendResponse(pbs->getSocket(), retlist);
5455  return;
5456  }
5457 
5458  QString srcAbs = sgroup.FindFile(src);
5459  if (srcAbs.isEmpty())
5460  {
5461  LOG(VB_GENERAL, LOG_ERR, LOC +
5462  QString("HandleMoveFile: Unable to find %1").arg(src));
5463  retlist << "0" << "Source file not found";
5464  SendResponse(pbs->getSocket(), retlist);
5465  return;
5466  }
5467 
5468  // Path of files must be unique within SG. Rename will permit <sgdir1>/<dst>
5469  // even when <sgdir2>/<dst> already exists.
5470  // Directory paths do not have to be unique.
5471  QString dstAbs = sgroup.FindFile(dst);
5472  if (!dstAbs.isEmpty() && QFileInfo(dstAbs).isFile())
5473  {
5474  LOG(VB_GENERAL, LOG_ERR, LOC +
5475  QString("HandleMoveFile: Destination exists at %1").arg(dstAbs));
5476  retlist << "0" << "Destination file exists";
5477  SendResponse(pbs->getSocket(), retlist);
5478  return;
5479  }
5480 
5481  // Files never move filesystems, so use current SG dir
5482  int sgPathSize = srcAbs.size() - src.size();
5483  dstAbs = srcAbs.mid(0, sgPathSize) + dst;
5484 
5485  // Renaming on same filesystem should always be fast but is liable to delays
5486  // for unknowable reasons so we delegate to a separate thread for safety.
5487  auto *renamer = new RenameThread(*this, *pbs, srcAbs, dstAbs);
5488  MThreadPool::globalInstance()->start(renamer, "Rename");
5489 }
5490 
5492 
5494 {
5495  // Only permit one rename to run at any time
5496  QMutexLocker lock(&s_renamelock);
5497  LOG(VB_FILE, LOG_INFO, QString("MainServer::RenameThread: Renaming %1 -> %2")
5498  .arg(m_src, m_dst));
5499 
5500  QStringList retlist;
5501  QFileInfo fi(m_dst);
5502 
5503  if (QDir().mkpath(fi.path()) && QFile::rename(m_src, m_dst))
5504  {
5505  retlist << "1";
5506  }
5507  else
5508  {
5509  retlist << "0" << "Rename failed";
5510  LOG(VB_FILE, LOG_ERR, "MainServer::DoRenameThread: Rename failed");
5511  }
5512  m_ms.SendResponse(m_pbs.getSocket(), retlist);
5513 }
5514 
5516 {
5517  if (m_ms)
5518  m_ms->DoTruncateThread(this);
5519 }
5520 
5522 {
5523  if (gCoreContext->GetBoolSetting("TruncateDeletesSlowly", false))
5524  {
5525  TruncateAndClose(nullptr, ds->m_fd, ds->m_filename, ds->m_size);
5526  }
5527  else
5528  {
5529  QMutexLocker dl(&m_deletelock);
5530  close(ds->m_fd);
5531  }
5532 }
5533 
5534 bool MainServer::HandleDeleteFile(const QStringList &slist, PlaybackSock *pbs)
5535 {
5536  return HandleDeleteFile(slist[1], slist[2], pbs);
5537 }
5538 
5539 bool MainServer::HandleDeleteFile(const QString& filename, const QString& storagegroup,
5540  PlaybackSock *pbs)
5541 {
5542  StorageGroup sgroup(storagegroup, "", false);
5543  QStringList retlist;
5544 
5545  if ((filename.isEmpty()) ||
5546  (filename.contains("/../")) ||
5547  (filename.startsWith("../")))
5548  {
5549  LOG(VB_GENERAL, LOG_ERR, LOC +
5550  QString("ERROR deleting file, filename '%1' "
5551  "fails sanity checks").arg(filename));
5552  if (pbs)
5553  {
5554  retlist << "0";
5555  SendResponse(pbs->getSocket(), retlist);
5556  }
5557  return false;
5558  }
5559 
5560  QString fullfile = sgroup.FindFile(filename);
5561 
5562  if (fullfile.isEmpty()) {
5563  LOG(VB_GENERAL, LOG_ERR, LOC +
5564  QString("Unable to find %1 in HandleDeleteFile()") .arg(filename));
5565  if (pbs)
5566  {
5567  retlist << "0";
5568  SendResponse(pbs->getSocket(), retlist);
5569  }
5570  return false;
5571  }
5572 
5573  QFile checkFile(fullfile);
5574  bool followLinks = gCoreContext->GetBoolSetting("DeletesFollowLinks", false);
5575  off_t size = 0;
5576 
5577  // This will open the file and unlink the dir entry. The actual file
5578  // data will be deleted in the truncate thread spawned below.
5579  // Since stat fails after unlinking on some filesystems, get the size first
5580  const QFileInfo info(fullfile);
5581  size = info.size();
5582  int fd = DeleteFile(fullfile, followLinks);
5583 
5584  if ((fd < 0) && checkFile.exists())
5585  {
5586  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error deleting file: %1.")
5587  .arg(fullfile));
5588  if (pbs)
5589  {
5590  retlist << "0";
5591  SendResponse(pbs->getSocket(), retlist);
5592  }
5593  return false;
5594  }
5595 
5596  if (pbs)
5597  {
5598  retlist << "1";
5599  SendResponse(pbs->getSocket(), retlist);
5600  }
5601 
5602  // DeleteFile() opened up a file for us to delete
5603  if (fd >= 0)
5604  {
5605  // Thread off the actual file truncate
5606  auto *truncateThread = new TruncateThread(this, fullfile, fd, size);
5607  truncateThread->run();
5608  }
5609 
5610  return true;
5611 }
5612 
5613 // Helper function for the guts of HandleCommBreakQuery + HandleCutlistQuery
5614 void MainServer::HandleCutMapQuery(const QString &chanid,
5615  const QString &starttime,
5616  PlaybackSock *pbs, bool commbreak)
5617 {
5618  MythSocket *pbssock = nullptr;
5619  if (pbs)
5620  pbssock = pbs->getSocket();
5621 
5622  frm_dir_map_t markMap;
5623  frm_dir_map_t::const_iterator it;
5624  QDateTime recstartdt = MythDate::fromSecsSinceEpoch(starttime.toLongLong());
5625  QStringList retlist;
5626  int rowcnt = 0;
5627 
5628  const ProgramInfo pginfo(chanid.toUInt(), recstartdt);
5629 
5630  if (pginfo.GetChanID())
5631  {
5632  if (commbreak)
5633  pginfo.QueryCommBreakList(markMap);
5634  else
5635  pginfo.QueryCutList(markMap);
5636 
5637  for (it = markMap.cbegin(); it != markMap.cend(); ++it)
5638  {
5639  rowcnt++;
5640  QString intstr = QString("%1").arg(*it);
5641  retlist << intstr;
5642  retlist << QString::number(it.key());
5643  }
5644  }
5645 
5646  if (rowcnt > 0)
5647  retlist.prepend(QString("%1").arg(rowcnt));
5648  else
5649  retlist << "-1";
5650 
5651  if (pbssock)
5652  SendResponse(pbssock, retlist);
5653 }
5654 
5655 void MainServer::HandleCommBreakQuery(const QString &chanid,
5656  const QString &starttime,
5657  PlaybackSock *pbs)
5658 {
5659 // Commercial break query
5660 // Format: QUERY_COMMBREAK <chanid> <starttime>
5661 // chanid is chanid, starttime is startime of program in
5662 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5663 // a ProgramInfo structure in a string list.
5664 // Return structure is [number of rows] followed by a triplet of values:
5665 // each triplet : [type] [long portion 1] [long portion 2]
5666 // type is the value in the map, right now 4 = commbreak start, 5= end
5667  HandleCutMapQuery(chanid, starttime, pbs, true);
5668 }
5669 
5670 void MainServer::HandleCutlistQuery(const QString &chanid,
5671  const QString &starttime,
5672  PlaybackSock *pbs)
5673 {
5674 // Cutlist query
5675 // Format: QUERY_CUTLIST <chanid> <starttime>
5676 // chanid is chanid, starttime is startime of program in
5677 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5678 // a ProgramInfo structure in a string list.
5679 // Return structure is [number of rows] followed by a triplet of values:
5680 // each triplet : [type] [long portion 1] [long portion 2]
5681 // type is the value in the map, right now 0 = commbreak start, 1 = end
5682  HandleCutMapQuery(chanid, starttime, pbs, false);
5683 }
5684 
5685 
5686 void MainServer::HandleBookmarkQuery(const QString &chanid,
5687  const QString &starttime,
5688  PlaybackSock *pbs)
5689 // Bookmark query
5690 // Format: QUERY_BOOKMARK <chanid> <starttime>
5691 // chanid is chanid, starttime is startime of program in
5692 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5693 // a ProgramInfo structure in a string list.
5694 // Return value is a long-long encoded as two separate values
5695 {
5696  MythSocket *pbssock = nullptr;
5697  if (pbs)
5698  pbssock = pbs->getSocket();
5699 
5700  QDateTime recstartts = MythDate::fromSecsSinceEpoch(starttime.toLongLong());
5701  uint64_t bookmark = ProgramInfo::QueryBookmark(
5702  chanid.toUInt(), recstartts);
5703 
5704  QStringList retlist;
5705  retlist << QString::number(bookmark);
5706 
5707  if (pbssock)
5708  SendResponse(pbssock, retlist);
5709 }
5710 
5711 
5712 void MainServer::HandleSetBookmark(QStringList &tokens,
5713  PlaybackSock *pbs)
5714 {
5715 // Bookmark query
5716 // Format: SET_BOOKMARK <chanid> <starttime> <position>
5717 // chanid is chanid, starttime is startime of program in
5718 // # of seconds since Jan 1, 1970, in UTC time. Same format as in
5719 // a ProgramInfo structure in a string list. The two longs are the two
5720 // portions of the bookmark value to set.
5721 
5722  MythSocket *pbssock = nullptr;
5723  if (pbs)
5724  pbssock = pbs->getSocket();
5725 
5726  QString chanid = tokens[1];
5727  QString starttime = tokens[2];
5728  long long bookmark = tokens[3].toLongLong();
5729 
5730  QDateTime recstartts = MythDate::fromSecsSinceEpoch(starttime.toLongLong());
5731  QStringList retlist;
5732 
5733  ProgramInfo pginfo(chanid.toUInt(), recstartts);
5734 
5735  if (pginfo.GetChanID())
5736  {
5737  pginfo.SaveBookmark(bookmark);
5738  retlist << "OK";
5739  }
5740  else
5741  retlist << "FAILED";
5742 
5743  if (pbssock)
5744  SendResponse(pbssock, retlist);
5745 }
5746 
5747 void MainServer::HandleSettingQuery(const QStringList &tokens, PlaybackSock *pbs)
5748 {
5749 // Format: QUERY_SETTING <hostname> <setting>
5750 // Returns setting value as a string
5751 
5752  MythSocket *pbssock = nullptr;
5753  if (pbs)
5754  pbssock = pbs->getSocket();
5755 
5756  QString hostname = tokens[1];
5757  QString setting = tokens[2];
5758  QStringList retlist;
5759 
5760  QString retvalue = gCoreContext->GetSettingOnHost(setting, hostname, "-1");
5761 
5762  retlist << retvalue;
5763  if (pbssock)
5764  SendResponse(pbssock, retlist);
5765 }
5766 
5767 void MainServer::HandleDownloadFile(const QStringList &command,
5768  PlaybackSock *pbs)
5769 {
5770  bool synchronous = (command[0] == "DOWNLOAD_FILE_NOW");
5771  QString srcURL = command[1];
5772  QString storageGroup = command[2];
5773  QString filename = command[3];
5774  StorageGroup sgroup(storageGroup, gCoreContext->GetHostName(), false);
5775  QString outDir = sgroup.FindNextDirMostFree();
5776  QString outFile;
5777  QStringList retlist;
5778 
5779  MythSocket *pbssock = nullptr;
5780  if (pbs)
5781  pbssock = pbs->getSocket();
5782 
5783  if (filename.isEmpty())
5784  {
5785  QFileInfo finfo(srcURL);
5786  filename = finfo.fileName();
5787  }
5788 
5789  if (outDir.isEmpty())
5790  {
5791  LOG(VB_GENERAL, LOG_ERR, LOC +
5792  QString("Unable to determine directory "
5793  "to write to in %1 write command").arg(command[0]));
5794  retlist << "downloadfile_directory_not_found";
5795  if (pbssock)
5796  SendResponse(pbssock, retlist);
5797  return;
5798  }
5799 
5800  if ((filename.contains("/../")) ||
5801  (filename.startsWith("../")))
5802  {
5803  LOG(VB_GENERAL, LOG_ERR, LOC +
5804  QString("ERROR: %1 write filename '%2' does not pass "
5805  "sanity checks.") .arg(command[0]).arg(filename));
5806  retlist << "downloadfile_filename_dangerous";
5807  if (pbssock)
5808  SendResponse(pbssock, retlist);
5809  return;
5810  }
5811 
5812  outFile = outDir + "/" + filename;
5813 
5814  if (synchronous)
5815  {
5816  if (GetMythDownloadManager()->download(srcURL, outFile))
5817  {
5818  retlist << "OK";
5819  retlist << gCoreContext->GetMasterHostPrefix(storageGroup)
5820  + filename;
5821  }
5822  else
5823  retlist << "ERROR";
5824  }
5825  else
5826  {
5827  QMutexLocker locker(&m_downloadURLsLock);
5828  m_downloadURLs[outFile] =
5829  gCoreContext->GetMasterHostPrefix(storageGroup) +
5831 
5832  GetMythDownloadManager()->queueDownload(srcURL, outFile, this);
5833  retlist << "OK";
5834  retlist << gCoreContext->GetMasterHostPrefix(storageGroup) + filename;
5835  }
5836 
5837  if (pbssock)
5838  SendResponse(pbssock, retlist);
5839 }
5840 
5841 void MainServer::HandleSetSetting(const QStringList &tokens,
5842  PlaybackSock *pbs)
5843 {
5844 // Format: SET_SETTING <hostname> <setting> <value>
5845  MythSocket *pbssock = nullptr;
5846  if (pbs)
5847  pbssock = pbs->getSocket();
5848 
5849  QString hostname = tokens[1];
5850  QString setting = tokens[2];
5851  QString svalue = tokens[3];
5852  QStringList retlist;
5853 
5854  if (gCoreContext->SaveSettingOnHost(setting, svalue, hostname))
5855  retlist << "OK";
5856  else
5857  retlist << "ERROR";
5858 
5859  if (pbssock)
5860  SendResponse(pbssock, retlist);
5861 }
5862 
5864 {
5865  MythSocket *pbssock = pbs->getSocket();
5866 
5867  QStringList retlist;
5868 
5869  if (m_metadatafactory)
5870  {
5871  QStringList hosts;
5872  GetActiveBackends(hosts);
5873  m_metadatafactory->VideoScan(hosts);
5874  retlist << "OK";
5875  }
5876  else
5877  retlist << "ERROR";
5878 
5879  if (pbssock)
5880  SendResponse(pbssock, retlist);
5881 }
5882 
5883 void MainServer::HandleScanMusic(const QStringList &slist, PlaybackSock *pbs)
5884 {
5885  MythSocket *pbssock = pbs->getSocket();
5886 
5887  QStringList strlist;
5888 
5889  if (m_ismaster)
5890  {
5891  // get a list of hosts with a directory defined for the 'Music' storage group
5893  QString sql = "SELECT DISTINCT hostname "
5894  "FROM storagegroup "
5895  "WHERE groupname = 'Music'";
5896  if (!query.exec(sql) || !query.isActive())
5897  MythDB::DBError("MainServer::HandleScanMusic get host list", query);
5898  else
5899  {
5900  while(query.next())
5901  {
5902  QString hostname = query.value(0).toString();
5903 
5904  if (hostname == gCoreContext->GetHostName())
5905  {
5906  // this is the master BE with a music storage group directory defined so run the file scanner
5907  LOG(VB_GENERAL, LOG_INFO, LOC +
5908  QString("HandleScanMusic: running filescanner on master BE '%1'").arg(hostname));
5909  QScopedPointer<MythSystem> cmd(MythSystem::Create(GetAppBinDir() + "mythutil --scanmusic",
5913  }
5914  else
5915  {
5916  // found a slave BE so ask it to run the file scanner
5918  if (slave)
5919  {
5920  LOG(VB_GENERAL, LOG_INFO, LOC +
5921  QString("HandleScanMusic: asking slave '%1' to run file scanner").arg(hostname));
5922  slave->ForwardRequest(slist);
5923  slave->DecrRef();
5924  }
5925  else
5926  {
5927  LOG(VB_GENERAL, LOG_INFO, LOC +
5928  QString("HandleScanMusic: Failed to grab slave socket on '%1'").arg(hostname));
5929  }
5930  }
5931  }
5932  }
5933  }
5934  else
5935  {
5936  // must be a slave with a music storage group directory defined so run the file scanner
5937  LOG(VB_GENERAL, LOG_INFO, LOC +
5938  QString("HandleScanMusic: running filescanner on slave BE '%1'")
5940  QScopedPointer<MythSystem> cmd(MythSystem::Create(GetAppBinDir() + "mythutil --scanmusic",
5944  }
5945 
5946  strlist << "OK";
5947 
5948  if (pbssock)
5949  SendResponse(pbssock, strlist);
5950 }
5951 
5953 {
5954 // format: MUSIC_TAG_UPDATE_VOLATILE <hostname> <songid> <rating> <playcount> <lastplayed>
5955 
5956  QStringList strlist;
5957 
5958  MythSocket *pbssock = pbs->getSocket();
5959 
5960  QString hostname = slist[1];
5961 
5963  {
5964  // forward the request to the slave BE
5966  if (slave)
5967  {
5968  LOG(VB_GENERAL, LOG_INFO, LOC +
5969  QString("HandleMusicTagUpdateVolatile: asking slave '%1' to update the metadata").arg(hostname));
5970  strlist = slave->ForwardRequest(slist);
5971  slave->DecrRef();
5972 
5973  if (pbssock)
5974  SendResponse(pbssock, strlist);
5975 
5976  return;
5977  }
5978 
5979  LOG(VB_GENERAL, LOG_INFO, LOC +
5980  QString("HandleMusicTagUpdateVolatile: Failed to grab slave socket on '%1'").arg(hostname));
5981 
5982  strlist << "ERROR: slave not found";
5983 
5984  if (pbssock)
5985  SendResponse(pbssock, strlist);
5986 
5987  return;
5988  }
5989 
5990  // run mythutil to update the metadata
5991  QStringList paramList;
5992  paramList.append(QString("--songid='%1'").arg(slist[2]));
5993  paramList.append(QString("--rating='%1'").arg(slist[3]));
5994  paramList.append(QString("--playcount='%1'").arg(slist[4]));
5995  paramList.append(QString("--lastplayed='%1'").arg(slist[5]));
5996 
5997  QString command = GetAppBinDir() + "mythutil --updatemeta " + paramList.join(" ");
5998 
5999  LOG(VB_GENERAL, LOG_INFO, LOC +
6000  QString("HandleMusicTagUpdateVolatile: running %1'").arg(command));
6001  QScopedPointer<MythSystem> cmd(MythSystem::Create(command,
6005 
6006  strlist << "OK";
6007 
6008  if (pbssock)
6009  SendResponse(pbssock, strlist);
6010 }
6011 
6012 void MainServer::HandleMusicCalcTrackLen(const QStringList &slist, PlaybackSock *pbs)
6013 {
6014 // format: MUSIC_CALC_TRACK_LENGTH <hostname> <songid>
6015 
6016  QStringList strlist;
6017 
6018  MythSocket *pbssock = pbs->getSocket();
6019 
6020  QString hostname = slist[1];
6021 
6023  {
6024  // forward the request to the slave BE
6026  if (slave)
6027  {
6028  LOG(VB_GENERAL, LOG_INFO, LOC +
6029  QString("HandleMusicCalcTrackLen: asking slave '%1' to update the track length").arg(hostname));
6030  strlist = slave->ForwardRequest(slist);
6031  slave->DecrRef();
6032 
6033  if (pbssock)
6034  SendResponse(pbssock, strlist);
6035 
6036  return;
6037  }
6038 
6039  LOG(VB_GENERAL, LOG_INFO, LOC +
6040  QString("HandleMusicCalcTrackLen: Failed to grab slave socket on '%1'").arg(hostname));
6041 
6042  strlist << "ERROR: slave not found";
6043 
6044  if (pbssock)
6045  SendResponse(pbssock, strlist);
6046