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