jellystat.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. <?php
  2. /**
  3. * JellyStat Homepage Plugin for Organizr
  4. * Supports both Emby and Jellyfin servers via JellyStat API or embedded interface
  5. */
  6. trait JellyStatHomepageItem
  7. {
  8. public function jellystatSettingsArray($infoOnly = false)
  9. {
  10. $homepageInformation = [
  11. 'name' => 'JellyStat',
  12. 'enabled' => true,
  13. 'image' => 'plugins/images/homepage/jellystat.png',
  14. 'category' => 'Media Server',
  15. 'settingsArray' => __FUNCTION__
  16. ];
  17. if ($infoOnly) {
  18. return $homepageInformation;
  19. }
  20. $homepageSettings = [
  21. 'debug' => true,
  22. 'settings' => [
  23. 'Enable' => [
  24. $this->settingsOption('enable', 'homepageJellyStatEnabled'),
  25. $this->settingsOption('auth', 'homepageJellyStatAuth'),
  26. ],
  27. 'Display Mode' => [
  28. $this->settingsOption('select', 'homepageJellyStatDisplayMode', ['label' => 'Display Mode', 'options' => [
  29. ['name' => 'Native Statistics View', 'value' => 'native'],
  30. ['name' => 'Embedded JellyStat Interface', 'value' => 'iframe']
  31. ]]),
  32. ],
  33. 'Connection' => [
  34. $this->settingsOption('url', 'jellyStatURL', ['label' => 'JellyStat URL', 'help' => 'URL to your JellyStat instance']),
  35. $this->settingsOption('token', 'jellyStatApikey', ['label' => 'JellyStat API Key', 'help' => 'API key for JellyStat (required for native mode)']),
  36. $this->settingsOption('disable-cert-check', 'jellyStatDisableCertCheck'),
  37. $this->settingsOption('use-custom-certificate', 'jellyStatUseCustomCertificate'),
  38. ],
  39. 'Native Mode Options' => [
  40. $this->settingsOption('number', 'homepageJellyStatRefresh', ['label' => 'Auto-refresh Interval (minutes)', 'min' => 1, 'max' => 60]),
  41. $this->settingsOption('number', 'homepageJellyStatDays', ['label' => 'Statistics Period (days)', 'min' => 1, 'max' => 365]),
  42. $this->settingsOption('switch', 'homepageJellyStatShowLibraries', ['label' => 'Show Library Statistics']),
  43. $this->settingsOption('switch', 'homepageJellyStatShowUsers', ['label' => 'Show User Statistics']),
  44. $this->settingsOption('switch', 'homepageJellyStatShowMostWatched', ['label' => 'Show Most Watched Content']),
  45. $this->settingsOption('switch', 'homepageJellyStatShowRecentActivity', ['label' => 'Show Recent Activity']),
  46. $this->settingsOption('number', 'homepageJellyStatMaxItems', ['label' => 'Maximum Items to Display', 'min' => 5, 'max' => 50]),
  47. ],
  48. 'Iframe Mode Options' => [
  49. $this->settingsOption('number', 'homepageJellyStatIframeHeight', ['label' => 'Iframe Height (pixels)', 'min' => 300, 'max' => 2000]),
  50. $this->settingsOption('switch', 'homepageJellyStatIframeScrolling', ['label' => 'Allow Scrolling in Iframe']),
  51. ],
  52. 'Test Connection' => [
  53. $this->settingsOption('blank', null, ['label' => 'Please Save before Testing']),
  54. $this->settingsOption('test', 'jellystat'),
  55. ]
  56. ]
  57. ];
  58. return array_merge($homepageInformation, $homepageSettings);
  59. }
  60. public function testConnectionJellyStat()
  61. {
  62. if (!$this->homepageItemPermissions($this->jellystatHomepagePermissions('test'), true)) {
  63. return false;
  64. }
  65. $url = $this->config['jellyStatURL'] ?? '';
  66. $token = $this->config['jellyStatApikey'] ?? '';
  67. $disableCert = $this->config['jellyStatDisableCertCheck'] ?? false;
  68. $customCert = $this->config['jellyStatUseCustomCertificate'] ?? false;
  69. if (empty($url)) {
  70. $this->setAPIResponse('error', 'JellyStat URL not configured', 500);
  71. return false;
  72. }
  73. $displayMode = $this->config['homepageJellyStatDisplayMode'] ?? 'native';
  74. if ($displayMode === 'iframe') {
  75. // For iframe mode, just test if the URL is reachable
  76. try {
  77. $options = $this->requestOptions($url, null, $disableCert, $customCert);
  78. $response = Requests::get($this->qualifyURL($url), [], $options);
  79. if ($response->success) {
  80. $this->setAPIResponse('success', 'Successfully connected to JellyStat', 200);
  81. return true;
  82. } else {
  83. $this->setAPIResponse('error', 'Failed to connect to JellyStat URL', 500);
  84. return false;
  85. }
  86. } catch (Exception $e) {
  87. $this->setAPIResponse('error', 'Connection test failed: ' . $e->getMessage(), 500);
  88. return false;
  89. }
  90. } else {
  91. // For native mode, test API connection
  92. if (empty($token)) {
  93. $this->setAPIResponse('error', 'JellyStat API key not configured for native mode', 500);
  94. return false;
  95. }
  96. try {
  97. $options = $this->requestOptions($url, null, $disableCert, $customCert);
  98. // Test JellyStat API - try to get server info or stats
  99. $testUrl = $this->qualifyURL($url) . '/api/getLibraries';
  100. $headers = ['Authorization' => 'Bearer ' . $token];
  101. $response = Requests::get($testUrl, $headers, $options);
  102. if ($response->success) {
  103. $data = json_decode($response->body, true);
  104. if (isset($data) && is_array($data)) {
  105. $this->setAPIResponse('success', 'Successfully connected to JellyStat API', 200);
  106. return true;
  107. }
  108. }
  109. // Fallback test - try different API endpoint structure
  110. $testUrl = $this->qualifyURL($url) . '/api/v1/stats';
  111. $response = Requests::get($testUrl, $headers, $options);
  112. if ($response->success) {
  113. $this->setAPIResponse('success', 'Successfully connected to JellyStat API', 200);
  114. return true;
  115. }
  116. $this->setAPIResponse('error', 'Connection test failed - invalid response from JellyStat API', 500);
  117. return false;
  118. } catch (Exception $e) {
  119. $this->setAPIResponse('error', 'Connection test failed: ' . $e->getMessage(), 500);
  120. return false;
  121. }
  122. }
  123. }
  124. public function jellystatHomepagePermissions($key = null)
  125. {
  126. $permissions = [
  127. 'test' => [
  128. 'enabled' => [
  129. 'homepageJellyStatEnabled',
  130. ],
  131. 'auth' => [
  132. 'homepageJellyStatAuth',
  133. ],
  134. 'not_empty' => [
  135. 'jellyStatURL',
  136. 'jellyStatApikey'
  137. ]
  138. ],
  139. 'main' => [
  140. 'enabled' => [
  141. 'homepageJellyStatEnabled'
  142. ],
  143. 'auth' => [
  144. 'homepageJellyStatAuth'
  145. ],
  146. 'not_empty' => [
  147. 'jellyStatURL',
  148. 'jellyStatApikey'
  149. ]
  150. ]
  151. ];
  152. return $this->homepageCheckKeyPermissions($key, $permissions);
  153. }
  154. public function homepageOrderJellyStat()
  155. {
  156. if ($this->homepageItemPermissions($this->jellystatHomepagePermissions('main'))) {
  157. $displayMode = $this->config['homepageJellyStatDisplayMode'] ?? 'native';
  158. if ($displayMode === 'iframe') {
  159. return $this->renderJellyStatIframe();
  160. } else {
  161. return $this->renderJellyStatNative();
  162. }
  163. }
  164. }
  165. private function renderJellyStatIframe()
  166. {
  167. $url = $this->config['jellyStatURL'] ?? '';
  168. $height = $this->config['homepageJellyStatIframeHeight'] ?? 800;
  169. $scrolling = ($this->config['homepageJellyStatIframeScrolling'] ?? true) ? 'auto' : 'no';
  170. return '
  171. <div id="' . __FUNCTION__ . '">
  172. <div class="white-box">
  173. <div class="white-box-header">
  174. <i class="fa fa-bar-chart"></i> JellyStat Dashboard
  175. </div>
  176. <div class="white-box-content" style="padding: 0;">
  177. <iframe
  178. src="' . htmlspecialchars($this->qualifyURL($url)) . '"
  179. width="100%"
  180. height="' . intval($height) . 'px"
  181. style="border: none; border-radius: 0 0 4px 4px;"
  182. scrolling="' . $scrolling . '"
  183. frameborder="0">
  184. <p>Your browser does not support iframes. Please visit <a href="' . htmlspecialchars($url) . '" target="_blank">JellyStat</a> directly.</p>
  185. </iframe>
  186. </div>
  187. </div>
  188. </div>';
  189. }
  190. private function renderJellyStatNative()
  191. {
  192. $refreshInterval = ($this->config['homepageJellyStatRefresh'] ?? 5) * 60000; // Convert minutes to milliseconds
  193. $days = $this->config['homepageJellyStatDays'] ?? 30;
  194. $maxItems = $this->config['homepageJellyStatMaxItems'] ?? 10;
  195. $showLibraries = ($this->config['homepageJellyStatShowLibraries'] ?? true) ? 'true' : 'false';
  196. $showUsers = ($this->config['homepageJellyStatShowUsers'] ?? true) ? 'true' : 'false';
  197. $showMostWatched = ($this->config['homepageJellyStatShowMostWatched'] ?? true) ? 'true' : 'false';
  198. $showRecentActivity = ($this->config['homepageJellyStatShowRecentActivity'] ?? true) ? 'true' : 'false';
  199. return '
  200. <div id="' . __FUNCTION__ . '">
  201. <div class="white-box">
  202. <div class="white-box-header">
  203. <i class="fa fa-bar-chart"></i> JellyStat Analytics
  204. <span class="pull-right">
  205. <small id="jellystat-last-update" class="text-muted"></small>
  206. <button class="btn btn-xs btn-primary" onclick="refreshJellyStatData()" title="Refresh Data">
  207. <i class="fa fa-refresh" id="jellystat-refresh-icon"></i>
  208. </button>
  209. </span>
  210. </div>
  211. <div class="white-box-content">
  212. <div class="row" id="jellystat-content">
  213. <div class="col-lg-12 text-center">
  214. <i class="fa fa-spinner fa-spin"></i> Loading JellyStat data...
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. <script>
  221. var jellyStatRefreshTimer;
  222. var jellyStatLastRefresh = 0;
  223. function refreshJellyStatData() {
  224. var refreshIcon = $("#jellystat-refresh-icon");
  225. refreshIcon.addClass("fa-spin");
  226. // Show loading state
  227. $("#jellystat-content").html("<div class=\"col-lg-12 text-center\"><i class=\"fa fa-spinner fa-spin\"></i> Loading JellyStat data...</div>");
  228. // Load JellyStat data
  229. getJellyStatData()
  230. .always(function() {
  231. refreshIcon.removeClass("fa-spin");
  232. jellyStatLastRefresh = Date.now();
  233. updateJellyStatLastRefreshTime();
  234. });
  235. }
  236. function updateJellyStatLastRefreshTime() {
  237. if (jellyStatLastRefresh > 0) {
  238. var ago = Math.floor((Date.now() - jellyStatLastRefresh) / 1000);
  239. var timeText = ago < 60 ? ago + "s ago" : Math.floor(ago / 60) + "m ago";
  240. $("#jellystat-last-update").text("Updated " + timeText);
  241. }
  242. }
  243. function getJellyStatData() {
  244. return organizrAPI2("GET", "api/v2/homepage/jellystat")
  245. .done(function(data) {
  246. if (data && data.response && data.response.result === "success" && data.response.data) {
  247. renderJellyStatData(data.response.data);
  248. } else {
  249. $("#jellystat-content").html("<div class=\"col-lg-12 text-center text-danger\">Failed to load JellyStat data</div>");
  250. }
  251. })
  252. .fail(function(xhr, status, error) {
  253. $("#jellystat-content").html("<div class=\"col-lg-12 text-center text-danger\">Error loading JellyStat data</div>");
  254. });
  255. }
  256. function renderJellyStatData(stats) {
  257. var html = "";
  258. // Library Statistics
  259. if (' . $showLibraries . ' && stats.libraries && stats.libraries.length > 0) {
  260. html += "<div class=\"col-lg-6\">";
  261. html += "<h5><i class=\"fa fa-folder text-info\"></i> Library Statistics</h5>";
  262. html += "<div class=\"table-responsive\">";
  263. html += "<table class=\"table table-striped table-condensed\">";
  264. html += "<thead><tr><th>Library</th><th>Items</th><th>Size</th></tr></thead>";
  265. html += "<tbody>";
  266. stats.libraries.slice(0, ' . $maxItems . ').forEach(function(lib) {
  267. html += "<tr>";
  268. html += "<td><strong>" + (lib.name || "Unknown Library") + "</strong></td>";
  269. html += "<td><span class=\"label label-primary\">" + (lib.item_count || 0) + "</span></td>";
  270. html += "<td>" + (lib.size || "Unknown") + "</td>";
  271. html += "</tr>";
  272. });
  273. html += "</tbody></table></div></div>";
  274. }
  275. // User Statistics
  276. if (' . $showUsers . ' && stats.users && stats.users.length > 0) {
  277. html += "<div class=\"col-lg-6\">";
  278. html += "<h5><i class=\"fa fa-users\"></i> Active Users (" + stats.users.length + " total)</h5>";
  279. html += "<div class=\"row\">";
  280. stats.users.slice(0, 8).forEach(function(user) {
  281. var lastActivity = "Never";
  282. if (user.last_activity && user.last_activity !== "0001-01-01T00:00:00.0000000Z") {
  283. var activityDate = new Date(user.last_activity);
  284. lastActivity = activityDate.toLocaleDateString();
  285. }
  286. var playCount = user.play_count || 0;
  287. html += "<div class=\"col-md-6 col-sm-6\" style=\"margin-bottom: 10px;\">";
  288. html += "<div class=\"media\">";
  289. html += "<div class=\"media-left\"><i class=\"fa fa-user fa-2x text-muted\"></i></div>";
  290. html += "<div class=\"media-body\">";
  291. html += "<h6 class=\"media-heading\">" + (user.name || "Unknown User") + " <span class=\"label label-info\">" + playCount + " plays</span></h6>";
  292. html += "<small class=\"text-muted\">Last Activity: " + lastActivity + "</small>";
  293. html += "</div></div></div>";
  294. });
  295. html += "</div></div>";
  296. }
  297. // Most Watched Content
  298. if (' . $showMostWatched . ' && stats.most_watched && stats.most_watched.length > 0) {
  299. html += "<div class=\"col-lg-12\" style=\"margin-top: 20px;\">";
  300. html += "<h5><i class=\"fa fa-star text-warning\"></i> Most Watched Content</h5>";
  301. html += "<div class=\"table-responsive\">";
  302. html += "<table class=\"table table-striped table-condensed\">";
  303. html += "<thead><tr><th>Title</th><th>Type</th><th>Plays</th><th>Runtime</th><th>Year</th></tr></thead>";
  304. html += "<tbody>";
  305. stats.most_watched.slice(0, ' . $maxItems . ').forEach(function(item) {
  306. html += "<tr>";
  307. html += "<td><strong>" + (item.title || "Unknown Title") + "</strong></td>";
  308. html += "<td>" + (item.type || "Unknown") + "</td>";
  309. html += "<td><span class=\"label label-primary\">" + (item.play_count || 0) + "</span></td>";
  310. html += "<td>" + (item.runtime || "Unknown") + "</td>";
  311. html += "<td>" + (item.year || "N/A") + "</td>";
  312. html += "</tr>";
  313. });
  314. html += "</tbody></table></div></div>";
  315. }
  316. // Recent Activity
  317. if (' . $showRecentActivity . ' && stats.recent_activity && stats.recent_activity.length > 0) {
  318. html += "<div class=\"col-lg-12\" style=\"margin-top: 20px;\">";
  319. html += "<h5><i class=\"fa fa-clock-o text-success\"></i> Recent Activity</h5>";
  320. html += "<div class=\"table-responsive\">";
  321. html += "<table class=\"table table-striped table-condensed\">";
  322. html += "<thead><tr><th>Date</th><th>User</th><th>Title</th><th>Type</th></tr></thead>";
  323. html += "<tbody>";
  324. stats.recent_activity.slice(0, ' . $maxItems . ').forEach(function(activity) {
  325. var date = new Date(activity.date);
  326. var formattedDate = date.toLocaleDateString() + " " + date.toLocaleTimeString([], {hour: "2-digit", minute:"2-digit"});
  327. html += "<tr>";
  328. html += "<td><small>" + formattedDate + "</small></td>";
  329. html += "<td>" + (activity.user || "Unknown User") + "</td>";
  330. html += "<td><strong>" + (activity.title || "Unknown Title") + "</strong></td>";
  331. html += "<td>" + (activity.type || "Unknown") + "</td>";
  332. html += "</tr>";
  333. });
  334. html += "</tbody></table></div></div>";
  335. }
  336. if (!html) {
  337. html = "<div class=\"col-lg-12 text-center text-muted\">";
  338. html += "<i class=\"fa fa-exclamation-circle fa-3x\" style=\"margin-bottom: 10px;\"></i>";
  339. html += "<h4>No JellyStat data available</h4>";
  340. html += "<p>Check your JellyStat connection and API configuration.</p>";
  341. html += "</div>";
  342. }
  343. $("#jellystat-content").html(html);
  344. }
  345. // Auto-refresh setup
  346. var refreshInterval = ' . $refreshInterval . ';
  347. if (refreshInterval > 0) {
  348. jellyStatRefreshTimer = setInterval(function() {
  349. refreshJellyStatData();
  350. }, refreshInterval);
  351. }
  352. // Update time display every 30 seconds
  353. setInterval(updateJellyStatLastRefreshTime, 30000);
  354. // Initial load
  355. $(document).ready(function() {
  356. refreshJellyStatData();
  357. });
  358. // Cleanup timer when page unloads
  359. $(window).on("beforeunload", function() {
  360. if (jellyStatRefreshTimer) {
  361. clearInterval(jellyStatRefreshTimer);
  362. }
  363. });
  364. </script>
  365. ';
  366. }
  367. /**
  368. * Main function to get JellyStat data
  369. */
  370. public function getJellyStatData($options = null)
  371. {
  372. if (!$this->homepageItemPermissions($this->jellystatHomepagePermissions('main'), true)) {
  373. $this->setAPIResponse('error', 'User not approved to view this homepage item - check plugin configuration', 401);
  374. return false;
  375. }
  376. try {
  377. $url = $this->config['jellyStatURL'] ?? '';
  378. $token = $this->config['jellyStatApikey'] ?? '';
  379. $days = intval($this->config['homepageJellyStatDays'] ?? 30);
  380. if (empty($url) || empty($token)) {
  381. $this->setAPIResponse('error', 'JellyStat URL or API key not configured', 500);
  382. return false;
  383. }
  384. $stats = $this->fetchJellyStatStats($url, $token, $days);
  385. if (isset($stats['error']) && $stats['error']) {
  386. $this->setAPIResponse('error', $stats['message'], 500);
  387. return false;
  388. }
  389. $this->setAPIResponse('success', 'JellyStat data retrieved successfully', 200, $stats);
  390. return true;
  391. } catch (Exception $e) {
  392. $this->setAPIResponse('error', 'Failed to retrieve JellyStat data: ' . $e->getMessage(), 500);
  393. return false;
  394. }
  395. }
  396. /**
  397. * Fetch statistics from JellyStat API
  398. */
  399. private function fetchJellyStatStats($url, $token, $days = 30)
  400. {
  401. $disableCert = $this->config['jellyStatDisableCertCheck'] ?? false;
  402. $customCert = $this->config['jellyStatUseCustomCertificate'] ?? false;
  403. $options = $this->requestOptions($url, null, $disableCert, $customCert);
  404. $baseUrl = $this->qualifyURL($url);
  405. $headers = ['Authorization' => 'Bearer ' . $token];
  406. $stats = [
  407. 'period' => "{$days} days",
  408. 'libraries' => [],
  409. 'users' => [],
  410. 'most_watched' => [],
  411. 'recent_activity' => []
  412. ];
  413. try {
  414. // Get Library Statistics
  415. $librariesUrl = $baseUrl . '/api/getLibraries';
  416. $response = Requests::get($librariesUrl, $headers, $options);
  417. if ($response->success) {
  418. $data = json_decode($response->body, true);
  419. if (is_array($data)) {
  420. $stats['libraries'] = array_map(function($lib) {
  421. return [
  422. 'name' => $lib['Name'] ?? $lib['name'] ?? 'Unknown Library',
  423. 'item_count' => $lib['ItemCount'] ?? $lib['item_count'] ?? 0,
  424. 'size' => $this->formatBytes($lib['Size'] ?? $lib['size'] ?? 0)
  425. ];
  426. }, $data);
  427. }
  428. }
  429. // Get User Statistics
  430. $usersUrl = $baseUrl . '/api/getUserStats';
  431. $response = Requests::get($usersUrl, $headers, $options);
  432. if ($response->success) {
  433. $data = json_decode($response->body, true);
  434. if (is_array($data)) {
  435. $stats['users'] = array_map(function($user) {
  436. return [
  437. 'name' => $user['UserName'] ?? $user['name'] ?? 'Unknown User',
  438. 'play_count' => $user['PlayCount'] ?? $user['play_count'] ?? 0,
  439. 'last_activity' => $user['LastActivityDate'] ?? $user['last_activity'] ?? null
  440. ];
  441. }, $data);
  442. }
  443. }
  444. // Get Most Watched Content
  445. $mostWatchedUrl = $baseUrl . '/api/getMostWatched?days=' . $days;
  446. $response = Requests::get($mostWatchedUrl, $headers, $options);
  447. if ($response->success) {
  448. $data = json_decode($response->body, true);
  449. if (is_array($data)) {
  450. $stats['most_watched'] = array_map(function($item) {
  451. return [
  452. 'title' => $item['Name'] ?? $item['title'] ?? 'Unknown Title',
  453. 'type' => $item['Type'] ?? $item['type'] ?? 'Unknown',
  454. 'play_count' => $item['PlayCount'] ?? $item['play_count'] ?? 0,
  455. 'runtime' => $this->formatDuration($item['RunTimeTicks'] ?? $item['runtime'] ?? 0),
  456. 'year' => $item['ProductionYear'] ?? $item['year'] ?? null
  457. ];
  458. }, $data);
  459. }
  460. }
  461. // Get Recent Activity
  462. $recentActivityUrl = $baseUrl . '/api/getRecentActivity?days=' . $days;
  463. $response = Requests::get($recentActivityUrl, $headers, $options);
  464. if ($response->success) {
  465. $data = json_decode($response->body, true);
  466. if (is_array($data)) {
  467. $stats['recent_activity'] = array_map(function($activity) {
  468. return [
  469. 'date' => $activity['Date'] ?? $activity['date'] ?? date('c'),
  470. 'user' => $activity['UserName'] ?? $activity['user'] ?? 'Unknown User',
  471. 'title' => $activity['ItemName'] ?? $activity['title'] ?? 'Unknown Title',
  472. 'type' => $activity['ItemType'] ?? $activity['type'] ?? 'Unknown'
  473. ];
  474. }, $data);
  475. }
  476. }
  477. } catch (Exception $e) {
  478. return ['error' => true, 'message' => 'Failed to fetch JellyStat data: ' . $e->getMessage()];
  479. }
  480. return $stats;
  481. }
  482. /**
  483. * Format bytes to human readable format
  484. */
  485. private function formatBytes($size, $precision = 2)
  486. {
  487. if ($size == 0) return '0 B';
  488. $base = log($size, 1024);
  489. $suffixes = ['B', 'KB', 'MB', 'GB', 'TB'];
  490. return round(pow(1024, $base - floor($base)), $precision) . ' ' . $suffixes[floor($base)];
  491. }
  492. /**
  493. * Format duration for display
  494. */
  495. private function formatDuration($ticks)
  496. {
  497. if ($ticks == 0) return 'Unknown';
  498. // Convert ticks to seconds (ticks are in 100-nanosecond intervals)
  499. $seconds = $ticks / 10000000;
  500. if ($seconds < 3600) {
  501. return gmdate('i:s', $seconds);
  502. } else {
  503. return gmdate('H:i:s', $seconds);
  504. }
  505. }
  506. }