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