embyLiveTVTracker.php 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. <?php
  2. trait EmbyLiveTVTrackerHomepageItem
  3. {
  4. public function embyLiveTVTrackerSettingsArray($infoOnly = false)
  5. {
  6. $homepageInformation = [
  7. 'name' => 'EmbyLiveTVTracker',
  8. 'enabled' => strpos('personal', $this->config['license']) !== false,
  9. 'image' => 'plugins/images/homepage/embyLiveTVTracker.png',
  10. 'category' => 'Media Server',
  11. 'settingsArray' => __FUNCTION__
  12. ];
  13. if ($infoOnly) {
  14. return $homepageInformation;
  15. }
  16. $homepageSettings = [
  17. 'debug' => true,
  18. 'settings' => [
  19. 'Enable' => [
  20. $this->settingsOption('enable', 'homepageEmbyLiveTVTrackerEnabled'),
  21. $this->settingsOption('auth', 'homepageEmbyLiveTVTrackerAuth'),
  22. ],
  23. 'Connection' => [
  24. $this->settingsOption('url', 'embyURL'),
  25. $this->settingsOption('token', 'embyToken'),
  26. $this->settingsOption('disable-cert-check', 'embyDisableCertCheck'),
  27. $this->settingsOption('use-custom-certificate', 'embyUseCustomCertificate'),
  28. ],
  29. 'Display Options' => [
  30. $this->settingsOption('number', 'homepageEmbyLiveTVTrackerRefresh', ['label' => 'Auto-refresh Interval (minutes)', 'min' => 1, 'max' => 60]),
  31. $this->settingsOption('switch', 'homepageEmbyLiveTVTrackerCompactView', ['label' => 'Use Compact View']),
  32. $this->settingsOption('switch', 'homepageEmbyLiveTVTrackerShowDuration', ['label' => 'Show Recording Duration']),
  33. $this->settingsOption('switch', 'homepageEmbyLiveTVTrackerShowSeriesInfo', ['label' => 'Show Series Information']),
  34. $this->settingsOption('switch', 'homepageEmbyLiveTVTrackerShowUserInfo', ['label' => 'Show User Information']),
  35. $this->settingsOption('number', 'homepageEmbyLiveTVTrackerMaxItems', ['label' => 'Maximum Scheduled Items', 'min' => 5, 'max' => 50]),
  36. $this->settingsOption('switch', 'homepageEmbyLiveTVTrackerShowCompleted', ['label' => 'Show Completed Recordings']),
  37. $this->settingsOption('number', 'homepageEmbyLiveTVTrackerDaysShown', ['label' => 'Days of Completed Recordings', 'min' => 1, 'max' => 30]),
  38. $this->settingsOption('number', 'homepageEmbyLiveTVTrackerMaxCompletedItems', ['label' => 'Maximum Completed Items', 'min' => 5, 'max' => 50]),
  39. $this->settingsOption('switch', 'homepageEmbyLiveTVTrackerDebug', ['label' => 'Enable Debug Logging']),
  40. ],
  41. 'Test Connection' => [
  42. $this->settingsOption('blank', null, ['label' => 'Please Save before Testing']),
  43. $this->settingsOption('test', 'embyLiveTVTracker'),
  44. ]
  45. ]
  46. ];
  47. return array_merge($homepageInformation, $homepageSettings);
  48. }
  49. public function testConnectionEmbyLiveTVTracker()
  50. {
  51. if (!$this->homepageItemPermissions($this->embyLiveTVTrackerHomepagePermissions('test'), true)) {
  52. return false;
  53. }
  54. $url = $this->qualifyURL($this->config['embyURL']);
  55. $url = $url . "/emby/System/Info?api_key=" . $this->config['embyToken'];
  56. $options = $this->requestOptions($url, null, $this->config['embyDisableCertCheck'], $this->config['embyUseCustomCertificate']);
  57. try {
  58. $response = Requests::get($url, [], $options);
  59. if ($response->success) {
  60. $info = json_decode($response->body, true);
  61. if (isset($info['ServerName'])) {
  62. // Test LiveTV functionality
  63. $liveTvUrl = $this->qualifyURL($this->config['embyURL']) . '/emby/LiveTv/Info?api_key=' . $this->config['embyToken'];
  64. try {
  65. $liveTvResponse = Requests::get($liveTvUrl, [], $options);
  66. $liveTvInfo = json_decode($liveTvResponse->body, true);
  67. $hasLiveTV = isset($liveTvInfo['Services']) && count($liveTvInfo['Services']) > 0;
  68. $message = 'Successfully connected to ' . $info['ServerName'];
  69. if ($hasLiveTV) {
  70. $message .= ' with LiveTV support enabled';
  71. } else {
  72. $message .= ' (Warning: LiveTV may not be configured)';
  73. }
  74. $this->setAPIResponse('success', $message, 200);
  75. } catch (Exception $e) {
  76. $this->setAPIResponse('success', 'Connected to ' . $info['ServerName'] . ' but LiveTV status unknown', 200);
  77. }
  78. } else {
  79. $this->setAPIResponse('error', 'Invalid response from Emby server', 500);
  80. }
  81. return true;
  82. } else {
  83. $this->setAPIResponse('error', 'Emby Connection Error', 500);
  84. return false;
  85. }
  86. } catch (Requests_Exception $e) {
  87. $this->setResponse(500, $e->getMessage());
  88. return false;
  89. }
  90. }
  91. public function embyLiveTVTrackerHomepagePermissions($key = null)
  92. {
  93. $permissions = [
  94. 'test' => [
  95. 'enabled' => [
  96. 'homepageEmbyLiveTVTrackerEnabled',
  97. ],
  98. 'auth' => [
  99. 'homepageEmbyLiveTVTrackerAuth',
  100. ],
  101. 'not_empty' => [
  102. 'embyURL',
  103. 'embyToken'
  104. ]
  105. ],
  106. 'main' => [
  107. 'enabled' => [
  108. 'homepageEmbyLiveTVTrackerEnabled'
  109. ],
  110. 'auth' => [
  111. 'homepageEmbyLiveTVTrackerAuth'
  112. ],
  113. 'not_empty' => [
  114. 'embyURL',
  115. 'embyToken'
  116. ]
  117. ]
  118. ];
  119. return $this->homepageCheckKeyPermissions($key, $permissions);
  120. }
  121. public function homepageOrderEmbyLiveTVTracker()
  122. {
  123. if ($this->homepageItemPermissions($this->embyLiveTVTrackerHomepagePermissions('main'))) {
  124. $refreshInterval = ($this->config['homepageEmbyLiveTVTrackerRefresh'] ?? 5) * 60000; // Convert minutes to milliseconds
  125. $compactView = ($this->config['homepageEmbyLiveTVTrackerCompactView'] ?? false) ? 'true' : 'false';
  126. $showDuration = ($this->config['homepageEmbyLiveTVTrackerShowDuration'] ?? true) ? 'true' : 'false';
  127. $showSeriesInfo = ($this->config['homepageEmbyLiveTVTrackerShowSeriesInfo'] ?? true) ? 'true' : 'false';
  128. $showUserInfo = ($this->config['homepageEmbyLiveTVTrackerShowUserInfo'] ?? false) ? 'true' : 'false';
  129. $maxItems = $this->config['homepageEmbyLiveTVTrackerMaxItems'] ?? 10;
  130. $showCompleted = ($this->config['homepageEmbyLiveTVTrackerShowCompleted'] ?? true) ? 'true' : 'false';
  131. $daysShown = $this->config['homepageEmbyLiveTVTrackerDaysShown'] ?? 7;
  132. $maxCompletedItems = $this->config['homepageEmbyLiveTVTrackerMaxCompletedItems'] ?? 5;
  133. $panelClass = ($compactView === 'true') ? 'panel-compact' : '';
  134. $statsClass = ($compactView === 'true') ? 'col-sm-6' : 'col-sm-3';
  135. return '
  136. <div id="' . __FUNCTION__ . '">
  137. <div class="white-box ' . $panelClass . '">
  138. <div class="white-box-header">
  139. <i class="fa fa-tv"></i> Emby LiveTV Tracker
  140. <span class="pull-right">
  141. <small id="embylivetv-last-update" class="text-muted"></small>
  142. <button class="btn btn-xs btn-primary" onclick="refreshEmbyLiveTVData()" title="Refresh Data">
  143. <i class="fa fa-refresh" id="embylivetv-refresh-icon"></i>
  144. </button>
  145. </span>
  146. </div>
  147. <div class="white-box-content">
  148. <div class="row" id="embylivetv-stats">
  149. <div class="' . $statsClass . '">
  150. <div class="text-center">
  151. <h3 id="embylivetv-active-timers" class="text-success">-</h3>
  152. <small>Active Timers</small>
  153. </div>
  154. </div>
  155. <div class="' . $statsClass . '">
  156. <div class="text-center">
  157. <h3 id="embylivetv-series-timers" class="text-info">-</h3>
  158. <small>Series Timers</small>
  159. </div>
  160. </div>
  161. ' . (($compactView === 'false') ? '
  162. <div class="' . $statsClass . '">
  163. <div class="text-center">
  164. <h3 id="embylivetv-today-recordings" class="text-warning">-</h3>
  165. <small>Today\'s Recordings</small>
  166. </div>
  167. </div>
  168. <div class="' . $statsClass . '">
  169. <div class="text-center">
  170. <h3 id="embylivetv-total-recordings" class="text-primary">-</h3>
  171. <small>Total Recordings</small>
  172. </div>
  173. </div>
  174. ' : '') . '
  175. </div>
  176. <!-- Scheduled Recordings Table -->
  177. <div class="row" style="margin-top: 20px;">
  178. <div class="col-lg-12">
  179. <h4>
  180. Scheduled Recordings
  181. <small class="text-muted">Upcoming and active timers</small>
  182. </h4>
  183. <div class="table-responsive">
  184. <table class="table table-hover table-striped table-condensed">
  185. <thead>
  186. <tr>
  187. <th width="120">Date</th>
  188. <th>Series</th>
  189. ' . (($showSeriesInfo === 'true') ? '<th>Episode Title</th>' : '') . '
  190. <th>Channel</th>
  191. ' . (($showUserInfo === 'true') ? '<th>User</th>' : '') . '
  192. ' . (($showDuration === 'true') ? '<th width="80">Duration</th>' : '') . '
  193. <th width="70">Status</th>
  194. </tr>
  195. </thead>
  196. <tbody id="embylivetv-scheduled-table">
  197. <tr>
  198. <td colspan="' . (4 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showUserInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '" class="text-center">
  199. <i class="fa fa-spinner fa-spin"></i> Loading...
  200. </td>
  201. </tr>
  202. </tbody>
  203. </table>
  204. </div>
  205. </div>
  206. </div>
  207. <!-- Completed Recordings Table (only if enabled) -->
  208. ' . (($showCompleted === 'true') ? '
  209. <div class="row" style="margin-top: 20px;">
  210. <div class="col-lg-12">
  211. <h4>
  212. Completed Recordings
  213. <small class="text-muted">Recent recordings</small>
  214. </h4>
  215. <div class="table-responsive">
  216. <table class="table table-hover table-striped table-condensed">
  217. <thead>
  218. <tr>
  219. <th width="120">Date</th>
  220. <th>Series</th>
  221. ' . (($showSeriesInfo === 'true') ? '<th>Series</th>' : '') . '
  222. ' . (($showDuration === 'true') ? '<th width="80">Duration</th>' : '') . '
  223. <th width="70">Status</th>
  224. </tr>
  225. </thead>
  226. <tbody id="embylivetv-completed-table">
  227. <tr>
  228. <td colspan="' . (3 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '" class="text-center">
  229. <i class="fa fa-spinner fa-spin"></i> Loading...
  230. </td>
  231. </tr>
  232. </tbody>
  233. </table>
  234. </div>
  235. </div>
  236. </div>
  237. ' : '') . '
  238. </div>
  239. </div>
  240. </div>
  241. <style>
  242. .panel-compact .white-box-content { padding: 10px; }
  243. .panel-compact h3 { margin: 5px 0; font-size: 1.8em; }
  244. .panel-compact small { font-size: 0.85em; }
  245. #' . __FUNCTION__ . ' .table-condensed td { padding: 4px 8px; font-size: 0.9em; }
  246. #' . __FUNCTION__ . ' .status-success { color: #5cb85c; }
  247. #' . __FUNCTION__ . ' .status-recording { color: #d9534f; }
  248. #' . __FUNCTION__ . ' .status-scheduled { color: #f0ad4e; }
  249. </style>
  250. <script>
  251. var embyLiveTVRefreshTimer;
  252. var embyLiveTVLastRefresh = 0;
  253. function refreshEmbyLiveTVData() {
  254. var refreshIcon = $("#embylivetv-refresh-icon");
  255. refreshIcon.addClass("fa-spin");
  256. // Show loading state
  257. $("#embylivetv-stats h3").text("-");
  258. $("#embylivetv-scheduled-table").html("<tr><td colspan=\"' . (4 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showUserInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '\" class=\"text-center\"><i class=\"fa fa-spinner fa-spin\"></i> Loading...</td></tr>");
  259. ' . (($showCompleted === 'true') ? '$("#embylivetv-completed-table").html("<tr><td colspan=\"' . (4 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showUserInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '\" class=\"text-center\"><i class=\"fa fa-spinner fa-spin\"></i> Loading...</td></tr>");' : '') . ';
  260. // Load stats and activity
  261. homepageEmbyLiveTVTrackerStats()
  262. .always(function() {
  263. refreshIcon.removeClass("fa-spin");
  264. embyLiveTVLastRefresh = Date.now();
  265. updateEmbyLiveTVLastRefreshTime();
  266. });
  267. }
  268. function updateEmbyLiveTVLastRefreshTime() {
  269. if (embyLiveTVLastRefresh > 0) {
  270. var ago = Math.floor((Date.now() - embyLiveTVLastRefresh) / 1000);
  271. var timeText = ago < 60 ? ago + "s ago" : Math.floor(ago / 60) + "m ago";
  272. $("#embylivetv-last-update").text("Updated " + timeText);
  273. }
  274. }
  275. function homepageEmbyLiveTVTrackerStats() {
  276. return organizrAPI2("GET", "api/v2/homepage/embyLiveTVTracker/stats")
  277. .done(function(data) {
  278. console.log("Stats response received:", data);
  279. if (data && data.response && data.response.result === "success" && data.response.data) {
  280. console.log("Stats data is valid, loading activity...");
  281. $("#embylivetv-active-timers").text(data.response.data.activeTimers || "0");
  282. $("#embylivetv-series-timers").text(data.response.data.seriesTimers || "0");
  283. ' . (($compactView === 'false') ? '
  284. $("#embylivetv-today-recordings").text(data.response.data.todaysRecordings || "0");
  285. $("#embylivetv-total-recordings").text(data.response.data.totalRecordings || "0");
  286. ' : '') . '
  287. // Load activity
  288. homepageEmbyLiveTVTrackerActivity();
  289. } else {
  290. console.error("Stats response structure issue:", {
  291. hasData: !!data,
  292. hasResponse: !!(data && data.response),
  293. result: data && data.response && data.response.result,
  294. hasResponseData: !!(data && data.response && data.response.data)
  295. });
  296. console.error("Failed to load Emby LiveTV stats:", data.response ? data.response.message : "Unknown error");
  297. $("#embylivetv-stats h3").text("?").attr("title", "Error loading data");
  298. }
  299. })
  300. .fail(function(xhr, status, error) {
  301. console.error("Error loading Emby LiveTV stats:", error);
  302. $("#embylivetv-stats h3").text("!").attr("title", "Connection failed");
  303. });
  304. }
  305. function homepageEmbyLiveTVTrackerActivity() {
  306. console.log("Activity function called - making API request...");
  307. return organizrAPI2("GET", "api/v2/homepage/embyLiveTVTracker/activity?days=' . ($daysShown ?: 7) . '\u0026limit=' . ($maxItems ?: 10) . '")
  308. .done(function(data) {
  309. console.log("Activity response received:", data);
  310. console.log("Response structure check:", {
  311. hasData: !!data,
  312. hasResponse: !!(data && data.response),
  313. result: data && data.response && data.response.result,
  314. hasResponseData: !!(data && data.response && data.response.data),
  315. hasActivities: !!(data && data.response && data.response.data && data.response.data.activities),
  316. activitiesLength: data && data.response && data.response.data && data.response.data.activities ? data.response.data.activities.length : 0
  317. });
  318. if (data && data.response && data.response.result === "success" && data.response.data) {
  319. var scheduledRecordings = data.response.data.scheduledRecordings || [];
  320. var completedRecordings = data.response.data.completedRecordings || [];
  321. console.log("Scheduled recordings:", scheduledRecordings.length);
  322. console.log("Completed recordings:", completedRecordings.length);
  323. // Apply limits to each category separately to ensure we show both types
  324. var maxScheduled = Math.floor(' . $maxItems . ' * 0.7); // 70% for scheduled
  325. var maxCompleted = ' . $maxItems . ' - maxScheduled; // 30% for completed
  326. // If we have fewer scheduled than the 70% allocation, give more space to completed
  327. if (scheduledRecordings.length < maxScheduled) {
  328. maxCompleted = Math.min(completedRecordings.length, ' . $maxItems . ' - scheduledRecordings.length);
  329. }
  330. // If we have fewer completed than the 30% allocation, give more space to scheduled
  331. if (completedRecordings.length < maxCompleted) {
  332. maxScheduled = Math.min(scheduledRecordings.length, ' . $maxItems . ' - completedRecordings.length);
  333. }
  334. var scheduledActivities = scheduledRecordings.slice(0, maxScheduled);
  335. var completedActivities = completedRecordings.slice(0, maxCompleted);
  336. console.log("Split - Scheduled activities:", scheduledActivities.length);
  337. console.log("Split - Completed activities:", completedActivities.length);
  338. // Helper function to format scheduled activity rows
  339. function formatScheduledRow(activity) {
  340. var date = new Date(activity.date);
  341. var formattedDate = date.toLocaleDateString() + " " + date.toLocaleTimeString([], {hour: "2-digit", minute:"2-digit"});
  342. var status = activity.status || "Scheduled";
  343. var statusClass = status.toLowerCase() === "completed" ? "status-success" :
  344. status.toLowerCase() === "recording" ? "status-recording" : "status-scheduled";
  345. return "<tr>" +
  346. "<td><small>" + formattedDate + "</small></td>" +
  347. "<td>" + (activity.seriesName || activity.name || "-") + "</td>" +
  348. ' . (($showSeriesInfo === 'true') ? '"<td><small>" + (activity.episodeTitle || activity.name || "-") + "</small></td>" +' : '') . '
  349. "<td><small>" + (activity.channelName || "-") + "</small></td>" +
  350. ' . (($showUserInfo === 'true') ? '"<td><small>" + (activity.userName || "-") + "</small></td>" +' : '') . '
  351. ' . (($showDuration === 'true') ? '"<td><small>" + (activity.duration || "-") + "</small></td>" +' : '') . '
  352. "<td><span class=\"" + statusClass + "\"><i class=\"fa fa-circle\"></i></span></td>" +
  353. "</tr>";
  354. }
  355. // Helper function to format completed activity rows (no channel or user columns)
  356. function formatCompletedRow(activity) {
  357. var date = new Date(activity.date);
  358. var formattedDate = date.toLocaleDateString() + " " + date.toLocaleTimeString([], {hour: "2-digit", minute:"2-digit"});
  359. var status = activity.status || "Completed";
  360. var statusClass = "status-success";
  361. return "<tr>" +
  362. "<td><small>" + formattedDate + "</small></td>" +
  363. "<td>" + (activity.seriesName || activity.name || "-") + "</td>" +
  364. ' . (($showSeriesInfo === 'true') ? '"<td><small>" + (activity.seriesName || "-") + "</small></td>" +' : '') . '
  365. ' . (($showDuration === 'true') ? '"<td><small>" + (activity.duration || "-") + "</small></td>" +' : '') . '
  366. "<td><span class=\"" + statusClass + "\"><i class=\"fa fa-circle\"></i></span></td>" +
  367. "</tr>";
  368. }
  369. // Populate scheduled recordings table
  370. var scheduledTable = $("#embylivetv-scheduled-table");
  371. if (scheduledActivities.length === 0) {
  372. scheduledTable.html("<tr><td colspan=\"' . (4 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0) + ($showUserInfo === 'true' ? 1 : 0)) . '\" class=\"text-center text-muted\">No scheduled recordings</td></tr>");
  373. } else {
  374. var scheduledRows = scheduledActivities.map(formatScheduledRow).join("");
  375. scheduledTable.html(scheduledRows);
  376. }
  377. // Populate completed recordings table (only if enabled and table exists)
  378. ' . (($showCompleted === 'true') ? '
  379. var completedTable = $("#embylivetv-completed-table");
  380. if (completedActivities.length === 0) {
  381. completedTable.html("<tr><td colspan=\"' . (3 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '\" class=\"text-center text-muted\">No completed recordings</td></tr>");
  382. } else {
  383. var completedRows = completedActivities.map(formatCompletedRow).join("");
  384. completedTable.html(completedRows);
  385. }
  386. ' : '') . '
  387. } else {
  388. $("#embylivetv-scheduled-table").html("<tr><td colspan=\"' . (4 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0) + ($showUserInfo === 'true' ? 1 : 0)) . '\" class=\"text-center text-muted\">No activity data available</td></tr>");
  389. ' . (($showCompleted === 'true') ? '$("#embylivetv-completed-table").html("<tr><td colspan=\"' . (3 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '\" class=\"text-center text-muted\">No activity data available</td></tr>");' : '') . '
  390. }
  391. })
  392. .fail(function(xhr, status, error) {
  393. console.error("Error loading Emby LiveTV activity:", error);
  394. $("#embylivetv-scheduled-table").html("<tr><td colspan=\"' . (4 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showUserInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '\" class=\"text-center text-danger\">Error loading activity data</td></tr>");
  395. ' . (($showCompleted === 'true') ? '$("#embylivetv-completed-table").html("<tr><td colspan=\"' . (3 + ($showSeriesInfo === 'true' ? 1 : 0) + ($showDuration === 'true' ? 1 : 0)) . '\" class=\"text-center text-danger\">Error loading activity data</td></tr>");' : '') . '
  396. });
  397. }
  398. // Auto-refresh setup
  399. var refreshInterval = ' . $refreshInterval . ';
  400. if (refreshInterval > 0) {
  401. embyLiveTVRefreshTimer = setInterval(function() {
  402. refreshEmbyLiveTVData();
  403. }, refreshInterval);
  404. }
  405. // Update time display every 30 seconds
  406. setInterval(updateEmbyLiveTVLastRefreshTime, 30000);
  407. // Initial load
  408. $(document).ready(function() {
  409. refreshEmbyLiveTVData();
  410. });
  411. // Cleanup timer when page unloads
  412. $(window).on("beforeunload", function() {
  413. if (embyLiveTVRefreshTimer) {
  414. clearInterval(embyLiveTVRefreshTimer);
  415. }
  416. });
  417. </script>
  418. ';
  419. }
  420. }
  421. public function getHomepageEmbyLiveTVStats()
  422. {
  423. if (!$this->homepageItemPermissions($this->embyLiveTVTrackerHomepagePermissions('main'), true)) {
  424. return false;
  425. }
  426. if (!$this->config['embyURL'] || !$this->config['embyToken']) {
  427. $this->setAPIResponse('error', 'Emby URL or Token not configured', 500);
  428. return false;
  429. }
  430. try {
  431. $options = $this->requestOptions($this->config['embyURL'], null, $this->config['embyDisableCertCheck'], $this->config['embyUseCustomCertificate']);
  432. $baseUrl = $this->qualifyURL($this->config['embyURL']);
  433. $stats = [
  434. 'activeTimers' => 0,
  435. 'seriesTimers' => 0,
  436. 'todaysRecordings' => 0,
  437. 'totalRecordings' => 0,
  438. 'recentRecordings' => []
  439. ];
  440. // Get active timers
  441. $timersUrl = $baseUrl . '/emby/LiveTv/Timers?api_key=' . $this->config['embyToken'];
  442. try {
  443. $timersResponse = Requests::get($timersUrl, [], $options);
  444. if ($timersResponse->success) {
  445. $timers = json_decode($timersResponse->body, true);
  446. $stats['activeTimers'] = count($timers['Items'] ?? []);
  447. }
  448. } catch (Exception $e) {
  449. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Failed to get timers: ' . $e->getMessage());
  450. }
  451. // Get series timers
  452. $seriesTimersUrl = $baseUrl . '/emby/LiveTv/SeriesTimers?api_key=' . $this->config['embyToken'];
  453. try {
  454. $seriesTimersResponse = Requests::get($seriesTimersUrl, [], $options);
  455. if ($seriesTimersResponse->success) {
  456. $seriesTimers = json_decode($seriesTimersResponse->body, true);
  457. $stats['seriesTimers'] = count($seriesTimers['Items'] ?? []);
  458. }
  459. } catch (Exception $e) {
  460. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Failed to get series timers: ' . $e->getMessage());
  461. }
  462. // Get recordings from the last 90 days
  463. $recordingsUrl = $baseUrl . '/emby/LiveTv/Recordings?api_key=' . $this->config['embyToken'] . '&StartIndex=0&Limit=50&Fields=Overview,DateCreated&SortBy=DateCreated&SortOrder=Descending';
  464. try {
  465. $recordingsResponse = Requests::get($recordingsUrl, [], $options);
  466. if ($recordingsResponse->success) {
  467. $recordings = json_decode($recordingsResponse->body, true);
  468. $allRecordings = $recordings['Items'] ?? [];
  469. // Count today's recordings
  470. $today = date('Y-m-d');
  471. $todaysCount = 0;
  472. $recentRecordings = [];
  473. foreach ($allRecordings as $recording) {
  474. if (isset($recording['DateCreated'])) {
  475. $recordDate = date('Y-m-d', strtotime($recording['DateCreated']));
  476. if ($recordDate === $today) {
  477. $todaysCount++;
  478. }
  479. // Add to recent recordings list
  480. $recentRecordings[] = [
  481. 'date' => $recordDate,
  482. 'program' => $recording['Name'] ?? 'Unknown',
  483. 'series' => $recording['SeriesName'] ?? '',
  484. 'channel' => $recording['ChannelName'] ?? 'Unknown Channel',
  485. 'status' => 'Completed'
  486. ];
  487. }
  488. }
  489. $stats['todaysRecordings'] = $todaysCount;
  490. $stats['totalRecordings'] = $recordings['TotalRecordCount'] ?? count($allRecordings);
  491. $stats['recentRecordings'] = array_slice($recentRecordings, 0, 10); // Limit to 10 recent recordings
  492. }
  493. } catch (Exception $e) {
  494. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Failed to get recordings: ' . $e->getMessage());
  495. }
  496. $this->setAPIResponse('success', 'LiveTV stats retrieved successfully', 200, $stats);
  497. return true;
  498. } catch (Exception $e) {
  499. $this->setAPIResponse('error', 'Failed to retrieve LiveTV stats: ' . $e->getMessage(), 500);
  500. return false;
  501. }
  502. }
  503. public function getHomepageEmbyLiveTVActivity()
  504. {
  505. $debugEnabled = $this->config['homepageEmbyLiveTVTrackerDebug'] ?? false;
  506. if (!$this->homepageItemPermissions($this->embyLiveTVTrackerHomepagePermissions('main'), true)) {
  507. if ($debugEnabled) {
  508. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Permission denied for user');
  509. }
  510. $this->setAPIResponse('error', 'Permission denied', 403);
  511. return false;
  512. }
  513. if (!$this->config['embyURL'] || !$this->config['embyToken']) {
  514. $this->setAPIResponse('error', 'Emby URL or Token not configured', 500);
  515. return false;
  516. }
  517. try {
  518. if ($debugEnabled) {
  519. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Activity method called - starting execution');
  520. }
  521. $options = $this->requestOptions($this->config['embyURL'], null, $this->config['embyDisableCertCheck'], $this->config['embyUseCustomCertificate']);
  522. $baseUrl = $this->qualifyURL($this->config['embyURL']);
  523. if ($debugEnabled) {
  524. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Base URL configured: ' . $baseUrl);
  525. }
  526. $scheduledRecordings = [];
  527. $completedRecordings = [];
  528. $maxItems = intval($this->config['homepageEmbyLiveTVTrackerMaxItems'] ?? 10);
  529. $showCompleted = $this->config['homepageEmbyLiveTVTrackerShowCompleted'] ?? true;
  530. $maxCompletedItems = intval($this->config['homepageEmbyLiveTVTrackerMaxCompletedItems'] ?? 5);
  531. // Get user info if user info is enabled
  532. $userMap = [];
  533. $showUserInfo = $this->config['homepageEmbyLiveTVTrackerShowUserInfo'] ?? false;
  534. if ($showUserInfo) {
  535. $usersUrl = $baseUrl . '/emby/Users?api_key=' . $this->config['embyToken'];
  536. try {
  537. $usersResponse = Requests::get($usersUrl, [], $options);
  538. if ($usersResponse->success) {
  539. $users = json_decode($usersResponse->body, true);
  540. foreach ($users as $user) {
  541. $userMap[$user['Id']] = $user['Name'];
  542. }
  543. if ($debugEnabled) {
  544. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Retrieved ' . count($userMap) . ' users for mapping');
  545. }
  546. }
  547. } catch (Exception $e) {
  548. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Failed to get users: ' . $e->getMessage());
  549. }
  550. }
  551. // Get scheduled recordings (active timers)
  552. $timersUrl = $baseUrl . '/emby/LiveTv/Timers?api_key=' . $this->config['embyToken'] . '&Fields=ChannelName,ChannelId,SeriesName,ProgramInfo,StartDate,EndDate,UserId';
  553. if ($debugEnabled) {
  554. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Fetching timers from URL: ' . $timersUrl);
  555. }
  556. try {
  557. $timersResponse = Requests::get($timersUrl, [], $options);
  558. if ($debugEnabled) {
  559. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Timers API response status: ' . ($timersResponse->success ? 'success' : 'failed'));
  560. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Timers API response code: ' . $timersResponse->status_code);
  561. }
  562. if ($timersResponse->success) {
  563. $timers = json_decode($timersResponse->body, true);
  564. $allTimers = $timers['Items'] ?? [];
  565. if ($debugEnabled) {
  566. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Retrieved ' . count($allTimers) . ' timers from Emby API');
  567. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Timers API raw response length: ' . strlen($timersResponse->body));
  568. $this->setLoggerChannel('EmbyLiveTVTracker')->info('First timer sample: ' . json_encode(array_slice($allTimers, 0, 1)));
  569. }
  570. // Sort timers by start date
  571. usort($allTimers, function($a, $b) {
  572. $aDate = $a['StartDate'] ?? '';
  573. $bDate = $b['StartDate'] ?? '';
  574. return strcmp($aDate, $bDate);
  575. });
  576. $timersToProcess = array_slice($allTimers, 0, intval($maxItems));
  577. if ($debugEnabled) {
  578. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Processing ' . count($timersToProcess) . ' timers (maxItems: ' . $maxItems . ')');
  579. $this->setLoggerChannel('EmbyLiveTVTracker')->info('All timers count before slice: ' . count($allTimers));
  580. $this->setLoggerChannel('EmbyLiveTVTracker')->info('MaxItems value: ' . $maxItems . ' (type: ' . gettype($maxItems) . ')');
  581. }
  582. foreach ($timersToProcess as $index => $timer) {
  583. if ($debugEnabled) {
  584. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Processing timer ' . ($index + 1) . ': ' . json_encode([
  585. 'Name' => $timer['Name'] ?? 'no name',
  586. 'StartDate' => $timer['StartDate'] ?? 'no start date',
  587. 'EndDate' => $timer['EndDate'] ?? 'no end date',
  588. 'ChannelName' => $timer['ChannelName'] ?? 'no channel name',
  589. 'Status' => $timer['Status'] ?? 'no status',
  590. 'UserId' => $timer['UserId'] ?? 'no user'
  591. ]));
  592. }
  593. // Calculate duration
  594. $duration = '-';
  595. if (isset($timer['StartDate']) && isset($timer['EndDate'])) {
  596. $start = strtotime($timer['StartDate']);
  597. $end = strtotime($timer['EndDate']);
  598. if ($start && $end) {
  599. $minutes = round(($end - $start) / 60);
  600. $hours = floor($minutes / 60);
  601. $remainingMinutes = $minutes % 60;
  602. if ($hours > 0) {
  603. $duration = sprintf('%dh %dm', $hours, $remainingMinutes);
  604. } else {
  605. $duration = sprintf('%dm', $minutes);
  606. }
  607. }
  608. }
  609. // Get channel name - timers should have this information
  610. $channelName = $timer['ChannelName'] ?? null;
  611. if (empty($channelName) && !empty($timer['ChannelId'])) {
  612. $channelName = 'Channel ' . $timer['ChannelId'];
  613. } elseif (empty($channelName)) {
  614. $channelName = 'Unknown Channel';
  615. }
  616. // Get user name
  617. $userName = null;
  618. if ($showUserInfo && !empty($timer['UserId']) && isset($userMap[$timer['UserId']])) {
  619. $userName = $userMap[$timer['UserId']];
  620. }
  621. // Determine status based on timing
  622. $status = 'Scheduled';
  623. $startTime = strtotime($timer['StartDate'] ?? '');
  624. $endTime = strtotime($timer['EndDate'] ?? '');
  625. $now = time();
  626. if ($startTime && $endTime) {
  627. if ($now >= $startTime && $now <= $endTime) {
  628. $status = 'Recording';
  629. } elseif ($now > $endTime) {
  630. $status = 'Completed';
  631. }
  632. }
  633. // Get series name and episode title - try multiple approaches
  634. $seriesName = '';
  635. $episodeTitle = '';
  636. // Check for episode title first
  637. if (!empty($timer['ProgramInfo']['EpisodeTitle'])) {
  638. $episodeTitle = $timer['ProgramInfo']['EpisodeTitle'];
  639. }
  640. // Get series name
  641. if (!empty($timer['SeriesName'])) {
  642. $seriesName = $timer['SeriesName'];
  643. } elseif (!empty($timer['ProgramInfo']['SeriesName'])) {
  644. $seriesName = $timer['ProgramInfo']['SeriesName'];
  645. } elseif (!empty($timer['ProgramInfo']['Name'])) {
  646. $seriesName = $timer['ProgramInfo']['Name'];
  647. } elseif (!empty($timer['Name'])) {
  648. $seriesName = $timer['Name'];
  649. }
  650. // Use episode title if available, otherwise use program/series name
  651. $displayName = $episodeTitle ? $episodeTitle : ($timer['Name'] ?? ($timer['ProgramInfo']['Name'] ?? 'Unknown Program'));
  652. $activity = [
  653. 'date' => $timer['StartDate'] ?? '',
  654. 'name' => $displayName,
  655. 'seriesName' => $seriesName,
  656. 'episodeTitle' => $episodeTitle,
  657. 'channelName' => $channelName,
  658. 'userName' => $userName,
  659. 'duration' => $duration,
  660. 'status' => $status,
  661. 'type' => 'timer'
  662. ];
  663. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Created activity for timer ' . ($index + 1) . ': ' . json_encode($activity));
  664. // Debug timer date parsing
  665. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Timer date debug - StartDate: "' . ($timer['StartDate'] ?? 'null') . '", parsed startTime: ' . ($startTime ? date('Y-m-d H:i:s', $startTime) : 'failed to parse') . ', EndDate: "' . ($timer['EndDate'] ?? 'null') . '", parsed endTime: ' . ($endTime ? date('Y-m-d H:i:s', $endTime) : 'failed to parse') . ', current time: ' . date('Y-m-d H:i:s', $now) . ', calculated status: ' . $status);
  666. $scheduledRecordings[] = $activity;
  667. }
  668. }
  669. } catch (Exception $e) {
  670. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Failed to get timers for activity: ' . $e->getMessage());
  671. }
  672. // Get completed recordings if enabled
  673. if ($showCompleted) {
  674. $recordingsUrl = $baseUrl . '/emby/LiveTv/Recordings?api_key=' . $this->config['embyToken'] . '&StartIndex=0&Limit=' . $maxCompletedItems . '&Fields=DateCreated,SeriesName,RunTimeTicks&SortBy=DateCreated&SortOrder=Descending';
  675. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Fetching completed recordings from URL: ' . $recordingsUrl);
  676. try {
  677. $recordingsResponse = Requests::get($recordingsUrl, [], $options);
  678. if ($recordingsResponse->success) {
  679. $recordings = json_decode($recordingsResponse->body, true);
  680. $allRecordings = $recordings['Items'] ?? [];
  681. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Retrieved ' . count($allRecordings) . ' completed recordings');
  682. foreach ($allRecordings as $recording) {
  683. if (isset($recording['DateCreated'])) {
  684. // Format duration
  685. $duration = '-';
  686. if (isset($recording['RunTimeTicks']) && $recording['RunTimeTicks'] > 0) {
  687. $minutes = floor($recording['RunTimeTicks'] / 600000000);
  688. $hours = floor($minutes / 60);
  689. $remainingMinutes = $minutes % 60;
  690. if ($hours > 0) {
  691. $duration = sprintf('%dh %dm', $hours, $remainingMinutes);
  692. } else {
  693. $duration = sprintf('%dm', $minutes);
  694. }
  695. }
  696. $completedRecordings[] = [
  697. 'date' => $recording['DateCreated'],
  698. 'name' => $recording['Name'] ?? 'Unknown Program',
  699. 'seriesName' => $recording['SeriesName'] ?? '',
  700. 'channelName' => 'Unknown Channel', // Completed recordings don't have reliable channel info
  701. 'userName' => null, // No user info available for completed recordings
  702. 'duration' => $duration,
  703. 'status' => 'Completed',
  704. 'type' => 'recording'
  705. ];
  706. }
  707. }
  708. }
  709. } catch (Exception $e) {
  710. $this->setLoggerChannel('EmbyLiveTVTracker')->warning('Failed to get completed recordings: ' . $e->getMessage());
  711. }
  712. }
  713. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Final scheduled recordings count: ' . count($scheduledRecordings));
  714. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Final completed recordings count: ' . count($completedRecordings));
  715. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Sample scheduled: ' . json_encode(array_slice($scheduledRecordings, 0, 1)));
  716. $this->setLoggerChannel('EmbyLiveTVTracker')->info('Sample completed: ' . json_encode(array_slice($completedRecordings, 0, 1)));
  717. $this->setAPIResponse('success', 'LiveTV activity retrieved successfully', 200, [
  718. 'scheduledRecordings' => $scheduledRecordings,
  719. 'completedRecordings' => $completedRecordings
  720. ]);
  721. return true;
  722. } catch (Exception $e) {
  723. $this->setAPIResponse('error', 'Failed to retrieve LiveTV activity: ' . $e->getMessage(), 500);
  724. return false;
  725. }
  726. }
  727. }
  728. ?>