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