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