userWatchStats.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <?php
  2. /**
  3. * User Watch Statistics Homepage Plugin
  4. * Provides comprehensive user watching statistics from Plex/Emby/Jellyfin
  5. */
  6. trait HomepageUserWatchStats
  7. {
  8. /**
  9. * Get user watch statistics data
  10. */
  11. public function userWatchStatsHomepagePermissions($user)
  12. {
  13. // Check if user has access to statistics
  14. if ($user['groupID'] == 0 || $user['groupID'] == 1) {
  15. return true;
  16. }
  17. return false;
  18. }
  19. /**
  20. * Main function to get watch statistics
  21. */
  22. public function getUserWatchStats($options = null)
  23. {
  24. if (!$this->homepageItemPermissions($this->userWatchStatsHomepagePermissions($this->user), true)) {
  25. return false;
  26. }
  27. try {
  28. $mediaServer = $this->config['homepageUserWatchStatsService'] ?? 'plex';
  29. $days = intval($options['days'] ?? 30);
  30. switch (strtolower($mediaServer)) {
  31. case 'plex':
  32. return $this->getPlexWatchStats($days);
  33. case 'emby':
  34. return $this->getEmbyWatchStats($days);
  35. case 'jellyfin':
  36. return $this->getJellyfinWatchStats($days);
  37. default:
  38. return $this->getPlexWatchStats($days);
  39. }
  40. } catch (Exception $e) {
  41. $this->writeLog('error', 'User Watch Stats Error: ' . $e->getMessage(), 'SYSTEM');
  42. return [
  43. 'error' => true,
  44. 'message' => 'Failed to retrieve watch statistics'
  45. ];
  46. }
  47. }
  48. /**
  49. * Get Plex watch statistics via Tautulli API
  50. */
  51. private function getPlexWatchStats($days = 30)
  52. {
  53. $tautulliUrl = $this->config['plexURL'] ?? '';
  54. $tautulliToken = $this->config['plexToken'] ?? '';
  55. if (empty($tautulliUrl) || empty($tautulliToken)) {
  56. return ['error' => true, 'message' => 'Tautulli URL or API key not configured'];
  57. }
  58. $endDate = date('Y-m-d');
  59. $startDate = date('Y-m-d', strtotime("-{$days} days"));
  60. $stats = [
  61. 'period' => "{$days} days",
  62. 'start_date' => $startDate,
  63. 'end_date' => $endDate,
  64. 'most_watched' => $this->getTautulliMostWatched($tautulliUrl, $tautulliToken, $days),
  65. 'least_watched' => $this->getTautulliLeastWatched($tautulliUrl, $tautulliToken, $days),
  66. 'user_stats' => $this->getTautulliUserStats($tautulliUrl, $tautulliToken, $days),
  67. 'recent_activity' => $this->getTautulliRecentActivity($tautulliUrl, $tautulliToken),
  68. 'top_users' => $this->getTautulliTopUsers($tautulliUrl, $tautulliToken, $days)
  69. ];
  70. return $stats;
  71. }
  72. /**
  73. * Get most watched content from Tautulli
  74. */
  75. private function getTautulliMostWatched($url, $token, $days)
  76. {
  77. $endpoint = rtrim($url, '/') . '/api/v2';
  78. $params = [
  79. 'apikey' => $token,
  80. 'cmd' => 'get_home_stats',
  81. 'time_range' => $days,
  82. 'stats_type' => 'plays',
  83. 'stats_count' => 10
  84. ];
  85. $response = $this->guzzle->request('GET', $endpoint, [
  86. 'query' => $params,
  87. 'timeout' => 15,
  88. 'connect_timeout' => 15,
  89. 'http_errors' => false
  90. ]);
  91. if ($response->getStatusCode() === 200) {
  92. $data = json_decode($response->getBody(), true);
  93. return $data['response']['data'] ?? [];
  94. }
  95. return [];
  96. }
  97. /**
  98. * Get user statistics from Tautulli
  99. */
  100. private function getTautulliUserStats($url, $token, $days)
  101. {
  102. $endpoint = rtrim($url, '/') . '/api/v2';
  103. $params = [
  104. 'apikey' => $token,
  105. 'cmd' => 'get_user_watch_time_stats',
  106. 'time_range' => $days
  107. ];
  108. $response = $this->guzzle->request('GET', $endpoint, [
  109. 'query' => $params,
  110. 'timeout' => 15,
  111. 'connect_timeout' => 15,
  112. 'http_errors' => false
  113. ]);
  114. if ($response->getStatusCode() === 200) {
  115. $data = json_decode($response->getBody(), true);
  116. return $data['response']['data'] ?? [];
  117. }
  118. return [];
  119. }
  120. /**
  121. * Get top users from Tautulli
  122. */
  123. private function getTautulliTopUsers($url, $token, $days)
  124. {
  125. $endpoint = rtrim($url, '/') . '/api/v2';
  126. $params = [
  127. 'apikey' => $token,
  128. 'cmd' => 'get_users',
  129. 'length' => 25
  130. ];
  131. $response = $this->guzzle->request('GET', $endpoint, [
  132. 'query' => $params,
  133. 'timeout' => 15,
  134. 'connect_timeout' => 15,
  135. 'http_errors' => false
  136. ]);
  137. if ($response->getStatusCode() === 200) {
  138. $data = json_decode($response->getBody(), true);
  139. $users = $data['response']['data']['data'] ?? [];
  140. // Sort by play count
  141. usort($users, function($a, $b) {
  142. return ($b['play_count'] ?? 0) - ($a['play_count'] ?? 0);
  143. });
  144. return array_slice($users, 0, 10);
  145. }
  146. return [];
  147. }
  148. /**
  149. * Get recent activity from Tautulli
  150. */
  151. private function getTautulliRecentActivity($url, $token)
  152. {
  153. $endpoint = rtrim($url, '/') . '/api/v2';
  154. $params = [
  155. 'apikey' => $token,
  156. 'cmd' => 'get_recently_added',
  157. 'count' => 10
  158. ];
  159. $response = $this->guzzle->request('GET', $endpoint, [
  160. 'query' => $params,
  161. 'timeout' => 15,
  162. 'connect_timeout' => 15,
  163. 'http_errors' => false
  164. ]);
  165. if ($response->getStatusCode() === 200) {
  166. $data = json_decode($response->getBody(), true);
  167. return $data['response']['data']['recently_added'] ?? [];
  168. }
  169. return [];
  170. }
  171. /**
  172. * Get least watched content (inverse of most watched)
  173. */
  174. private function getTautulliLeastWatched($url, $token, $days)
  175. {
  176. $endpoint = rtrim($url, '/') . '/api/v2';
  177. $params = [
  178. 'apikey' => $token,
  179. 'cmd' => 'get_libraries',
  180. ];
  181. $response = $this->guzzle->request('GET', $endpoint, [
  182. 'query' => $params,
  183. 'timeout' => 15,
  184. 'connect_timeout' => 15,
  185. 'http_errors' => false
  186. ]);
  187. if ($response->getStatusCode() === 200) {
  188. $data = json_decode($response->getBody(), true);
  189. $libraries = $data['response']['data'] ?? [];
  190. $leastWatched = [];
  191. foreach ($libraries as $library) {
  192. $libraryStats = $this->getTautulliLibraryStats($url, $token, $library['section_id'], $days);
  193. if (!empty($libraryStats)) {
  194. $leastWatched = array_merge($leastWatched, array_slice($libraryStats, -10));
  195. }
  196. }
  197. return $leastWatched;
  198. }
  199. return [];
  200. }
  201. /**
  202. * Get library statistics for least watched calculation
  203. */
  204. private function getTautulliLibraryStats($url, $token, $sectionId, $days)
  205. {
  206. $endpoint = rtrim($url, '/') . '/api/v2';
  207. $params = [
  208. 'apikey' => $token,
  209. 'cmd' => 'get_library_media_info',
  210. 'section_id' => $sectionId,
  211. 'length' => 50,
  212. 'order_column' => 'play_count',
  213. 'order_dir' => 'asc'
  214. ];
  215. $response = $this->guzzle->request('GET', $endpoint, [
  216. 'query' => $params,
  217. 'timeout' => 15,
  218. 'connect_timeout' => 15,
  219. 'http_errors' => false
  220. ]);
  221. if ($response->getStatusCode() === 200) {
  222. $data = json_decode($response->getBody(), true);
  223. return $data['response']['data']['data'] ?? [];
  224. }
  225. return [];
  226. }
  227. /**
  228. * Get Emby watch statistics
  229. */
  230. private function getEmbyWatchStats($days = 30)
  231. {
  232. $embyUrl = $this->config['embyURL'] ?? '';
  233. $embyToken = $this->config['embyToken'] ?? '';
  234. if (empty($embyUrl) || empty($embyToken)) {
  235. return ['error' => true, 'message' => 'Emby URL or API key not configured'];
  236. }
  237. // Implement Emby-specific statistics gathering
  238. return $this->getGenericMediaServerStats('emby', $embyUrl, $embyToken, $days);
  239. }
  240. /**
  241. * Get Jellyfin watch statistics
  242. */
  243. private function getJellyfinWatchStats($days = 30)
  244. {
  245. $jellyfinUrl = $this->config['jellyfinURL'] ?? '';
  246. $jellyfinToken = $this->config['jellyfinToken'] ?? '';
  247. if (empty($jellyfinUrl) || empty($jellyfinToken)) {
  248. return ['error' => true, 'message' => 'Jellyfin URL or API key not configured'];
  249. }
  250. // Implement Jellyfin-specific statistics gathering
  251. return $this->getGenericMediaServerStats('jellyfin', $jellyfinUrl, $jellyfinToken, $days);
  252. }
  253. /**
  254. * Generic media server stats for Emby/Jellyfin
  255. */
  256. private function getGenericMediaServerStats($type, $url, $token, $days)
  257. {
  258. // Basic structure for now - can be expanded based on Emby/Jellyfin APIs
  259. return [
  260. 'period' => "{$days} days",
  261. 'start_date' => date('Y-m-d', strtotime("-{$days} days")),
  262. 'end_date' => date('Y-m-d'),
  263. 'message' => ucfirst($type) . ' statistics coming soon',
  264. 'most_watched' => [],
  265. 'least_watched' => [],
  266. 'user_stats' => [],
  267. 'recent_activity' => [],
  268. 'top_users' => []
  269. ];
  270. }
  271. /**
  272. * Format duration for display
  273. */
  274. private function formatDuration($seconds)
  275. {
  276. if ($seconds < 3600) {
  277. return gmdate('i:s', $seconds);
  278. } else {
  279. return gmdate('H:i:s', $seconds);
  280. }
  281. }
  282. /**
  283. * Get user avatar URL
  284. */
  285. private function getUserAvatar($userId, $mediaServer = 'plex')
  286. {
  287. switch ($mediaServer) {
  288. case 'plex':
  289. return $this->getPlexUserAvatar($userId);
  290. case 'emby':
  291. return $this->getEmbyUserAvatar($userId);
  292. case 'jellyfin':
  293. return $this->getJellyfinUserAvatar($userId);
  294. default:
  295. return '/plugins/images/organizr/user-bg.png';
  296. }
  297. }
  298. /**
  299. * Get Plex user avatar
  300. */
  301. private function getPlexUserAvatar($userId)
  302. {
  303. $tautulliUrl = $this->config['plexURL'] ?? '';
  304. $tautulliToken = $this->config['plexToken'] ?? '';
  305. if (empty($tautulliUrl) || empty($tautulliToken)) {
  306. return '/plugins/images/organizr/user-bg.png';
  307. }
  308. $endpoint = rtrim($tautulliUrl, '/') . "/api/v2";
  309. $params = [
  310. 'apikey' => $tautulliToken,
  311. 'cmd' => 'get_user_thumb',
  312. 'user_id' => $userId
  313. ];
  314. try {
  315. $response = $this->guzzle->request('GET', $endpoint, [
  316. 'query' => $params,
  317. 'timeout' => 10,
  318. 'connect_timeout' => 10,
  319. 'http_errors' => false
  320. ]);
  321. if ($response->getStatusCode() === 200) {
  322. $data = json_decode($response->getBody(), true);
  323. return $data['response']['data']['thumb'] ?? '/plugins/images/organizr/user-bg.png';
  324. }
  325. } catch (Exception $e) {
  326. // Return default avatar on error
  327. }
  328. return '/plugins/images/organizr/user-bg.png';
  329. }
  330. /**
  331. * Get Emby user avatar
  332. */
  333. private function getEmbyUserAvatar($userId)
  334. {
  335. // Implement Emby avatar logic
  336. return '/plugins/images/organizr/user-bg.png';
  337. }
  338. /**
  339. * Get Jellyfin user avatar
  340. */
  341. private function getJellyfinUserAvatar($userId)
  342. {
  343. // Implement Jellyfin avatar logic
  344. return '/plugins/images/organizr/user-bg.png';
  345. }
  346. }