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