Parcourir la source

added jellyfin homepage item

CauseFX il y a 5 ans
Parent
commit
4b062f08b6

+ 203 - 11
api/classes/organizr.class.php

@@ -32,6 +32,7 @@ class Organizr
 	use HealthChecksHomepageItem;
 	use ICalHomepageItem;
 	use JDownloaderHomepageItem;
+	use JellyfinHomepageItem;
 	use LidarrHomepageItem;
 	use MonitorrHomepageItem;
 	use NetDataHomepageItem;
@@ -1805,7 +1806,7 @@ class Organizr
 					'type' => 'input',
 					'name' => 'embyURL',
 					'class' => 'embyAuth switchAuth',
-					'label' => 'Emby/Jellyfin URL',
+					'label' => 'Emby URL',
 					'value' => $this->config['embyURL'],
 					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
 					'placeholder' => 'http(s)://hostname:port'
@@ -1814,10 +1815,27 @@ class Organizr
 					'type' => 'password-alt',
 					'name' => 'embyToken',
 					'class' => 'embyAuth switchAuth',
-					'label' => 'Emby/Jellyfin Token',
+					'label' => 'Emby Token',
 					'value' => $this->config['embyToken'],
 					'placeholder' => ''
 				),
+				array(
+					'type' => 'input',
+					'name' => 'jellyfinURL',
+					'class' => 'jellyfinAuth switchAuth',
+					'label' => 'Jellyfin URL',
+					'value' => $this->config['jellyfinURL'],
+					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'jellyfinToken',
+					'class' => 'jellyfinAuth switchAuth',
+					'label' => 'Jellyfin Token',
+					'value' => $this->config['jellyfinToken'],
+					'placeholder' => ''
+				),
 			),
 			'Security' => array(
 				array(
@@ -3743,6 +3761,30 @@ class Organizr
 				';
 				}
 				break;
+			case 'homepageOrderjellyfinnowplaying':
+				if ($this->config['homepageJellyfinStreams'] && $this->config['homepageJellyfinEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
+					$item .= '
+				<script>
+				// Jellyfin Stream
+				homepageStream("jellyfin", "' . $this->config['homepageStreamRefresh'] . '");
+				// End Jellyfin Stream
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderjellyfinrecent':
+				if ($this->config['homepageJellyfinRecent'] && $this->config['homepageJellyfinEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
+					$item .= '
+				<script>
+				// Jellyfin Recent
+				homepageRecent("jellyfin", "' . $this->config['homepageRecentRefresh'] . '");
+				// End Jellyfin Recent
+				</script>
+				';
+				}
+				break;
 			case 'homepageOrderombi':
 				if ($this->config['homepageOmbiEnabled']) {
 					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>';
@@ -4687,7 +4729,7 @@ class Organizr
 				)
 			),
 			array(
-				'name' => 'Emby-Jellyfin',
+				'name' => 'Emby',
 				'enabled' => strpos('personal', $this->config['license']) !== false,
 				'image' => 'plugins/images/tabs/emby.png',
 				'category' => 'Media Server',
@@ -4721,12 +4763,6 @@ class Organizr
 							'name' => 'embyToken',
 							'label' => 'Token',
 							'value' => $this->config['embyToken']
-						),
-						array(
-							'type' => 'switch',
-							'name' => 'homepageJellyfinInstead',
-							'label' => 'Jellyfin',
-							'value' => $this->config['homepageJellyfinInstead']
 						)
 					),
 					'Active Streams' => array(
@@ -4848,6 +4884,162 @@ class Organizr
 					)
 				)
 			),
+			array(
+				'name' => 'Jellyfin',
+				'enabled' => strpos('personal', $this->config['license']) !== false,
+				'image' => 'plugins/images/tabs/jellyfin.png',
+				'category' => 'Media Server',
+				'settings' => array(
+					'Enable' => array(
+						array(
+							'type' => 'switch',
+							'name' => 'homepageJellyfinEnabled',
+							'label' => 'Enable',
+							'value' => $this->config['homepageJellyfinEnabled']
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageJellyfinAuth',
+							'label' => 'Minimum Authentication',
+							'value' => $this->config['homepageJellyfinAuth'],
+							'options' => $groups
+						)
+					),
+					'Connection' => array(
+						array(
+							'type' => 'input',
+							'name' => 'jellyfinURL',
+							'label' => 'URL',
+							'value' => $this->config['jellyfinURL'],
+							'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+							'placeholder' => 'http(s)://hostname:port'
+						),
+						array(
+							'type' => 'password-alt',
+							'name' => 'jellyfinToken',
+							'label' => 'Token',
+							'value' => $this->config['jellyfinToken']
+						)
+					),
+					'Active Streams' => array(
+						array(
+							'type' => 'switch',
+							'name' => 'homepageJellyfinStreams',
+							'label' => 'Enable',
+							'value' => $this->config['homepageJellyfinStreams']
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageJellyStreamsAuth',
+							'label' => 'Minimum Authorization',
+							'value' => $this->config['homepageJellyStreamsAuth'],
+							'options' => $groups
+						),
+						array(
+							'type' => 'switch',
+							'name' => 'homepageShowStreamNames',
+							'label' => 'User Information',
+							'value' => $this->config['homepageShowStreamNames']
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageShowStreamNamesAuth',
+							'label' => 'Minimum Authorization',
+							'value' => $this->config['homepageShowStreamNamesAuth'],
+							'options' => $groups
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageStreamRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['homepageStreamRefresh'],
+							'options' => $this->optionTime()
+						),
+					),
+					'Recent Items' => array(
+						array(
+							'type' => 'switch',
+							'name' => 'homepageJellyfinRecent',
+							'label' => 'Enable',
+							'value' => $this->config['homepageJellyfinRecent']
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageJellyfinRecentAuth',
+							'label' => 'Minimum Authorization',
+							'value' => $this->config['homepageJellyfinRecentAuth'],
+							'options' => $groups
+						),
+						array(
+							'type' => 'number',
+							'name' => 'homepageRecentLimit',
+							'label' => 'Item Limit',
+							'value' => $this->config['homepageRecentLimit'],
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageRecentRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['homepageRecentRefresh'],
+							'options' => $this->optionTime()
+						),
+					),
+					'Misc Options' => array(
+						array(
+							'type' => 'input',
+							'name' => 'jellyfinTabName',
+							'label' => 'Jellyfin Tab Name',
+							'value' => $this->config['jellyfinTabName'],
+							'placeholder' => 'Only use if you have Jellyfin in a reverse proxy'
+						),
+						array(
+							'type' => 'input',
+							'name' => 'jellyfinTabURL',
+							'label' => 'Jellyfin Tab WAN URL',
+							'value' => $this->config['jellyfinTabURL'],
+							'placeholder' => 'http(s)://hostname:port'
+						),
+						array(
+							'type' => 'select',
+							'name' => 'cacheImageSize',
+							'label' => 'Image Cache Size',
+							'value' => $this->config['cacheImageSize'],
+							'options' => array(
+								array(
+									'name' => 'Low',
+									'value' => '.5'
+								),
+								array(
+									'name' => '1x',
+									'value' => '1'
+								),
+								array(
+									'name' => '2x',
+									'value' => '2'
+								),
+								array(
+									'name' => '3x',
+									'value' => '3'
+								)
+							)
+						)
+					),
+					'Test Connection' => array(
+						array(
+							'type' => 'blank',
+							'label' => 'Please Save before Testing'
+						),
+						array(
+							'type' => 'button',
+							'label' => '',
+							'icon' => 'fa fa-flask',
+							'class' => 'pull-right',
+							'text' => 'Test Connection',
+							'attr' => 'onclick="testAPIConnection(\'jellyfin\')"'
+						),
+					)
+				)
+			),
 			array(
 				'name' => 'JDownloader',
 				'enabled' => strpos('personal', $this->config['license']) !== false,
@@ -8123,8 +8315,8 @@ class Organizr
 	public function allJellyfinUsers($newOnly = false)
 	{
 		try {
-			if (!empty($this->config['embyURL']) && !empty($this->config['embyToken'])) {
-				$url = $this->qualifyURL($this->config['embyURL']) . '/Users?api_key=' . $this->config['embyToken'];
+			if (!empty($this->config['jellyfinURL']) && !empty($this->config['jellyfinToken'])) {
+				$url = $this->qualifyURL($this->config['jellyfinURL']) . '/Users?api_key=' . $this->config['jellyfinToken'];
 				$headers = array();
 				$response = Requests::get($url, $headers);
 				if ($response->success) {

+ 13 - 2
api/functions/organizr-functions.php

@@ -193,10 +193,10 @@ trait OrganizrFunctions
 		if (!empty($this->config['plexToken'])) {
 			$buttons .= '<button class="btn m-b-20 m-r-20 bg-plex text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'plex\')" type="button"><span class="btn-label"><i class="mdi mdi-plex"></i></span><span lang="en">Import Plex Users</span></button>';
 		}
-		if (!empty($this->config['embyURL']) && !empty($this->config['embyToken']) && ($this->config['homepageJellyfinInstead'])) {
+		if (!empty($this->config['jellyfinURL']) && !empty($this->config['jellyfinToken'])) {
 			$buttons .= '<button class="btn m-b-20 m-r-20 bg-primary text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'jellyfin\')" type="button"><span class="btn-label"><i class="mdi mdi-fish"></i></span><span lang="en">Import Jellyfin Users</span></button>';
 		}
-		if (!empty($this->config['embyURL']) && !empty($this->config['embyToken']) && (!$this->config['homepageJellyfinInstead'])) {
+		if (!empty($this->config['embyURL']) && !empty($this->config['embyToken'])) {
 			$buttons .= '<button class="btn m-b-20 m-r-20 bg-emby text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'emby\')" type="button"><span class="btn-label"><i class="mdi mdi-emby"></i></span><span lang="en">Import Jellyfin Users</span></button>';
 		}
 		return ($buttons !== '') ? $buttons : $emptyButtons;
@@ -235,6 +235,17 @@ trait OrganizrFunctions
 				}
 				$image_src = $embyAddress . '/Items/' . $image_url . '/Images/' . $itemType . '?' . implode('&', $imgParams);
 				break;
+			case 'jellyfin':
+				$jellyfinAddress = $this->qualifyURL($this->config['jellyfinURL']);
+				$imgParams = array();
+				if (isset($_GET['height'])) {
+					$imgParams['height'] = 'maxHeight=' . $_GET['height'];
+				}
+				if (isset($_GET['width'])) {
+					$imgParams['width'] = 'maxWidth=' . $_GET['width'];
+				}
+				$image_src = $jellyfinAddress . '/Items/' . $image_url . '/Images/' . $itemType . '?' . implode('&', $imgParams);
+				break;
 			default:
 				# code...
 				break;

+ 2 - 2
api/homepage/emby.php

@@ -140,7 +140,7 @@ trait EmbyHomepageItem
 		$embyItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
 		$embyItem['userThumb'] = '';
 		$embyItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
-		$embyURL = ($this->config['homepageJellyfinInstead']) ? $this->config['embyURL'] . '/web/index.html#!/itemdetails.html?id=' : 'https://app.emby.media/#!/item/item.html?id=';
+		$embyURL = 'https://app.emby.media/#!/item/item.html?id=';
 		$embyItem['address'] = $this->config['embyTabURL'] ? rtrim($this->config['embyTabURL'], '/') . "/web/#!/item/item.html?id=" . $embyItem['uid'] : $embyURL . $embyItem['uid'] . "&serverId=" . $embyItem['id'];
 		$embyItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '$' . $this->randString();
 		$embyItem['originalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '$' . $this->randString();
@@ -381,7 +381,7 @@ trait EmbyHomepageItem
 		}
 		$key = $array['key'] ?? null;
 		if (!$key) {
-			$this->setAPIResponse('error', 'Plex Metadata key is not defined', 422);
+			$this->setAPIResponse('error', 'Emby Metadata key is not defined', 422);
 			return false;
 		}
 		$url = $this->qualifyURL($this->config['embyURL']);

+ 439 - 0
api/homepage/jellyfin.php

@@ -0,0 +1,439 @@
+<?php
+
+trait JellyfinHomepageItem
+{
+	public function testConnectionJellyfin()
+	{
+		if (empty($this->config['jellyfinURL'])) {
+			$this->setAPIResponse('error', 'Jellyfin URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['jellyfinToken'])) {
+			$this->setAPIResponse('error', 'Jellyfin Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$url = $url . "/Users?api_key=" . $this->config['jellyfinToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$json = json_decode($response->body);
+				if (is_array($json) || is_object($json)) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				} else {
+					$this->setAPIResponse('error', 'URL or token incorrect', 409);
+					return false;
+				}
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Connection Error', 500);
+				return true;
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function resolveJellyfinItem($itemDetails)
+	{
+		$item = isset($itemDetails['NowPlayingItem']['Id']) ? $itemDetails['NowPlayingItem'] : $itemDetails;
+		// Static Height & Width
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		$actorHeight = 450;
+		$actorWidth = 300;
+		// Cache Directories
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheDirectoryWeb = 'plugins/images/cache/';
+		// Types
+		switch (@$item['Type']) {
+			case 'Series':
+				$jellyfinItem['type'] = 'tv';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = $item['Id'];
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['BackdropImageTags'][0]) ? 'Backdrop' : '');
+				break;
+			case 'Episode':
+				$jellyfinItem['type'] = 'tv';
+				$jellyfinItem['title'] = $item['SeriesName'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']);
+				$jellyfinItem['key'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']) . "-list";
+				$jellyfinItem['nowPlayingThumb'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] : false);
+				$jellyfinItem['nowPlayingKey'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] . '-np' : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] . '-np' : false);
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['ParentBackdropImageTags'][0]) ? 'Backdrop' : '');
+				$jellyfinItem['nowPlayingTitle'] = @$item['SeriesName'] . ' - ' . @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = 'S' . @$item['ParentIndexNumber'] . ' · E' . @$item['IndexNumber'];
+				break;
+			case 'MusicAlbum':
+			case 'Audio':
+				$jellyfinItem['type'] = 'music';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = (isset($item['AlbumId']) ? $item['AlbumId'] : @$item['ParentBackdropItemId']);
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = isset($item['AlbumId']) ? $item['AlbumId'] : $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = (isset($item['ParentBackdropItemId']) ? "Primary" : "Backdrop");
+				$jellyfinItem['nowPlayingTitle'] = @$item['AlbumArtist'] . ' - ' . @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = @$item['Album'];
+				break;
+			case 'Movie':
+				$jellyfinItem['type'] = 'movie';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = $item['Id'];
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$jellyfinItem['nowPlayingTitle'] = @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			case 'Video':
+				$jellyfinItem['type'] = 'video';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = $item['Id'];
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$jellyfinItem['nowPlayingTitle'] = @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			default:
+				return false;
+		}
+		$jellyfinItem['uid'] = $item['Id'];
+		$jellyfinItem['imageType'] = (isset($item['ImageTags']['Primary']) ? "Primary" : false);
+		$jellyfinItem['elapsed'] = isset($itemDetails['PlayState']['PositionTicks']) && $itemDetails['PlayState']['PositionTicks'] !== '0' ? (int)$itemDetails['PlayState']['PositionTicks'] : null;
+		$jellyfinItem['duration'] = isset($itemDetails['NowPlayingItem']['RunTimeTicks']) ? (int)$itemDetails['NowPlayingItem']['RunTimeTicks'] : (int)(isset($item['RunTimeTicks']) ? $item['RunTimeTicks'] : '');
+		$jellyfinItem['watched'] = ($jellyfinItem['elapsed'] && $jellyfinItem['duration'] ? floor(($jellyfinItem['elapsed'] / $jellyfinItem['duration']) * 100) : 0);
+		$jellyfinItem['transcoded'] = isset($itemDetails['TranscodingInfo']['CompletionPercentage']) ? floor((int)$itemDetails['TranscodingInfo']['CompletionPercentage']) : 100;
+		$jellyfinItem['stream'] = @$itemDetails['PlayState']['PlayMethod'];
+		$jellyfinItem['id'] = $item['ServerId'];
+		$jellyfinItem['session'] = @$itemDetails['DeviceId'];
+		$jellyfinItem['bandwidth'] = isset($itemDetails['TranscodingInfo']['Bitrate']) ? $itemDetails['TranscodingInfo']['Bitrate'] / 1000 : '';
+		$jellyfinItem['bandwidthType'] = 'wan';
+		$jellyfinItem['sessionType'] = (@$itemDetails['PlayState']['PlayMethod'] == 'Transcode') ? 'Transcoding' : 'Direct Playing';
+		$jellyfinItem['state'] = ((@(string)$itemDetails['PlayState']['IsPaused'] == '1') ? "pause" : "play");
+		$jellyfinItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
+		$jellyfinItem['userThumb'] = '';
+		$jellyfinItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
+		$jellyfinURL = $this->config['jellyfinURL'] . '/web/index.html#!/itemdetails.html?id=';
+		$jellyfinItem['address'] = $this->config['jellyfinTabURL'] ? rtrim($this->config['jellyfinTabURL'], '/') . "/web/#!/item/item.html?id=" . $jellyfinItem['uid'] : $jellyfinURL . $jellyfinItem['uid'] . "&serverId=" . $jellyfinItem['id'];
+		$jellyfinItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '$' . $this->randString();
+		$jellyfinItem['originalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '$' . $this->randString();
+		$jellyfinItem['openTab'] = $this->config['jellyfinTabURL'] && $this->config['jellyfinTabName'] ? true : false;
+		$jellyfinItem['tabName'] = $this->config['jellyfinTabName'] ? $this->config['jellyfinTabName'] : '';
+		// Stream info
+		$jellyfinItem['userStream'] = array(
+			'platform' => @(string)$itemDetails['Client'],
+			'product' => @(string)$itemDetails['Client'],
+			'device' => @(string)$itemDetails['DeviceName'],
+			'stream' => @$itemDetails['PlayState']['PlayMethod'],
+			'videoResolution' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]['Width']) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Width'] : '',
+			'throttled' => false,
+			'sourceVideoCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : '',
+			'videoCodec' => @$itemDetails['TranscodingInfo']['VideoCodec'],
+			'audioCodec' => @$itemDetails['TranscodingInfo']['AudioCodec'],
+			'sourceAudioCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][1]) ? $itemDetails['NowPlayingItem']['MediaStreams'][1]['Codec'] : (isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : ''),
+			'videoDecision' => $this->streamType(@$itemDetails['PlayState']['PlayMethod']),
+			'audioDecision' => $this->streamType(@$itemDetails['PlayState']['PlayMethod']),
+			'container' => isset($itemDetails['NowPlayingItem']['Container']) ? $itemDetails['NowPlayingItem']['Container'] : '',
+			'audioChannels' => @$itemDetails['TranscodingInfo']['AudioChannels']
+		);
+		// Genre catch all
+		if (isset($item['Genres'])) {
+			$genres = array();
+			foreach ($item['Genres'] as $genre) {
+				$genres[] = $genre;
+			}
+		}
+		// Actor catch all
+		if (isset($item['People'])) {
+			$actors = array();
+			foreach ($item['People'] as $key => $value) {
+				if (@$value['PrimaryImageTag'] && @$value['Role']) {
+					if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+						$actorImage = $cacheDirectoryWeb . (string)$value['Id'] . '-cast.jpg';
+					}
+					if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg') && (time() - 604800) > filemtime($cacheDirectory . (string)$value['Id'] . '-cast.jpg') || !file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+						$actorImage = 'api/v2/homepage/image?source=jellyfin&type=Primary&img=' . (string)$value['Id'] . '&height=' . $actorHeight . '&width=' . $actorWidth . '&key=' . (string)$value['Id'] . '-cast';
+					}
+					$actors[] = array(
+						'name' => (string)$value['Name'],
+						'role' => (string)$value['Role'],
+						'thumb' => $actorImage
+					);
+				}
+			}
+		}
+		// Metadata information
+		$jellyfinItem['metadata'] = array(
+			'guid' => $item['Id'],
+			'summary' => @(string)$item['Overview'],
+			'rating' => @(string)$item['CommunityRating'],
+			'duration' => @(string)$item['RunTimeTicks'],
+			'originallyAvailableAt' => @(string)$item['PremiereDate'],
+			'year' => (string)isset($item['ProductionYear']) ? $item['ProductionYear'] : '',
+			//'studio' => (string)$item['studio'],
+			'tagline' => @(string)$item['Taglines'][0],
+			'genres' => (isset($item['Genres'])) ? $genres : '',
+			'actors' => (isset($item['People'])) ? $actors : ''
+		);
+		if (file_exists($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg')) {
+			$jellyfinItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $jellyfinItem['nowPlayingKey'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $jellyfinItem['key'] . '.jpg')) {
+			$jellyfinItem['imageURL'] = $cacheDirectoryWeb . $jellyfinItem['key'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg')) {
+			$jellyfinItem['nowPlayingImageURL'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '';
+		}
+		if (file_exists($cacheDirectory . $jellyfinItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $jellyfinItem['key'] . '.jpg') || !file_exists($cacheDirectory . $jellyfinItem['key'] . '.jpg')) {
+			$jellyfinItem['imageURL'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '';
+		}
+		if (!$jellyfinItem['nowPlayingThumb']) {
+			$jellyfinItem['nowPlayingOriginalImage'] = $jellyfinItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+			$jellyfinItem['nowPlayingKey'] = "no-np";
+		}
+		if (!$jellyfinItem['thumb']) {
+			$jellyfinItem['originalImage'] = $jellyfinItem['imageURL'] = "plugins/images/cache/no-list.png";
+			$jellyfinItem['key'] = "no-list";
+		}
+		if (isset($useImage)) {
+			$jellyfinItem['useImage'] = $useImage;
+		}
+		return $jellyfinItem;
+	}
+	
+	public function getJellyfinHomepageStreams()
+	{
+		if (!$this->config['homepageJellyfinEnabled']) {
+			$this->setAPIResponse('error', 'Jellyfin homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageJellyfinStreams']) {
+			$this->setAPIResponse('error', 'Jellyfin homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageJellyfinAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageJellyStreamsAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['jellyfinURL'])) {
+			$this->setAPIResponse('error', 'Jellyfin URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['jellyfinToken'])) {
+			$this->setAPIResponse('error', 'Jellyfin Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$url = $url . '/Sessions?api_key=' . $this->config['jellyfinToken'] . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveJellyfinItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getJellyfinHomepageRecent()
+	{
+		if (!$this->config['homepageJellyfinEnabled']) {
+			$this->setAPIResponse('error', 'Jellyfin homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageJellyfinRecent']) {
+			$this->setAPIResponse('error', 'Jellyfin homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageJellyfinAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageJellyfinRecentAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['jellyfinURL'])) {
+			$this->setAPIResponse('error', 'Jellyfin URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['jellyfinToken'])) {
+			$this->setAPIResponse('error', 'Jellyfin Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$username = false;
+		$showPlayed = false;
+		$userId = 0;
+		try {
+			if (isset($this->user['username'])) {
+				$username = strtolower($this->user['username']);
+			}
+			// Get A User
+			$userIds = $url . "/Users?api_key=" . $this->config['jellyfinToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $value) { // Scan for admin user
+					if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+						$userId = $value['Id'];
+					}
+					if ($username && strtolower($value['Name']) == $username) {
+						$userId = $value['Id'];
+						$showPlayed = false;
+						break;
+					}
+				}
+				$url = $url . '/Users/' . $userId . '/Items/Latest?EnableImages=true&Limit=' . $this->config['homepageRecentLimit'] . '&api_key=' . $this->config['jellyfinToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveJellyfinItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getJellyfinHomepageMetadata($array)
+	{
+		if (!$this->config['homepageJellyfinEnabled']) {
+			$this->setAPIResponse('error', 'Jellyfin homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageJellyfinAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['jellyfinURL'])) {
+			$this->setAPIResponse('error', 'Jellyfin URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['jellyfinToken'])) {
+			$this->setAPIResponse('error', 'Jellyfin Token is not defined', 422);
+			return false;
+		}
+		$key = $array['key'] ?? null;
+		if (!$key) {
+			$this->setAPIResponse('error', 'Jellyfin Metadata key is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$username = false;
+		$showPlayed = false;
+		$userId = 0;
+		try {
+			if (isset($this->user['username'])) {
+				$username = strtolower($this->user['username']);
+			}
+			// Get A User
+			$userIds = $url . "/Users?api_key=" . $this->config['jellyfinToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $value) { // Scan for admin user
+					if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+						$userId = $value['Id'];
+					}
+					if ($username && strtolower($value['Name']) == $username) {
+						$userId = $value['Id'];
+						$showPlayed = false;
+						break;
+					}
+				}
+				$url = $url . '/Users/' . $userId . '/Items/' . $key . '?EnableImages=true&Limit=' . $this->config['homepageRecentLimit'] . '&api_key=' . $this->config['jellyfinToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$jellyfin = json_decode($response->body, true);
+				if (isset($jellyfin['NowPlayingItem']) || isset($jellyfin['Name'])) {
+					$items[] = $this->resolveJellyfinItem($jellyfin);
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+}

+ 23 - 0
api/v2/routes/connectionTester.php

@@ -93,6 +93,29 @@ $app->post('/test/emby', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 	
+});
+$app->post('/test/jellyfin', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/jellyfin",
+	 *     summary="Test connection to Jellyfin",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->testConnectionJellyfin($Organizr->apiData($request));
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
 });
 $app->post('/test/sabnzbd', function ($request, $response, $args) {
 	/**

+ 11 - 2
api/v2/routes/homepage.php

@@ -35,6 +35,15 @@ $app->get('/homepage/emby/streams', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 	
+});
+$app->get('/homepage/jellyfin/streams', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->getJellyfinHomepageStreams();
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
 });
 $app->get('/homepage/plex/recent', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
@@ -56,7 +65,7 @@ $app->get('/homepage/emby/recent', function ($request, $response, $args) {
 });
 $app->get('/homepage/jellyfin/recent', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	$Organizr->getEmbyHomepageRecent();
+	$Organizr->getJellyfinHomepageRecent();
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
@@ -92,7 +101,7 @@ $app->post('/homepage/emby/metadata', function ($request, $response, $args) {
 });
 $app->post('/homepage/jellyfin/metadata', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	$Organizr->getEmbyHomepageMetadata($Organizr->apiData($request));
+	$Organizr->getJellyfinHomepageMetadata($Organizr->apiData($request));
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')

+ 9 - 6
js/functions.js

@@ -4358,7 +4358,7 @@ function buildStreamItem(array,source){
 	var cards = '';
 	var count = 0;
 	var total = array.length;
-    var sourceIcon = (source === 'jellyfin' && activeInfo.settings.homepage.media.jellyfin) ? 'play' : source;
+    var sourceIcon = (source === 'jellyfin') ? 'play' : source;
     var streamDetails = {
         direct: 0,
         transcode: 0
@@ -4694,7 +4694,7 @@ function buildRequestItem(array, extra=null){
 function buildStream(array, type){
 	var streams = (typeof array.content !== 'undefined') ? array.content.length : false;
 	var originalType = type;
-    type = (type === 'emby' && activeInfo.settings.homepage.media.jellyfin) ? 'jellyfin' : type;
+    //type = (type === 'emby' && activeInfo.settings.homepage.media.jellyfin) ? 'jellyfin' : type;
 	return (streams) ? `
 	<div id="`+type+`Streams">
 		<div class="el-element-overlay row">
@@ -4722,7 +4722,7 @@ function buildRecent(array, type){
 	var header = '';
 	var headerAlt = '';
 	var refreshType = type;
-	type = (type === 'emby' && activeInfo.settings.homepage.media.jellyfin) ? 'jellyfin' : type;
+	//type = (type === 'emby' && activeInfo.settings.homepage.media.jellyfin) ? 'jellyfin' : type;
 	dropdown += (recent && movie) ? `<li><a data-filter="recent-movie" server-filter="`+type+`" href="javascript:void(0);">Movies</a></li>` : '';
 	dropdown += (recent && tv) ? `<li><a data-filter="recent-tv" server-filter="`+type+`" href="javascript:void(0);">Shows</a></li>` : '';
 	dropdown += (recent && video) ? `<li><a data-filter="recent-video" server-filter="`+type+`" href="javascript:void(0);">Videos</a></li>` : '';
@@ -5932,7 +5932,7 @@ function buildMetadata(array, source){
 	var genres = '';
 	var actors = '';
 	var rating = '<div class="col-xs-2 p-10"></div>';
-    var sourceIcon = (source === 'jellyfin' && activeInfo.settings.homepage.media.jellyfin) ? 'play' : source;
+    var sourceIcon = (source === 'jellyfin') ? 'play' : source;
 	$.each(array.content, function(i,v) {
 		var hasActor = (typeof v.metadata.actors !== 'string') ? true : false;
 		var hasGenre = (typeof v.metadata.genres !== 'string') ? true : false;
@@ -6580,7 +6580,6 @@ function homepageRecent(type, timeout){
 			break;
 		case 'emby':
 		case 'jellyfin':
-			type = 'emby';
 			var action = 'getEmbyRecent';
 			break;
 		default:
@@ -8601,11 +8600,15 @@ function changeAuth(){
         case 'emby_local':
         case 'emby_connect':
         case 'emby_all':
-	    case 'jellyfin':
             $('.switchAuth').parent().parent().parent().hide();
             $('.backendAuth').parent().parent().parent().show();
             $('.embyAuth').parent().parent().parent().show();
             break;
+	    case 'jellyfin':
+		    $('.switchAuth').parent().parent().parent().hide();
+		    $('.backendAuth').parent().parent().parent().show();
+		    $('.jellyfinAuth').parent().parent().parent().show();
+		    break;
         case 'ftp':
             $('.switchAuth').parent().parent().parent().hide();
             $('.backendAuth').parent().parent().parent().show();