Browse Source

add new homepage item files

CauseFX 5 years ago
parent
commit
1a99f03950

+ 39 - 0
api/homepage/calendar.php

@@ -0,0 +1,39 @@
+<?php
+
+trait CalendarHomepageItem
+{
+	public function getCalendar()
+	{
+		$startDate = date('Y-m-d', strtotime("-" . $this->config['calendarStart'] . " days"));
+		$endDate = date('Y-m-d', strtotime("+" . $this->config['calendarEnd'] . " days"));
+		$icalCalendarSources = array();
+		$calendarItems = array();
+		// SONARR CONNECT
+		$items = $this->getSonarrCalendar($startDate, $endDate);
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// LIDARR CONNECT
+		$items = $this->getLidarrCalendar($startDate, $endDate);
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// RADARR CONNECT
+		$items = $this->getRadarrCalendar($startDate, $endDate);
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// SICKRAGE/BEARD/MEDUSA CONNECT
+		$items = $this->getSickRageCalendar();
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// COUCHPOTATO CONNECT
+		$items = $this->getCouchPotatoCalendar();
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// iCal URL
+		$calendarSources['ical'] = $this->getICalendar();
+		unset($items);
+		// Finish
+		$calendarSources['events'] = $calendarItems;
+		$this->setAPIResponse('success', null, 200, $calendarSources);
+		return $calendarSources;
+	}
+}

+ 133 - 0
api/homepage/couchpotato.php

@@ -0,0 +1,133 @@
+<?php
+
+trait CouchPotatoHomepageItem
+{
+	public function getCouchPotatoCalendar()
+	{
+		if (!$this->config['homepageCouchpotatoEnabled']) {
+			$this->setAPIResponse('error', 'CouchPotato homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageCouchpotatoAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['couchpotatoURL'])) {
+			$this->setAPIResponse('error', 'Radarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['couchpotatoToken'])) {
+			$this->setAPIResponse('error', 'Radarr Token is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['couchpotatoURL'], $this->config['couchpotatoToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\CouchPotato\CouchPotato($value['url'], $value['token']);
+				$calendar = $this->formatCouchCalendar($downloader->getMediaList(array('status' => 'active,done')), $key);
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($calendar)) {
+				$calendarItems = array_merge($calendarItems, $calendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatCouchCalendar($array, $number)
+	{
+		$api = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($api['movies'] as $child) {
+			$i++;
+			$movieName = $child['info']['original_title'];
+			$movieID = $child['info']['tmdb_id'];
+			if (!isset($movieID)) {
+				$movieID = "";
+			}
+			$physicalRelease = (isset($child['info']['released']) ? $child['info']['released'] : null);
+			$backupRelease = (isset($child['info']['release_date']['theater']) ? $child['info']['release_date']['theater'] : null);
+			$physicalRelease = (isset($physicalRelease) ? $physicalRelease : $backupRelease);
+			$physicalRelease = strtotime($physicalRelease);
+			$physicalRelease = date("Y-m-d", $physicalRelease);
+			$oldestDay = new DateTime ($this->currentTime);
+			$oldestDay->modify('-' . $this->config['calendarStart'] . ' days');
+			$newestDay = new DateTime ($this->currentTime);
+			$newestDay->modify('+' . $this->config['calendarEnd'] . ' days');
+			$startDt = new DateTime ($physicalRelease);
+			$calendarStartDiff = date_diff($startDt, $newestDay);
+			$calendarEndDiff = date_diff($startDt, $oldestDay);
+			if (!$this->calendarDaysCheck($calendarStartDiff->format('%R') . $calendarStartDiff->days, $calendarEndDiff->format('%R') . $calendarEndDiff->days)) {
+				continue;
+			}
+			if (new DateTime() < $startDt) {
+				$notReleased = "true";
+			} else {
+				$notReleased = "false";
+			}
+			$downloaded = ($child['status'] == "active") ? "0" : "1";
+			if ($downloaded == "0" && $notReleased == "true") {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			if (!empty($child['info']['images']['backdrop_original'])) {
+				$banner = $child['info']['images']['backdrop_original'][0];
+			} elseif (!empty($child['info']['images']['backdrop'])) {
+				$banner = $child['info']['images']['backdrop_original'][0];
+			} else {
+				$banner = "/plugins/images/cache/no-np.png";
+			}
+			if ($banner !== "/plugins/images/cache/no-np.png") {
+				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+				$imageURL = $banner;
+				$cacheFile = $cacheDirectory . $movieID . '.jpg';
+				$banner = 'plugins/images/cache/' . $movieID . '.jpg';
+				if (!file_exists($cacheFile)) {
+					$this->cacheImage($imageURL, $movieID);
+					unset($imageURL);
+					unset($cacheFile);
+				}
+			}
+			$hasFile = (!empty($child['releases']) && !empty($child['releases'][0]['files']['movie']));
+			$details = array(
+				"topTitle" => $movieName,
+				"bottomTitle" => $child['info']['tagline'],
+				"status" => $child['status'],
+				"overview" => $child['info']['plot'],
+				"runtime" => $child['info']['runtime'],
+				"image" => $banner,
+				"ratings" => isset($child['info']['rating']['imdb'][0]) ? $child['info']['rating']['imdb'][0] : '',
+				"videoQuality" => $hasFile ? $child['releases'][0]['quality'] : "unknown",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"genres" => $child['info']['genres'],
+				"year" => isset($child['info']['year']) ? $child['info']['year'] : '',
+				"studio" => isset($child['info']['year']) ? $child['info']['year'] : '',
+			);
+			array_push($gotCalendar, array(
+				"id" => "CouchPotato-" . $number . "-" . $i,
+				"title" => $movieName,
+				"start" => $physicalRelease,
+				"className" => "inline-popups bg-calendar calendar-item movieID--" . $movieID,
+				"imagetype" => "film " . $downloaded,
+				"imagetypeFilter" => "film",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details
+			));
+			
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 82 - 0
api/homepage/deluge.php

@@ -0,0 +1,82 @@
+<?php
+
+trait DelugeHomepageItem
+{
+	public function testConnectionDeluge()
+	{
+		if (empty($this->config['delugeURL'])) {
+			$this->setAPIResponse('error', 'Deluge URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['delugePassword'])) {
+			$this->setAPIResponse('error', 'Deluge Password is not defined', 422);
+			return false;
+		}
+		try {
+			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']));
+			$torrents = $deluge->getTorrents(null, 'comment, download_payload_rate, eta, hash, is_finished, is_seed, message, name, paused, progress, queue, state, total_size, upload_payload_rate');
+			$this->setAPIResponse('success', 'API Connection succeeded', 200);
+			return true;
+		} catch (Exception $e) {
+			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+		
+	}
+	
+	public function delugeStatus($queued, $status, $state)
+	{
+		if ($queued == '-1' && $state == '100' && ($status == 'Seeding' || $status == 'Queued' || $status == 'Paused')) {
+			$state = 'Seeding';
+		} elseif ($state !== '100') {
+			$state = 'Downloading';
+		} else {
+			$state = 'Finished';
+		}
+		return ($state) ? $state : $status;
+	}
+	
+	public function getDelugeHomepageQueue()
+	{
+		if (!$this->config['homepageDelugeEnabled']) {
+			$this->setAPIResponse('error', 'Deluge homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageDelugeAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['delugeURL'])) {
+			$this->setAPIResponse('error', 'Deluge URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['delugePassword'])) {
+			$this->setAPIResponse('error', 'Deluge Password is not defined', 422);
+			return false;
+		}
+		try {
+			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']));
+			$torrents = $deluge->getTorrents(null, 'comment, download_payload_rate, eta, hash, is_finished, is_seed, message, name, paused, progress, queue, state, total_size, upload_payload_rate');
+			foreach ($torrents as $key => $value) {
+				$tempStatus = $this->delugeStatus($value->queue, $value->state, $value->progress);
+				if ($tempStatus == 'Seeding' && $this->config['delugeHideSeeding']) {
+					//do nothing
+				} elseif ($tempStatus == 'Finished' && $this->config['delugeHideCompleted']) {
+					//do nothing
+				} else {
+					$api['content']['queueItems'][] = $value;
+				}
+			}
+			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
+			$api['content']['historyItems'] = false;
+		} catch (Excecption $e) {
+			$this->writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 439 - 0
api/homepage/emby.php

@@ -0,0 +1,439 @@
+<?php
+
+trait EmbyHomepageItem
+{
+	public function testConnectionEmby()
+	{
+		if (empty($this->config['embyURL'])) {
+			$this->setAPIResponse('error', 'Emby URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['embyToken'])) {
+			$this->setAPIResponse('error', 'Emby Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$url = $url . "/Users?api_key=" . $this->config['embyToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Emby Connection Error', 500);
+				return true;
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function resolveEmbyItem($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
+		//$embyItem['array-item'] = $item;
+		//$embyItem['array-itemdetails'] = $itemDetails;
+		switch (@$item['Type']) {
+			case 'Series':
+				$embyItem['type'] = 'tv';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = $item['Id'];
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['BackdropImageTags'][0]) ? 'Backdrop' : '');
+				break;
+			case 'Episode':
+				$embyItem['type'] = 'tv';
+				$embyItem['title'] = $item['SeriesName'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']);
+				$embyItem['key'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']) . "-list";
+				$embyItem['nowPlayingThumb'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] : false);
+				$embyItem['nowPlayingKey'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] . '-np' : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] . '-np' : false);
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['ParentBackdropImageTags'][0]) ? 'Backdrop' : '');
+				$embyItem['nowPlayingTitle'] = @$item['SeriesName'] . ' - ' . @$item['Name'];
+				$embyItem['nowPlayingBottom'] = 'S' . @$item['ParentIndexNumber'] . ' · E' . @$item['IndexNumber'];
+				break;
+			case 'MusicAlbum':
+			case 'Audio':
+				$embyItem['type'] = 'music';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = (isset($item['AlbumId']) ? $item['AlbumId'] : @$item['ParentBackdropItemId']);
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = isset($item['AlbumId']) ? $item['AlbumId'] : $item['Id'];
+				$embyItem['nowPlayingImageType'] = (isset($item['ParentBackdropItemId']) ? "Primary" : "Backdrop");
+				$embyItem['nowPlayingTitle'] = @$item['AlbumArtist'] . ' - ' . @$item['Name'];
+				$embyItem['nowPlayingBottom'] = @$item['Album'];
+				break;
+			case 'Movie':
+				$embyItem['type'] = 'movie';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = $item['Id'];
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$embyItem['nowPlayingTitle'] = @$item['Name'];
+				$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			case 'Video':
+				$embyItem['type'] = 'video';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = $item['Id'];
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$embyItem['nowPlayingTitle'] = @$item['Name'];
+				$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			default:
+				return false;
+		}
+		$embyItem['uid'] = $item['Id'];
+		$embyItem['imageType'] = (isset($item['ImageTags']['Primary']) ? "Primary" : false);
+		$embyItem['elapsed'] = isset($itemDetails['PlayState']['PositionTicks']) && $itemDetails['PlayState']['PositionTicks'] !== '0' ? (int)$itemDetails['PlayState']['PositionTicks'] : null;
+		$embyItem['duration'] = isset($itemDetails['NowPlayingItem']['RunTimeTicks']) ? (int)$itemDetails['NowPlayingItem']['RunTimeTicks'] : (int)(isset($item['RunTimeTicks']) ? $item['RunTimeTicks'] : '');
+		$embyItem['watched'] = ($embyItem['elapsed'] && $embyItem['duration'] ? floor(($embyItem['elapsed'] / $embyItem['duration']) * 100) : 0);
+		$embyItem['transcoded'] = isset($itemDetails['TranscodingInfo']['CompletionPercentage']) ? floor((int)$itemDetails['TranscodingInfo']['CompletionPercentage']) : 100;
+		$embyItem['stream'] = @$itemDetails['PlayState']['PlayMethod'];
+		$embyItem['id'] = $item['ServerId'];
+		$embyItem['session'] = @$itemDetails['DeviceId'];
+		$embyItem['bandwidth'] = isset($itemDetails['TranscodingInfo']['Bitrate']) ? $itemDetails['TranscodingInfo']['Bitrate'] / 1000 : '';
+		$embyItem['bandwidthType'] = 'wan';
+		$embyItem['sessionType'] = (@$itemDetails['PlayState']['PlayMethod'] == 'Transcode') ? 'Transcoding' : 'Direct Playing';
+		$embyItem['state'] = ((@(string)$itemDetails['PlayState']['IsPaused'] == '1') ? "pause" : "play");
+		$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=';
+		$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();
+		$embyItem['openTab'] = $this->config['embyTabURL'] && $this->config['embyTabName'] ? true : false;
+		$embyItem['tabName'] = $this->config['embyTabName'] ? $this->config['embyTabName'] : '';
+		// Stream info
+		$embyItem['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=emby&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
+		$embyItem['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 . $embyItem['nowPlayingKey'] . '.jpg')) {
+			$embyItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $embyItem['nowPlayingKey'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
+			$embyItem['imageURL'] = $cacheDirectoryWeb . $embyItem['key'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
+			$embyItem['nowPlayingImageURL'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '';
+		}
+		if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['key'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
+			$embyItem['imageURL'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '';
+		}
+		if (!$embyItem['nowPlayingThumb']) {
+			$embyItem['nowPlayingOriginalImage'] = $embyItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+			$embyItem['nowPlayingKey'] = "no-np";
+		}
+		if (!$embyItem['thumb']) {
+			$embyItem['originalImage'] = $embyItem['imageURL'] = "plugins/images/cache/no-list.png";
+			$embyItem['key'] = "no-list";
+		}
+		if (isset($useImage)) {
+			$embyItem['useImage'] = $useImage;
+		}
+		return $embyItem;
+	}
+	
+	public function getEmbyHomepageStreams()
+	{
+		if (!$this->config['homepageEmbyEnabled']) {
+			$this->setAPIResponse('error', 'Emby homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageEmbyStreams']) {
+			$this->setAPIResponse('error', 'Emby homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageEmbyAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageEmbyStreamsAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['embyURL'])) {
+			$this->setAPIResponse('error', 'Emby URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['embyToken'])) {
+			$this->setAPIResponse('error', 'Emby Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$url = $url . '/Sessions?api_key=' . $this->config['embyToken'] . '&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();
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveEmbyItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getEmbyHomepageRecent()
+	{
+		if (!$this->config['homepageEmbyEnabled']) {
+			$this->setAPIResponse('error', 'Emby homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageEmbyRecent']) {
+			$this->setAPIResponse('error', 'Emby homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageEmbyAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageEmbyRecentAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['embyURL'])) {
+			$this->setAPIResponse('error', 'Emby URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['embyToken'])) {
+			$this->setAPIResponse('error', 'Emby Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$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['embyToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$emby = json_decode($response->body, true);
+				foreach ($emby 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['embyToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveEmbyItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getEmbyHomepageMetadata($array)
+	{
+		if (!$this->config['homepageEmbyEnabled']) {
+			$this->setAPIResponse('error', 'Emby homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageEmbyAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['embyURL'])) {
+			$this->setAPIResponse('error', 'Emby URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['embyToken'])) {
+			$this->setAPIResponse('error', 'Emby Token is not defined', 422);
+			return false;
+		}
+		$key = $array['key'] ?? null;
+		if (!$key) {
+			$this->setAPIResponse('error', 'Plex Metadata key is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$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['embyToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$emby = json_decode($response->body, true);
+				foreach ($emby 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['embyToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$emby = json_decode($response->body, true);
+				if (isset($emby['NowPlayingItem']) || isset($emby['Name'])) {
+					$items[] = $this->resolveEmbyItem($emby);
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+}

+ 64 - 0
api/homepage/healthchecks.php

@@ -0,0 +1,64 @@
+<?php
+
+trait HealthChecksHomepageItem
+{
+	public function healthChecksTags($tags)
+	{
+		$return = '?tag=';
+		if (!$tags) {
+			return '';
+		} elseif ($tags == '*') {
+			return '';
+		} else {
+			if (strpos($tags, ',') !== false) {
+				$list = explode(',', $tags);
+				return $return . implode("&tag=", $list);
+			} else {
+				return $return . $tags;
+			}
+		}
+	}
+	
+	public function getHealthChecks($tags = null)
+	{
+		if (!$this->config['homepageHealthChecksEnabled']) {
+			$this->setAPIResponse('error', 'HealthChecks homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageHealthChecksAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['healthChecksURL'])) {
+			$this->setAPIResponse('error', 'HealthChecks URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['healthChecksToken'])) {
+			$this->setAPIResponse('error', 'HealthChecks Token is not defined', 422);
+			return false;
+		}
+		$api['content']['checks'] = array();
+		$tags = ($tags) ? $this->healthChecksTags($tags) : '';
+		$healthChecks = explode(',', $this->config['healthChecksToken']);
+		foreach ($healthChecks as $token) {
+			$url = $this->qualifyURL($this->config['healthChecksURL']) . '/' . $tags;
+			try {
+				$headers = array('X-Api-Key' => $token);
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				$response = Requests::get($url, $headers, $options);
+				if ($response->success) {
+					$healthResults = json_decode($response->body, true);
+					$api['content']['checks'] = array_merge($api['content']['checks'], $healthResults['checks']);
+				}
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'HealthChecks Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			};
+		}
+		usort($api['content']['checks'], function ($a, $b) {
+			return $a['status'] <=> $b['status'];
+		});
+		$api['content']['checks'] = isset($api['content']['checks']) ? $api['content']['checks'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 295 - 0
api/homepage/ical.php

@@ -0,0 +1,295 @@
+<?php
+
+trait ICalHomepageItem
+{
+	public function calendarDaysCheck($entryStart, $entryEnd)
+	{
+		$success = false;
+		$entryStart = intval($entryStart);
+		$entryEnd = intval($entryEnd);
+		if ($entryStart >= 0 && $entryEnd <= 0) {
+			$success = true;
+		}
+		return $success;
+	}
+	
+	public function calendarStandardizeTimezone($timezone)
+	{
+		switch ($timezone) {
+			case('CST'):
+			case('Central Time'):
+			case('Central Standard Time'):
+				$timezone = 'America/Chicago';
+				break;
+			case('CET'):
+			case('Central European Time'):
+				$timezone = 'Europe/Berlin';
+				break;
+			case('EST'):
+			case('Eastern Time'):
+			case('Eastern Standard Time'):
+				$timezone = 'America/New_York';
+				break;
+			case('PST'):
+			case('Pacific Time'):
+			case('Pacific Standard Time'):
+				$timezone = 'America/Los_Angeles';
+				break;
+			case('China Time'):
+			case('China Standard Time'):
+				$timezone = 'Asia/Beijing';
+				break;
+			case('IST'):
+			case('India Time'):
+			case('India Standard Time'):
+				$timezone = 'Asia/New_Delhi';
+				break;
+			case('JST');
+			case('Japan Time'):
+			case('Japan Standard Time'):
+				$timezone = 'Asia/Tokyo';
+				break;
+		}
+		return $timezone;
+	}
+	
+	public function getCalenderRepeat($value)
+	{
+		//FREQ=DAILY
+		//RRULE:FREQ=WEEKLY;BYDAY=TH
+		$first = explode('=', $value);
+		if (count($first) > 1) {
+			$second = explode(';', $first[1]);
+		} else {
+			return $value;
+		}
+		if ($second) {
+			return $second[0];
+		} else {
+			return $first[1];
+		}
+	}
+	
+	public function getCalenderRepeatUntil($value)
+	{
+		$first = explode('UNTIL=', $value);
+		if (count($first) > 1) {
+			if (strpos($first[1], ';') !== false) {
+				$check = explode(';', $first[1]);
+				return $check[0];
+			} else {
+				return $first[1];
+			}
+		} else {
+			return false;
+		}
+	}
+	
+	public function getCalenderRepeatCount($value)
+	{
+		$first = explode('COUNT=', $value);
+		if (count($first) > 1) {
+			return $first[1];
+		} else {
+			return false;
+		}
+	}
+	
+	public function file_get_contents_curl($url)
+	{
+		$ch = curl_init();
+		curl_setopt($ch, CURLOPT_AUTOREFERER, true);
+		curl_setopt($ch, CURLOPT_HEADER, 0);
+		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+		curl_setopt($ch, CURLOPT_URL, $url);
+		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+		$data = curl_exec($ch);
+		curl_close($ch);
+		return $data;
+	}
+	
+	public function getIcsEventsAsArray($file)
+	{
+		$icalString = $this->file_get_contents_curl($file);
+		$icsDates = array();
+		/* Explode the ICs Data to get datas as array according to string ‘BEGIN:’ */
+		$icsData = explode("BEGIN:", $icalString);
+		/* Iterating the icsData value to make all the start end dates as sub array */
+		foreach ($icsData as $key => $value) {
+			$icsDatesMeta [$key] = explode("\n", $value);
+		}
+		/* Itearting the Ics Meta Value */
+		foreach ($icsDatesMeta as $key => $value) {
+			foreach ($value as $subKey => $subValue) {
+				/* to get ics events in proper order */
+				$icsDates = $this->getICSDates($key, $subKey, $subValue, $icsDates);
+			}
+		}
+		return $icsDates;
+	}
+	
+	/* funcion is to avaid the elements wich is not having the proper start, end  and summary informations */
+	public function getICSDates($key, $subKey, $subValue, $icsDates)
+	{
+		if ($key != 0 && $subKey == 0) {
+			$icsDates [$key] ["BEGIN"] = $subValue;
+		} else {
+			$subValueArr = explode(":", $subValue, 2);
+			if (isset ($subValueArr [1])) {
+				$icsDates [$key] [$subValueArr [0]] = $subValueArr [1];
+			}
+		}
+		return $icsDates;
+	}
+	
+	public function getICalendar()
+	{
+		if (!$this->config['homepageCalendarEnabled']) {
+			$this->setAPIResponse('error', 'iCal homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageCalendarAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['calendariCal'])) {
+			$this->setAPIResponse('error', 'iCal URL is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$calendars = array();
+		$calendarURLList = explode(',', $this->config['calendariCal']);
+		$icalEvents = array();
+		foreach ($calendarURLList as $key => $value) {
+			$icsEvents = $this->getIcsEventsAsArray($value);
+			if (isset($icsEvents) && !empty($icsEvents)) {
+				$timeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? trim($icsEvents[1]['X-WR-TIMEZONE']) : date_default_timezone_get();
+				$originalTimeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? str_replace('"', '', trim($icsEvents[1]['X-WR-TIMEZONE'])) : false;
+				unset($icsEvents [1]);
+				foreach ($icsEvents as $icsEvent) {
+					$startKeys = $this->array_filter_key($icsEvent, function ($key) {
+						return strpos($key, 'DTSTART') === 0;
+					});
+					$endKeys = $this->array_filter_key($icsEvent, function ($key) {
+						return strpos($key, 'DTEND') === 0;
+					});
+					if (!empty($startKeys) && !empty($endKeys) && isset($icsEvent['SUMMARY'])) {
+						/* Getting start date and time */
+						$repeat = isset($icsEvent ['RRULE']) ? $icsEvent ['RRULE'] : false;
+						if (!$originalTimeZone) {
+							$tzKey = array_keys($startKeys);
+							if (strpos($tzKey[0], 'TZID=') !== false) {
+								$originalTimeZone = explode('TZID=', (string)$tzKey[0]);
+								$originalTimeZone = (count($originalTimeZone) >= 2) ? str_replace('"', '', $originalTimeZone[1]) : false;
+							}
+						}
+						$start = reset($startKeys);
+						$end = reset($endKeys);
+						$totalDays = $this->config['calendarStart'] + $this->config['calendarEnd'];
+						if ($repeat) {
+							$repeatOverride = $this->getCalenderRepeatCount(trim($icsEvent["RRULE"]));
+							switch (trim(strtolower($this->getCalenderRepeat($repeat)))) {
+								case 'daily':
+									$repeat = ($repeatOverride) ? $repeatOverride : $totalDays;
+									$term = 'days';
+									break;
+								case 'weekly':
+									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 7);
+									$term = 'weeks';
+									break;
+								case 'monthly':
+									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 30);
+									$term = 'months';
+									break;
+								case 'yearly':
+									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 365);
+									$term = 'years';
+									break;
+								default:
+									$repeat = ($repeatOverride) ? $repeatOverride : $totalDays;
+									$term = 'days';
+									break;
+							}
+						} else {
+							$repeat = 1;
+							$term = 'day';
+						}
+						$calendarTimes = 0;
+						while ($calendarTimes < $repeat) {
+							$currentDate = new DateTime ($this->currentTime);
+							$oldestDay = new DateTime ($this->currentTime);
+							$oldestDay->modify('-' . $this->config['calendarStart'] . ' days');
+							$newestDay = new DateTime ($this->currentTime);
+							$newestDay->modify('+' . $this->config['calendarEnd'] . ' days');
+							/* Converting to datetime and apply the timezone to get proper date time */
+							$startDt = new DateTime ($start);
+							/* Getting end date with time */
+							$endDt = new DateTime ($end);
+							if ($calendarTimes !== 0) {
+								$dateDiff = date_diff($startDt, $currentDate);
+								$startDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+								$startDt->modify('+' . $calendarTimes . ' ' . $term);
+								$endDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+								$endDt->modify('+' . $calendarTimes . ' ' . $term);
+							} elseif ($calendarTimes == 0 && $repeat !== 1) {
+								$dateDiff = date_diff($startDt, $currentDate);
+								$startDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+								$endDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+							}
+							$calendarStartDiff = date_diff($startDt, $newestDay);
+							$calendarEndDiff = date_diff($startDt, $oldestDay);
+							if ($originalTimeZone && $originalTimeZone !== 'UTC' && (strpos($start, 'Z') == false)) {
+								$originalTimeZone = $this->calendarStandardizeTimezone($originalTimeZone);
+								$dateTimeOriginalTZ = new DateTimeZone($originalTimeZone);
+								$dateTimeOriginal = new DateTime('now', $dateTimeOriginalTZ);
+								$dateTimeUTCTZ = new DateTimeZone(date_default_timezone_get());
+								$dateTimeUTC = new DateTime('now', $dateTimeUTCTZ);
+								$dateTimeOriginalOffset = $dateTimeOriginal->getOffset() / 3600;
+								$dateTimeUTCOffset = $dateTimeUTC->getOffset() / 3600;
+								$diff = $dateTimeUTCOffset - $dateTimeOriginalOffset;
+								$startDt->modify('+ ' . $diff . ' hour');
+								$endDt->modify('+ ' . $diff . ' hour');
+							}
+							$startDt->setTimeZone(new DateTimezone ($timeZone));
+							$endDt->setTimeZone(new DateTimezone ($timeZone));
+							$startDate = $startDt->format(DateTime::ATOM);
+							$endDate = $endDt->format(DateTime::ATOM);
+							if (new DateTime() < $endDt) {
+								$extraClass = 'text-info';
+							} else {
+								$extraClass = 'text-success';
+							}
+							/* Getting the name of event */
+							$eventName = $icsEvent['SUMMARY'];
+							if (!$this->calendarDaysCheck($calendarStartDiff->format('%R') . $calendarStartDiff->days, $calendarEndDiff->format('%R') . $calendarEndDiff->days)) {
+								break;
+							}
+							if (isset($icsEvent["RRULE"]) && $this->getCalenderRepeatUntil(trim($icsEvent["RRULE"]))) {
+								$untilDate = new DateTime ($this->getCalenderRepeatUntil(trim($icsEvent["RRULE"])));
+								$untilDiff = date_diff($currentDate, $untilDate);
+								if ($untilDiff->days > 0) {
+									break;
+								}
+							}
+							$icalEvents[] = array(
+								'title' => $eventName,
+								'imagetype' => 'calendar-o text-warning text-custom-calendar ' . $extraClass,
+								'imagetypeFilter' => 'ical',
+								'className' => 'bg-calendar calendar-item bg-custom-calendar',
+								'start' => $startDate,
+								'end' => $endDate,
+								'bgColor' => str_replace('text', 'bg', $extraClass),
+							);
+							$calendarTimes = $calendarTimes + 1;
+						}
+					}
+				}
+			}
+		}
+		$calendarSources = $icalEvents;
+		$this->setAPIResponse('success', null, 200, $calendarSources);
+		return $calendarSources;
+	}
+}

+ 81 - 0
api/homepage/jdownloader.php

@@ -0,0 +1,81 @@
+<?php
+
+trait JDownloaderHomepageItem
+{
+	public function testConnectionJDownloader()
+	{
+		if (empty($this->config['jdownloaderURL'])) {
+			$this->setAPIResponse('error', 'JDownloader URL is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jdownloaderURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('success', 'JDownloader Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function getJdownloaderHomepageQueue()
+	{
+		if (!$this->config['homepageJdownloaderEnabled']) {
+			$this->setAPIResponse('error', 'JDownloader homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageJdownloaderAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['jdownloaderURL'])) {
+			$this->setAPIResponse('error', 'JDownloader URL is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jdownloaderURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$temp = json_decode($response->body, true);
+				$packages = $temp['packages'];
+				if ($packages['downloader']) {
+					$api['content']['queueItems'] = $packages['downloader'];
+				} else {
+					$api['content']['queueItems'] = [];
+				}
+				if ($packages['linkgrabber_decrypted']) {
+					$api['content']['grabberItems'] = $packages['linkgrabber_decrypted'];
+				} else {
+					$api['content']['grabberItems'] = [];
+				}
+				if ($packages['linkgrabber_failed']) {
+					$api['content']['encryptedItems'] = $packages['linkgrabber_failed'];
+				} else {
+					$api['content']['encryptedItems'] = [];
+				}
+				if ($packages['linkgrabber_offline']) {
+					$api['content']['offlineItems'] = $packages['linkgrabber_offline'];
+				} else {
+					$api['content']['offlineItems'] = [];
+				}
+				$api['content']['$status'] = array($temp['downloader_state'], $temp['grabber_collecting'], $temp['update_ready']);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 224 - 0
api/homepage/lidarr.php

@@ -0,0 +1,224 @@
+<?php
+
+trait LidarrHomepageItem
+{
+	public function testConnectionLidarr()
+	{
+		if (empty($this->config['lidarrURL'])) {
+			$this->setAPIResponse('error', 'Lidarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['lidarrToken'])) {
+			$this->setAPIResponse('error', 'Lidarr Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['lidarrURL'], $this->config['lidarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+				$results = $downloader->getSystemStatus();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function getLidarrQueue()
+	{
+		if (!$this->config['homepageRadarrEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageRadarrQueueEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['radarrURL'])) {
+			$this->setAPIResponse('error', 'Radarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['radarrToken'])) {
+			$this->setAPIResponse('error', 'Radarr Token is not defined', 422);
+			return false;
+		}
+		$queueItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getQueue();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? '' : $downloadList;
+				} else {
+					$queue = '';
+				}
+				if (!empty($queue)) {
+					$queueItems = array_merge($queueItems, $queue);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$api['content']['queueItems'] = $queueItems;
+		$api['content']['historyItems'] = false;
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;;
+	}
+	
+	public function getLidarrCalendar($startDate = null, $endDate = null)
+	{
+		$startDate = ($startDate) ?? $_GET['start'];
+		$endDate = ($endDate) ?? $_GET['end'];
+		if (!$this->config['homepageLidarrEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageLidarrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['lidarrURL'])) {
+			$this->setAPIResponse('error', 'Lidarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['lidarrToken'])) {
+			$this->setAPIResponse('error', 'Lidarr Token is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['lidarrURL'], $this->config['lidarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+				$results = $downloader->getCalendar($startDate, $endDate, );
+				$result = json_decode($results, true);
+				if (is_array($result) || is_object($result)) {
+					$calendar = (array_key_exists('error', $result)) ? '' : $this->formatLidarrCalendar($results, $key);
+				} else {
+					$calendar = '';
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($calendar)) {
+				$calendarItems = array_merge($calendarItems, $calendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatLidarrCalendar($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array as $child) {
+			$i++;
+			$albumName = $child['title'];
+			$artistName = $child['artist']['artistName'];
+			$albumID = '';
+			$releaseDate = $child['releaseDate'];
+			$releaseDate = strtotime($releaseDate);
+			$releaseDate = date("Y-m-d H:i:s", $releaseDate);
+			if (new DateTime() < new DateTime($releaseDate)) {
+				$unaired = true;
+			}
+			if (isset($child['statistics']['percentOfTracks'])) {
+				if ($child['statistics']['percentOfTracks'] == '100.0') {
+					$downloaded = '1';
+				} else {
+					$downloaded = '0';
+				}
+			} else {
+				$downloaded = '0';
+			}
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$fanart = "/plugins/images/cache/no-np.png";
+			foreach ($child['artist']['images'] as $image) {
+				if ($image['coverType'] == "fanart") {
+					$fanart = str_replace('http://', 'https://', $image['url']);
+				}
+			}
+			$details = array(
+				"seasonCount" => '',
+				"status" => '',
+				"topTitle" => $albumName,
+				"bottomTitle" => $artistName,
+				"overview" => isset($child['artist']['overview']) ? $child['artist']['overview'] : '',
+				"runtime" => '',
+				"image" => $fanart,
+				"ratings" => $child['artist']['ratings']['value'],
+				"videoQuality" => "unknown",
+				"audioChannels" => "unknown",
+				"audioCodec" => "unknown",
+				"videoCodec" => "unknown",
+				"size" => "unknown",
+				"genres" => $child['genres'],
+			);
+			array_push($gotCalendar, array(
+				"id" => "Lidarr-" . $number . "-" . $i,
+				"title" => $artistName,
+				"start" => $child['releaseDate'],
+				"className" => "inline-popups bg-calendar calendar-item musicID--",
+				"imagetype" => "music " . $downloaded,
+				"imagetypeFilter" => "music",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+				"data" => $child
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+	
+}

+ 112 - 0
api/homepage/monitorr.php

@@ -0,0 +1,112 @@
+<?php
+
+trait MonitorrHomepageItem
+{
+	public function getMonitorrHomepageData()
+	{
+		if (!$this->config['homepageMonitorrEnabled']) {
+			$this->setAPIResponse('error', 'Monitorr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageMonitorrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['monitorrURL'])) {
+			$this->setAPIResponse('error', 'Monitorr URL is not defined', 422);
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['monitorrURL']);
+		$dataUrl = $url . '/assets/php/loop.php';
+		try {
+			$response = Requests::get($dataUrl, ['Token' => $this->config['organizrAPI']], []);
+			if ($response->success) {
+				$html = html_entity_decode($response->body);
+				// This section grabs the names of all services by regex
+				$services = [];
+				$servicesMatch = [];
+				$servicePattern = '/<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnonline">Online<\/div><\/a><\/div><\/div>|<div id="servicetitleoffline".*><div>(.*)<\/div><\/div><div class="btnoffline".*>Offline<\/div><\/div><\/div>|<div id="servicetitlenolink".*><div>(.*)<\/div><\/div><div class="btnonline".*>Online<\/div><\/div><\/div>|<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnunknown">/';
+				preg_match_all($servicePattern, $html, $servicesMatch);
+				$services = array_filter($servicesMatch[1]) + array_filter($servicesMatch[2]) + array_filter($servicesMatch[3]) + array_filter($servicesMatch[4]);
+				$statuses = [];
+				foreach ($services as $key => $service) {
+					$statusPattern = '/' . $service . '<\/div><\/div><div class="btnonline">(Online)<\/div>|' . $service . '<\/div><\/div><div class="btnoffline".*>(Offline)<\/div><\/div><\/div>|' . $service . '<\/div><\/div><div class="btnunknown">(.*)<\/div><\/a>/';
+					$status = [];
+					preg_match($statusPattern, $html, $status);
+					$statuses[$service] = $status;
+					foreach ($status as $match) {
+						if ($match == 'Online') {
+							$statuses[$service] = [
+								'status' => true
+							];
+						} else if ($match == 'Offline') {
+							$statuses[$service] = [
+								'status' => false
+							];
+						} else if ($match == 'Unresponsive') {
+							$statuses[$service] = [
+								'status' => 'unresponsive'
+							];
+						}
+					}
+					$statuses[$service]['sort'] = $key;
+					$imageMatch = [];
+					$imgPattern = '/assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitle"><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg imgoffline" alt=.*><\/div><\/div><div id="servicetitleoffline".*><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitlenolink".*><div>' . $service . '/';
+					preg_match($imgPattern, $html, $imageMatch);
+					unset($imageMatch[0]);
+					$imageMatch = array_values($imageMatch);
+					// array_push($api['imagematches'][$service], $imageMatch);
+					foreach ($imageMatch as $match) {
+						if ($match !== '') {
+							$image = $match;
+						}
+					}
+					$ext = explode('.', $image);
+					$ext = $ext[key(array_slice($ext, -1, 1, true))];
+					$imageUrl = $url . '/assets' . $image;
+					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+					$img = Requests::get($imageUrl, ['Token' => $this->config['organizrAPI']], []);
+					if ($img->success) {
+						$base64 = 'data:image/' . $ext . ';base64,' . base64_encode($img->body);
+						$statuses[$service]['image'] = $base64;
+					} else {
+						$statuses[$service]['image'] = $cacheDirectory . 'no-list.png';
+					}
+					$linkMatch = [];
+					$linkPattern = '/<a class="servicetile" href="(.*)" target="_blank" style="display: block"><div id="serviceimg"><div><img id="' . strtolower($service) . '-service-img/';
+					preg_match($linkPattern, $html, $linkMatch);
+					$linkMatch = array_values($linkMatch);
+					unset($linkMatch[0]);
+					foreach ($linkMatch as $link) {
+						if ($link !== '') {
+							$statuses[$service]['link'] = $link;
+						}
+					}
+				}
+				foreach ($statuses as $status) {
+					foreach ($status as $key => $value) {
+						if (!isset($sortArray[$key])) {
+							$sortArray[$key] = array();
+						}
+						$sortArray[$key][] = $value;
+					}
+				}
+				array_multisort($sortArray['status'], SORT_ASC, $sortArray['sort'], SORT_ASC, $statuses);
+				$api['services'] = $statuses;
+				$api['options'] = [
+					'title' => $this->config['monitorrHeader'],
+					'titleToggle' => $this->config['monitorrHeaderToggle'],
+					'compact' => $this->config['monitorrCompact'],
+				];
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Monitorr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 401);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 516 - 0
api/homepage/netdata.php

@@ -0,0 +1,516 @@
+<?php
+
+trait NetDataHomepageItem
+{
+	public function getNetdataHomepageData()
+	{
+		if (!$this->config['homepageNetdataEnabled']) {
+			$this->setAPIResponse('error', 'NetData homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageNetdataAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['netdataURL'])) {
+			$this->setAPIResponse('error', 'NetData URL is not defined', 422);
+			return false;
+		}
+		$api = [];
+		$api['data'] = [];
+		$api['url'] = $this->config['netdataURL'];
+		$url = $this->qualifyURL($this->config['netdataURL']);
+		for ($i = 1; $i < 8; $i++) {
+			if ($this->config['netdata' . ($i) . 'Enabled']) {
+				switch ($this->config['netdata' . $i . 'Data']) {
+					case 'disk-read':
+						$data = $this->disk('in', $url);
+						break;
+					case 'disk-write':
+						$data = $this->disk('out', $url);
+						$data['value'] = abs($data['value']);
+						$data['percent'] = abs($data['percent']);
+						break;
+					case 'cpu':
+						$data = $this->cpu($url);
+						break;
+					case 'net-in':
+						$data = $this->net('received', $url);
+						break;
+					case 'net-out':
+						$data = $this->net('sent', $url);
+						$data['value'] = abs($data['value']);
+						$data['percent'] = abs($data['percent']);
+						break;
+					case 'ram-used':
+						$data = $this->ram($url);
+						break;
+					case 'swap-used':
+						$data = $this->swap($url);
+						break;
+					case 'disk-avail':
+						$data = $this->diskSpace('avail', $url);
+						break;
+					case 'disk-used':
+						$data = $this->diskSpace('used', $url);
+						break;
+					case 'ipmi-temp-c':
+						$data = $this->ipmiTemp($url, 'c');
+						break;
+					case 'ipmi-temp-f':
+						$data = $this->ipmiTemp($url, 'f');
+						break;
+					case 'cpu-temp-c':
+						$data = $this->cpuTemp($url, 'c');
+						break;
+					case 'cpu-temp-f':
+						$data = $this->cpuTemp($url, 'f');
+						break;
+					case 'custom':
+						$data = $this->customNetdata($url, $i);
+						break;
+					default:
+						$data = [
+							'title' => 'DNC',
+							'value' => 0,
+							'units' => 'N/A',
+							'max' => 100,
+						];
+						break;
+				}
+				$data['title'] = $this->config['netdata' . $i . 'Title'];
+				$data['colour'] = $this->config['netdata' . $i . 'Colour'];
+				$data['chart'] = $this->config['netdata' . $i . 'Chart'];
+				$data['size'] = $this->config['netdata' . $i . 'Size'];
+				$data['lg'] = $this->config['netdata' . ($i) . 'lg'];
+				$data['md'] = $this->config['netdata' . ($i) . 'md'];
+				$data['sm'] = $this->config['netdata' . ($i) . 'sm'];
+				array_push($api['data'], $data);
+			}
+		}
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+		
+	}
+	
+	public function netdataSettingsArray()
+	{
+		$array = array(
+			'name' => 'Netdata',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/netdata.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageNetdataEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageNetdataEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageNetdataAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageNetdataAuth'],
+						'options' => $this->groupSelect()
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'netdataURL',
+						'label' => 'URL',
+						'value' => $this->config['netdataURL'],
+						'help' => 'Please enter the local IP:PORT of your netdata instance'
+					),
+					array(
+						'type' => 'blank',
+						'label' => ''
+					),
+				),
+			)
+		);
+		for ($i = 1; $i <= 7; $i++) {
+			$array['settings']['Chart ' . $i] = array(
+				array(
+					'type' => 'switch',
+					'name' => 'netdata' . $i . 'Enabled',
+					'label' => 'Enable',
+					'value' => $this->config['netdata' . $i . 'Enabled']
+				),
+				array(
+					'type' => 'blank',
+					'label' => ''
+				),
+				array(
+					'type' => 'input',
+					'name' => 'netdata' . $i . 'Title',
+					'label' => 'Title',
+					'value' => $this->config['netdata' . $i . 'Title'],
+					'help' => 'Title for the netdata graph'
+				),
+				array(
+					'type' => 'select',
+					'name' => 'netdata' . $i . 'Data',
+					'label' => 'Data',
+					'value' => $this->config['netdata' . $i . 'Data'],
+					'options' => $this->netdataOptions(),
+				),
+				array(
+					'type' => 'select',
+					'name' => 'netdata' . $i . 'Chart',
+					'label' => 'Chart',
+					'value' => $this->config['netdata' . $i . 'Chart'],
+					'options' => $this->netdataChartOptions(),
+				),
+				array(
+					'type' => 'select',
+					'name' => 'netdata' . $i . 'Colour',
+					'label' => 'Colour',
+					'value' => $this->config['netdata' . $i . 'Colour'],
+					'options' => $this->netdataColourOptions(),
+				),
+				array(
+					'type' => 'select',
+					'name' => 'netdata' . $i . 'Size',
+					'label' => 'Size',
+					'value' => $this->config['netdata' . $i . 'Size'],
+					'options' => $this->netdataSizeOptions(),
+				),
+				array(
+					'type' => 'blank',
+					'label' => ''
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'netdata' . $i . 'lg',
+					'label' => 'Show on large screens',
+					'value' => $this->config['netdata' . $i . 'lg']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'netdata' . $i . 'md',
+					'label' => 'Show on medium screens',
+					'value' => $this->config['netdata' . $i . 'md']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'netdata' . $i . 'sm',
+					'label' => 'Show on small screens',
+					'value' => $this->config['netdata' . $i . 'sm']
+				),
+			);
+		}
+		$array['settings']['Custom data'] = array(
+			array(
+				'type' => 'html',
+				'label' => '',
+				'override' => 12,
+				'html' => '
+			<div>
+			    <p>This is where you can define custom data sources for your netdata charts. To use a custom source, you need to select "Custom" in the data field for the chart.</p>
+			    <p>To define a custom data source, you need to add an entry to the JSON below, where the key is the chart number you want the custom data to be used for. Here is an example to set chart 1 custom data source to RAM percentage:</p>
+			    <pre>{
+			    "1": {
+			        "url": "/api/v1/data?chart=system.ram&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used|buffers|active|wired",
+			        "value": "result,0",
+			        "units": "%",
+			        "max": 100
+			    }
+			}</pre>
+			    <p>The URL is appended to your netdata URL and returns JSON formatted data. The value field tells Organizr how to return the value you want from the netdata API. This should be formatted as comma-separated keys to access the desired value.</p>
+			    <table class="table table-striped">
+			        <thead>
+			            <tr>
+			                <th>Parameter</th>
+			                <th>Description</th>
+			                <th>Required</th>
+			            </tr>
+			        </thead>
+			        <tbody>
+			            <tr>
+			                <td>url</td>
+			                <td>Specifies the netdata API endpoint</td>
+			                <td><i class="fa fa-check text-success" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>value</td>
+			                <td>Specifies the selector used to get the data form the netdata response</td>
+			                <td><i class="fa fa-check text-success" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>units</td>
+			                <td>Specifies the units shown in the graph/chart. Defaults to %</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>max</td>
+			                <td>Specifies the maximum possible value for the data. Defaults to 100</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>mutator</td>
+			                <td>Used to perform simple mathematical operations on the result (+, -, /, *). For example: dividing the result by 1000 would be "/1000". These operations can be chained together by putting them in a comma-seprated format.</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>netdata</td>
+			                <td>Can be used to override the netdata instance data is retrieved from (in the format: http://IP:PORT)</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			        </tbody>
+			    </table>
+			</div>'
+			),
+			array(
+				'type' => 'html',
+				'name' => 'netdataCustomTextAce',
+				'class' => 'jsonTextarea hidden',
+				'label' => 'Custom definitions',
+				'override' => 12,
+				'html' => '<div id="netdataCustomTextAce" style="height: 300px;">' . htmlentities($this->config['netdataCustom']) . '</div>',
+			),
+			array(
+				'type' => 'textbox',
+				'name' => 'netdataCustom',
+				'class' => 'jsonTextarea hidden',
+				'id' => 'netdataCustomText',
+				'label' => '',
+				'value' => $this->config['netdataCustom'],
+			)
+		);
+		$array['settings']['Options'] = array(
+			array(
+				'type' => 'select',
+				'name' => 'homepageNetdataRefresh',
+				'label' => 'Refresh Seconds',
+				'value' => $this->config['homepageNetdataRefresh'],
+				'options' => $this->optionTime()
+			),
+		);
+		return $array;
+	}
+	
+	public function disk($dimension, $url)
+	{
+		$data = [];
+		// Get Data
+		$dataUrl = $url . '/api/v1/data?chart=system.io&dimensions=' . $dimension . '&format=array&points=540&group=average&gtime=0&options=absolute|jsonwrap|nonzero&after=-540';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$data['value'] = $json['latest_values'][0] / 1000;
+				$data['percent'] = $this->getPercent($json['latest_values'][0], $json['max']);
+				$data['units'] = 'MiB/s';
+				$data['max'] = $json['max'];
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		return $data;
+	}
+	
+	public function diskSpace($dimension, $url)
+	{
+		$data = [];
+		// Get Data
+		$dataUrl = $url . '/api/v1/data?chart=disk_space._&format=json&points=509&group=average&gtime=0&options=ms|jsonwrap|nonzero&after=-540&dimension=' . $dimension;
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$data['value'] = $json['result']['data'][0][1];
+				$data['percent'] = $data['value'];
+				$data['units'] = '%';
+				$data['max'] = 100;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		return $data;
+	}
+	
+	public function net($dimension, $url)
+	{
+		$data = [];
+		// Get Data
+		$dataUrl = $url . '/api/v1/data?chart=system.net&dimensions=' . $dimension . '&format=array&points=540&group=average&gtime=0&options=absolute|jsonwrap|nonzero&after=-540';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$data['value'] = $json['latest_values'][0] / 1000;
+				$data['percent'] = $this->getPercent($json['latest_values'][0], $json['max']);
+				$data['units'] = 'Mbit/s';
+				$data['max'] = $json['max'];
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		return $data;
+	}
+	
+	public function cpu($url)
+	{
+		$data = [];
+		$dataUrl = $url . '/api/v1/data?chart=system.cpu&format=array';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$data['value'] = $json[0];
+				$data['percent'] = $data['value'];
+				$data['max'] = 100;
+				$data['units'] = '%';
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		return $data;
+	}
+	
+	public function ram($url)
+	{
+		$data = [];
+		$dataUrl = $url . '/api/v1/data?chart=system.ram&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used|buffers|active|wired';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$data['value'] = $json['result'][0];
+				$data['percent'] = $data['value'];
+				$data['max'] = 100;
+				$data['units'] = '%';
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		return $data;
+	}
+	
+	public function swap($url)
+	{
+		$data = [];
+		$dataUrl = $url . '/api/v1/data?chart=system.swap&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$data['value'] = $json['result'][0];
+				$data['percent'] = $data['value'];
+				$data['max'] = 100;
+				$data['units'] = '%';
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		return $data;
+	}
+	
+	public function getPercent($val, $max)
+	{
+		if ($max == 0) {
+			return 0;
+		} else {
+			return ($val / $max) * 100;
+		}
+	}
+	
+	public function customNetdata($url, $id)
+	{
+		try {
+			$customs = json_decode($this->config['netdataCustom'], true, 512, JSON_THROW_ON_ERROR);
+		} catch (Exception $e) {
+			$customs = false;
+		}
+		if ($customs == false) {
+			return [
+				'error' => 'unable to parse custom JSON'
+			];
+		} else if (!isset($customs[$id])) {
+			return [
+				'error' => 'custom definition not found'
+			];
+		} else {
+			$data = [];
+			$custom = $customs[$id];
+			if (isset($custom['url']) && isset($custom['value'])) {
+				if (isset($custom['netdata']) && $custom['netdata'] != '') {
+					$url = $this->qualifyURL($custom['netdata']);
+				}
+				$dataUrl = $url . '/' . $custom['url'];
+				try {
+					$response = Requests::get($dataUrl);
+					if ($response->success) {
+						$json = json_decode($response->body, true);
+						if (!isset($custom['max']) || $custom['max'] == '') {
+							$custom['max'] = 100;
+						}
+						$data['max'] = $custom['max'];
+						if (!isset($custom['units']) || $custom['units'] == '') {
+							$custom['units'] = '%';
+						}
+						$data['units'] = $custom['units'];
+						$selectors = explode(',', $custom['value']);
+						foreach ($selectors as $selector) {
+							if (is_numeric($selector)) {
+								$selector = (int)$selector;
+							}
+							if (!isset($data['value'])) {
+								$data['value'] = $json[$selector];
+							} else {
+								$data['value'] = $data['value'][$selector];
+							}
+						}
+						if (isset($custom['mutator'])) {
+							$data['value'] = $this->parseMutators($data['value'], $custom['mutator']);
+						}
+						if ($data['max'] == 0) {
+							$data['percent'] = 0;
+						} else {
+							$data['percent'] = ($data['value'] / $data['max']) * 100;
+						}
+					}
+				} catch (Requests_Exception $e) {
+					$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				};
+			} else {
+				$data['error'] = 'custom definition incomplete';
+			}
+			return $data;
+		}
+	}
+	
+	public function parseMutators($val, $mutators)
+	{
+		$mutators = explode(',', $mutators);
+		foreach ($mutators as $m) {
+			$op = $m[0];
+			try {
+				$m = (float)substr($m, 1);
+				switch ($op) {
+					case '+':
+						$val = $val + $m;
+						break;
+					case '-':
+						$val = $val - $m;
+						break;
+					case '/':
+						$val = $val / $m;
+						break;
+					case '*':
+						$val = $val * $m;
+						break;
+					default:
+						break;
+				}
+			} catch (Exception $e) {
+				//
+			}
+		}
+		return $val;
+	}
+}

+ 74 - 0
api/homepage/nzbget.php

@@ -0,0 +1,74 @@
+<?php
+
+trait NZBGetHomepageItem
+{
+	public function testConnectionNZBGet()
+	{
+		if (empty($this->config['nzbgetURL'])) {
+			$this->setAPIResponse('error', 'NZBGet URL is not defined', 422);
+			return false;
+		}
+		try {
+			$url = $this->qualifyURL($this->config['nzbgetURL']);
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$urlGroups = $url . '/jsonrpc/listgroups';
+			if ($this->config['nzbgetUsername'] !== '' && $this->decrypt($this->config['nzbgetPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Basic(array($this->config['nzbgetUsername'], $this->decrypt($this->config['nzbgetPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$response = Requests::get($urlGroups, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('success', 'NZBGet: An Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getNzbgetHomepageQueue()
+	{
+		if (!$this->config['homepageNzbgetEnabled']) {
+			$this->setAPIResponse('error', 'NZBGet homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageNzbgetAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['nzbgetURL'])) {
+			$this->setAPIResponse('error', 'NZBGet URL is not defined', 422);
+			return false;
+		}
+		try {
+			$url = $this->qualifyURL($this->config['nzbgetURL']);
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$urlGroups = $url . '/jsonrpc/listgroups';
+			$urlHistory = $url . '/jsonrpc/history';
+			if ($this->config['nzbgetUsername'] !== '' && $this->decrypt($this->config['nzbgetPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Basic(array($this->config['nzbgetUsername'], $this->decrypt($this->config['nzbgetPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$response = Requests::get($urlGroups, array(), $options);
+			if ($response->success) {
+				$api['content']['queueItems'] = json_decode($response->body, true);
+			}
+			$response = Requests::get($urlHistory, array(), $options);
+			if ($response->success) {
+				$api['content']['historyItems'] = json_decode($response->body, true);
+			}
+			$api['content'] = isset($api['content']) ? $api['content'] : false;
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+}

+ 53 - 0
api/homepage/octoprint.php

@@ -0,0 +1,53 @@
+<?php
+
+trait OctoPrintHomepageItem
+{
+	public function getOctoprintHomepageData()
+	{
+		if (!$this->config['homepageOctoprintEnabled']) {
+			$this->setAPIResponse('error', 'OctoPrint homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageOctoprintAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['octoprintURL'])) {
+			$this->setAPIResponse('error', 'OctoPrint URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['octoprintToken'])) {
+			$this->setAPIResponse('error', 'OctoPrint Token is not defined', 422);
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['octoprintURL']);
+		$endpoints = ['job', 'settings'];
+		$api['data']['url'] = $this->config['octoprintURL'];
+		foreach ($endpoints as $endpoint) {
+			$dataUrl = $url . '/api/' . $endpoint;
+			try {
+				$headers = array('X-API-KEY' => $this->config['octoprintToken']);
+				$response = Requests::get($dataUrl, $headers);
+				if ($response->success) {
+					$json = json_decode($response->body, true);
+					$api['data'][$endpoint] = $json;
+					$api['options'] = [
+						'title' => $this->config['octoprintHeader'],
+						'titleToggle' => $this->config['octoprintHeaderToggle'],
+					];
+				} else {
+					$this->setAPIResponse('error', 'OctoPrint connection error', 409);
+					return false;
+				}
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'Octoprint Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			};
+		}
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 391 - 0
api/homepage/ombi.php

@@ -0,0 +1,391 @@
+<?php
+
+trait OmbiHomepageItem
+{
+	public function testConnectionOmbi()
+	{
+		if (empty($this->config['ombiURL'])) {
+			$this->setAPIResponse('error', 'Ombi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['ombiToken'])) {
+			$this->setAPIResponse('error', 'Ombi Token is not defined', 422);
+			return false;
+		}
+		$headers = array(
+			"Accept" => "application/json",
+			"Apikey" => $this->config['ombiToken'],
+		);
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$test = Requests::get($url . "/api/v1/Settings/about", $headers, $options);
+			if ($test->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function ombiTVDefault($type)
+	{
+		return $type == $this->config['ombiTvDefault'];
+	}
+	
+	public function getOmbiRequests($type = "both", $limit = 50)
+	{
+		if (!$this->config['homepageOmbiEnabled']) {
+			$this->setAPIResponse('error', 'Ombi homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageOmbiAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['ombiURL'])) {
+			$this->setAPIResponse('error', 'Ombi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['ombiToken'])) {
+			$this->setAPIResponse('error', 'Ombi Token is not defined', 422);
+			return false;
+		}
+		$api['count'] = array(
+			'movie' => 0,
+			'tv' => 0,
+			'limit' => (integer)$limit
+		);
+		$headers = array(
+			"Accept" => "application/json",
+			"Apikey" => $this->config['ombiToken'],
+		);
+		$requests = array();
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			switch ($type) {
+				case 'movie':
+					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
+					break;
+				case 'tv':
+					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
+					break;
+				default:
+					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
+					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
+					break;
+			}
+			if ($movie->success || $tv->success) {
+				if (isset($movie)) {
+					$movie = json_decode($movie->body, true);
+					//$movie = array_reverse($movie);
+					foreach ($movie as $key => $value) {
+						$proceed = (($this->config['ombiLimitUser']) && strtolower($this->user['username']) == strtolower($value['requestedUser']['userName'])) || (!$this->config['ombiLimitUser']) || $this->qualifyRequest(1);
+						if ($proceed) {
+							$api['count']['movie']++;
+							$requests[] = array(
+								'id' => $value['theMovieDbId'],
+								'title' => $value['title'],
+								'overview' => $value['overview'],
+								'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? 'https://image.tmdb.org/t/p/w300/' . $value['posterPath'] : 'plugins/images/cache/no-list.png',
+								'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
+								'approved' => $value['approved'],
+								'available' => $value['available'],
+								'denied' => $value['denied'],
+								'deniedReason' => $value['deniedReason'],
+								'user' => $value['requestedUser']['userName'],
+								'userAlias' => $value['requestedUser']['userAlias'],
+								'request_id' => $value['id'],
+								'request_date' => $value['requestedDate'],
+								'release_date' => $value['releaseDate'],
+								'type' => 'movie',
+								'icon' => 'mdi mdi-filmstrip',
+								'color' => 'palette-Deep-Purple-900 bg white',
+							);
+						}
+					}
+				}
+				if (isset($tv) && (is_array($tv) || is_object($tv))) {
+					$tv = json_decode($tv->body, true);
+					foreach ($tv as $key => $value) {
+						if (count($value['childRequests']) > 0) {
+							$proceed = (($this->config['ombiLimitUser']) && strtolower($this->user['username']) == strtolower($value['childRequests'][0]['requestedUser']['userName'])) || (!$this->config['ombiLimitUser']) || $this->qualifyRequest(1);
+							if ($proceed) {
+								$api['count']['tv']++;
+								$requests[] = array(
+									'id' => $value['tvDbId'],
+									'title' => $value['title'],
+									'overview' => $value['overview'],
+									'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? $value['posterPath'] : 'plugins/images/cache/no-list.png',
+									'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
+									'approved' => $value['childRequests'][0]['approved'],
+									'available' => $value['childRequests'][0]['available'],
+									'denied' => $value['childRequests'][0]['denied'],
+									'deniedReason' => $value['childRequests'][0]['deniedReason'],
+									'user' => $value['childRequests'][0]['requestedUser']['userName'],
+									'userAlias' => $value['childRequests'][0]['requestedUser']['userAlias'],
+									'request_id' => $value['id'],
+									'request_date' => $value['childRequests'][0]['requestedDate'],
+									'release_date' => $value['releaseDate'],
+									'type' => 'tv',
+									'icon' => 'mdi mdi-television',
+									'color' => 'grayish-blue-bg',
+								);
+							}
+						}
+					}
+				}
+				//sort here
+				usort($requests, function ($item1, $item2) {
+					if ($item1['request_date'] == $item2['request_date']) {
+						return 0;
+					}
+					return $item1['request_date'] > $item2['request_date'] ? -1 : 1;
+				});
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($requests) ? array_slice($requests, 0, $limit) : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function addOmbiRequest($id, $type)
+	{
+		$id = ($id) ?? null;
+		$type = ($type) ?? null;
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		if (!$type) {
+			$this->setAPIResponse('error', 'Type was not supplied', 422);
+			return false;
+		}
+		if (!$this->config['homepageOmbiEnabled']) {
+			$this->setAPIResponse('error', 'Ombi homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageOmbiAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['ombiURL'])) {
+			$this->setAPIResponse('error', 'Ombi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['ombiToken'])) {
+			$this->setAPIResponse('error', 'Ombi Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		switch ($type) {
+			case 'season':
+			case 'tv':
+				$type = 'tv';
+				$add = array(
+					'tvDbId' => $id,
+					'requestAll' => $this->ombiTVDefault('all'),
+					'latestSeason' => $this->ombiTVDefault('last'),
+					'firstSeason' => $this->ombiTVDefault('first')
+				);
+				break;
+			default:
+				$type = 'movie';
+				$add = array("theMovieDbId" => (int)$id);
+				break;
+		}
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			if (isset($_COOKIE['Auth'])) {
+				$headers = array(
+					"Accept" => "application/json",
+					"Content-Type" => "application/json",
+					"Authorization" => "Bearer " . $_COOKIE['Auth']
+				);
+			} else {
+				$this->setAPIResponse('error', 'User does not have Auth Cookie', 500);
+				return false;
+			}
+			//https://api.themoviedb.org/3/movie/157336?api_key=83cf4ee97bb728eeaf9d4a54e64356a1
+			// Lets check if it exists inside Ombi first... but since I can't search with ID - i have to query title from id
+			$tmdbResponse = Requests::get('https://api.themoviedb.org/3/' . $type . '/' . $id . '?api_key=83cf4ee97bb728eeaf9d4a54e64356a1', [], $options);
+			if ($tmdbResponse->success) {
+				$details = json_decode($tmdbResponse->body, true);
+				if (count($details) > 0) {
+					switch ($type) {
+						case 'tv':
+							$title = $details['name'];
+							$idType = 'theTvDbId';
+							$tmdbResponseID = Requests::get('https://api.themoviedb.org/3/tv/' . $id . '/external_ids?api_key=83cf4ee97bb728eeaf9d4a54e64356a1', [], $options);
+							if ($tmdbResponseID->success) {
+								$detailsID = json_decode($tmdbResponseID->body, true);
+								if (count($detailsID) > 0) {
+									if (isset($detailsID['tvdb_id'])) {
+										$id = $detailsID['tvdb_id'];
+										$add['tvDbId'] = $id;
+									} else {
+										$this->setAPIResponse('error', 'Could not get TVDB Id', 422);
+										return false;
+									}
+								} else {
+									$this->setAPIResponse('error', 'Could not get TVDB Id', 422);
+									return false;
+								}
+							}
+							break;
+						case 'movie':
+							$title = $details['title'];
+							$idType = 'theMovieDbId';
+							break;
+						default:
+							$this->setAPIResponse('error', 'Ombi Type was not found', 422);
+							return false;
+					}
+				} else {
+					$this->setAPIResponse('error', 'No data returned from TMDB', 422);
+					return false;
+				}
+			} else {
+				$this->setAPIResponse('error', 'Could not contact TMDB', 422);
+				return false;
+			}
+			$searchResponse = Requests::get($url . '/api/v1/Search/' . $type . '/' . urlencode($title), $headers, $options);
+			if ($searchResponse->success) {
+				$details = json_decode($searchResponse->body, true);
+				if (count($details) > 0) {
+					foreach ($details as $k => $v) {
+						if ($v[$idType] == $id) {
+							if ($v['available']) {
+								$this->setAPIResponse('error', 'Request is already available', 409);
+								return false;
+							} elseif ($v['requested']) {
+								$this->setAPIResponse('error', 'Request is already requested', 409);
+								return false;
+							}
+						}
+					}
+				}
+			} else {
+				$this->setAPIResponse('error', 'Ombi Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::post($url . "/api/v1/Request/" . $type, $headers, json_encode($add), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'Ombi Request submitted', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Ombi Error Occurred', 500);
+				return false;
+			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function actionOmbiRequest($id, $type, $action)
+	{
+		$id = ($id) ?? null;
+		$type = ($type) ?? null;
+		$action = ($action) ?? null;
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		if (!$type) {
+			$this->setAPIResponse('error', 'Type was not supplied', 422);
+			return false;
+		}
+		if (!$action) {
+			$this->setAPIResponse('error', 'Action was not supplied', 422);
+			return false;
+		}
+		if (!$this->config['homepageOmbiEnabled']) {
+			$this->setAPIResponse('error', 'Ombi homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageOmbiAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest(1)) {
+			$this->setAPIResponse('error', 'User must be an admin', 401);
+			return false;
+		}
+		if (empty($this->config['ombiURL'])) {
+			$this->setAPIResponse('error', 'Ombi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['ombiToken'])) {
+			$this->setAPIResponse('error', 'Ombi Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		$headers = array(
+			"Accept" => "application/json",
+			"Content-Type" => "application/json",
+			"Apikey" => $this->config['ombiToken']
+		);
+		$data = array(
+			'id' => $id,
+		);
+		switch ($type) {
+			case 'season':
+			case 'tv':
+				$type = 'tv';
+				break;
+			default:
+				$type = 'movie';
+				break;
+		}
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			switch ($action) {
+				case 'approve':
+					$response = Requests::post($url . "/api/v1/Request/" . $type . "/approve", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been approved';
+					break;
+				case 'available':
+					$response = Requests::post($url . "/api/v1/Request/" . $type . "/available", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been marked available';
+					break;
+				case 'unavailable':
+					$response = Requests::post($url . "/api/v1/Request/" . $type . "/unavailable", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been marked unavailable';
+					break;
+				case 'deny':
+					$response = Requests::put($url . "/api/v1/Request/" . $type . "/deny", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been denied';
+					break;
+				case 'delete':
+					$response = Requests::delete($url . "/api/v1/Request/" . $type . "/" . $id, $headers, $options);
+					$message = 'Ombi Request has been deleted';
+					break;
+				default:
+					return false;
+			}
+			if ($response->success) {
+				$this->setAPIResponse('success', $message, 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Ombi Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+}

+ 87 - 0
api/homepage/pihole.php

@@ -0,0 +1,87 @@
+<?php
+
+trait PiHoleHomepageItem
+{
+	public function testConnectionPihole()
+	{
+		if (empty($this->config['piholeURL'])) {
+			$this->setAPIResponse('error', 'Pihole URL is not defined', 422);
+			return false;
+		}
+		$api = array();
+		$failed = false;
+		$errors = '';
+		$urls = explode(',', $this->config['piholeURL']);
+		foreach ($urls as $url) {
+			$url = $url . '/api.php?';
+			try {
+				$response = Requests::get($url, [], []);
+				if ($response->success) {
+					@$test = json_decode($response->body, true);
+					if (!is_array($test)) {
+						$ip = $this->qualifyURL($url, true)['host'];
+						$errors .= $ip . ': Response was not JSON';
+						$failed = true;
+					}
+				}
+				if (!$response->success) {
+					$ip = $this->qualifyURL($url, true)['host'];
+					$errors .= $ip . ': Unknown Failure';
+					$failed = true;
+				}
+			} catch (Requests_Exception $e) {
+				$failed = true;
+				$ip = $this->qualifyURL($url, true)['host'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			};
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function getPiholeHomepageStats()
+	{
+		if (!$this->config['homepagePiholeEnabled']) {
+			$this->setAPIResponse('error', 'Pihole homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePiholeAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['piholeURL'])) {
+			$this->setAPIResponse('error', 'Pihole URL is not defined', 422);
+			return false;
+		}
+		$api = array();
+		$urls = explode(',', $this->config['piholeURL']);
+		foreach ($urls as $url) {
+			$url = $url . '/api.php?';
+			try {
+				$response = Requests::get($url, [], []);
+				if ($response->success) {
+					@$piholeResults = json_decode($response->body, true);
+					if (is_array($piholeResults)) {
+						$ip = $this->qualifyURL($url, true)['host'];
+						$api['data'][$ip] = $piholeResults;
+					}
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				$this->writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				return false;
+			};
+		}
+		$api['options']['combine'] = $this->config['homepagePiholeCombine'];
+		$api['options']['title'] = $this->config['piholeHeaderToggle'];
+		$api = isset($api) ? $api : null;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 517 - 0
api/homepage/plex.php

@@ -0,0 +1,517 @@
+<?php
+
+trait PlexHomepageItem
+{
+	public function testConnectionPlex()
+	{
+		if (!empty($this->config['plexURL']) && !empty($this->config['plexToken'])) {
+			$url = $this->qualifyURL($this->config['plexURL']) . "/?X-Plex-Token=" . $this->config['plexToken'];
+			try {
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				$response = Requests::get($url, array(), $options);
+				libxml_use_internal_errors(true);
+				if ($response->success) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			};
+		} else {
+			$this->setAPIResponse('error', 'URL and/or Token not setup', 422);
+			return 'URL and/or Token not setup';
+		}
+	}
+	
+	public function resolvePlexItem($item)
+	{
+		// Static Height & Width
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		// 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 'show':
+				$plexItem['type'] = 'tv';
+				$plexItem['title'] = (string)$item['title'];
+				$plexItem['secondaryTitle'] = (string)$item['year'];
+				$plexItem['summary'] = (string)$item['summary'];
+				$plexItem['ratingKey'] = (string)$item['ratingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = (string)$item['year'];
+				$plexItem['metadataKey'] = (string)$item['ratingKey'];
+				break;
+			case 'season':
+				$plexItem['type'] = 'tv';
+				$plexItem['title'] = (string)$item['parentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['title'];
+				$plexItem['summary'] = (string)$item['parentSummary'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+				$plexItem['metadataKey'] = (string)$item['parentRatingKey'];
+				break;
+			case 'episode':
+				$plexItem['type'] = 'tv';
+				$plexItem['title'] = (string)$item['grandparentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['parentTitle'];
+				$plexItem['summary'] = (string)$item['title'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = ($item['parentThumb'] ? (string)$item['parentThumb'] : (string)$item['grandparentThumb']);
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['grandparentArt'];
+				$plexItem['nowPlayingKey'] = (string)$item['grandparentRatingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = 'S' . (string)$item['parentIndex'] . ' · E' . (string)$item['index'];
+				$plexItem['metadataKey'] = (string)$item['grandparentRatingKey'];
+				break;
+			case 'clip':
+				$useImage = (isset($item['live']) ? "plugins/images/cache/livetv.png" : null);
+				$plexItem['type'] = 'clip';
+				$plexItem['title'] = (isset($item['live']) ? 'Live TV' : (string)$item['title']);
+				$plexItem['secondaryTitle'] = '';
+				$plexItem['summary'] = (string)$item['summary'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = isset($item['ratingKey']) ? (string)$item['ratingKey'] . "-np" : (isset($item['live']) ? "livetv.png" : ":)");
+				$plexItem['nowPlayingTitle'] = $plexItem['title'];
+				$plexItem['nowPlayingBottom'] = isset($item['extraType']) ? "Trailer" : (isset($item['live']) ? "Live TV" : ":)");
+				break;
+			case 'album':
+			case 'track':
+				$plexItem['type'] = 'music';
+				$plexItem['title'] = (string)$item['parentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['title'];
+				$plexItem['summary'] = (string)$item['title'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = ($item['parentThumb']) ? (string)$item['parentThumb'] : (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['parentRatingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = (string)$item['parentTitle'];
+				$plexItem['metadataKey'] = isset($item['grandparentRatingKey']) ? (string)$item['grandparentRatingKey'] : (string)$item['parentRatingKey'];
+				break;
+			default:
+				$plexItem['type'] = 'movie';
+				$plexItem['title'] = (string)$item['title'];
+				$plexItem['secondaryTitle'] = (string)$item['year'];
+				$plexItem['summary'] = (string)$item['summary'];
+				$plexItem['ratingKey'] = (string)$item['ratingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = (string)$item['year'];
+				$plexItem['metadataKey'] = (string)$item['ratingKey'];
+		}
+		$plexItem['originalType'] = $item['type'];
+		$plexItem['uid'] = (string)$item['ratingKey'];
+		$plexItem['elapsed'] = isset($item['viewOffset']) && $item['viewOffset'] !== '0' ? (int)$item['viewOffset'] : null;
+		$plexItem['duration'] = isset($item['duration']) ? (int)$item['duration'] : (int)$item->Media['duration'];
+		$plexItem['addedAt'] = isset($item['addedAt']) ? (int)$item['addedAt'] : null;
+		$plexItem['watched'] = ($plexItem['elapsed'] && $plexItem['duration'] ? floor(($plexItem['elapsed'] / $plexItem['duration']) * 100) : 0);
+		$plexItem['transcoded'] = isset($item->TranscodeSession['progress']) ? floor((int)$item->TranscodeSession['progress'] - $plexItem['watched']) : '';
+		$plexItem['stream'] = isset($item->Media->Part->Stream['decision']) ? (string)$item->Media->Part->Stream['decision'] : '';
+		$plexItem['id'] = str_replace('"', '', (string)$item->Player['machineIdentifier']);
+		$plexItem['session'] = (string)$item->Session['id'];
+		$plexItem['bandwidth'] = (string)$item->Session['bandwidth'];
+		$plexItem['bandwidthType'] = (string)$item->Session['location'];
+		$plexItem['sessionType'] = isset($item->TranscodeSession['progress']) ? 'Transcoding' : 'Direct Playing';
+		$plexItem['state'] = (((string)$item->Player['state'] == "paused") ? "pause" : "play");
+		$plexItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['title'] : "";
+		$plexItem['userThumb'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['thumb'] : "";
+		$plexItem['userAddress'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->Player['address'] : "x.x.x.x";
+		$plexItem['address'] = $this->config['plexTabURL'] ? $this->config['plexTabURL'] . "/web/index.html#!/server/" . $this->config['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'] : "https://app.plex.tv/web/app#!/server/" . $this->config['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'];
+		$plexItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '$' . $this->randString();
+		$plexItem['originalImage'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '$' . $this->randString();
+		$plexItem['openTab'] = $this->config['plexTabURL'] && $this->config['plexTabName'] ? true : false;
+		$plexItem['tabName'] = $this->config['plexTabName'] ? $this->config['plexTabName'] : '';
+		// Stream info
+		$plexItem['userStream'] = array(
+			'platform' => (string)$item->Player['platform'],
+			'product' => (string)$item->Player['product'],
+			'device' => (string)$item->Player['device'],
+			'stream' => isset($item->Media) ? (string)$item->Media->Part['decision'] . ($item->TranscodeSession['throttled'] == '1' ? ' (Throttled)' : '') : '',
+			'videoResolution' => (string)$item->Media['videoResolution'],
+			'throttled' => ($item->TranscodeSession['throttled'] == 1) ? true : false,
+			'sourceVideoCodec' => (string)$item->TranscodeSession['sourceVideoCodec'],
+			'videoCodec' => (string)$item->TranscodeSession['videoCodec'],
+			'audioCodec' => (string)$item->TranscodeSession['audioCodec'],
+			'sourceAudioCodec' => (string)$item->TranscodeSession['sourceAudioCodec'],
+			'videoDecision' => $this->streamType((string)$item->TranscodeSession['videoDecision']),
+			'audioDecision' => $this->streamType((string)$item->TranscodeSession['audioDecision']),
+			'container' => (string)$item->TranscodeSession['container'],
+			'audioChannels' => (string)$item->TranscodeSession['audioChannels']
+		);
+		// Genre catch all
+		if ($item->Genre) {
+			$genres = array();
+			foreach ($item->Genre as $key => $value) {
+				$genres[] = (string)$value['tag'];
+			}
+		}
+		// Actor catch all
+		if ($item->Role) {
+			$actors = array();
+			foreach ($item->Role as $key => $value) {
+				if ($value['thumb']) {
+					$actors[] = array(
+						'name' => (string)$value['tag'],
+						'role' => (string)$value['role'],
+						'thumb' => (string)$value['thumb']
+					);
+				}
+			}
+		}
+		// Metadata information
+		$plexItem['metadata'] = array(
+			'guid' => (string)$item['guid'],
+			'summary' => (string)$item['summary'],
+			'rating' => (string)$item['rating'],
+			'duration' => (string)$item['duration'],
+			'originallyAvailableAt' => (string)$item['originallyAvailableAt'],
+			'year' => (string)$item['year'],
+			'studio' => (string)$item['studio'],
+			'tagline' => (string)$item['tagline'],
+			'genres' => ($item->Genre) ? $genres : '',
+			'actors' => ($item->Role) ? $actors : ''
+		);
+		if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
+			$plexItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $plexItem['nowPlayingKey'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
+			$plexItem['imageURL'] = $cacheDirectoryWeb . $plexItem['key'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
+			$plexItem['nowPlayingImageURL'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '';
+		}
+		if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['key'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
+			$plexItem['imageURL'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '';
+		}
+		if (!$plexItem['nowPlayingThumb']) {
+			$plexItem['nowPlayingOriginalImage'] = $plexItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+			$plexItem['nowPlayingKey'] = "no-np";
+		}
+		if (!$plexItem['thumb'] || $plexItem['addedAt'] >= (time() - 300)) {
+			$plexItem['originalImage'] = $plexItem['imageURL'] = "plugins/images/cache/no-list.png";
+			$plexItem['key'] = "no-list";
+		}
+		if (isset($useImage)) {
+			$plexItem['useImage'] = $useImage;
+		}
+		return $plexItem;
+	}
+	
+	public function getPlexHomepageStreams()
+	{
+		if (!$this->config['homepagePlexEnabled']) {
+			$this->setAPIResponse('error', 'Plex homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepagePlexStreams']) {
+			$this->setAPIResponse('error', 'Plex homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexStreamsAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['plexURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexID'])) {
+			$this->setAPIResponse('error', 'Plex Id is not defined', 422);
+			return false;
+		}
+		$ignore = array();
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/status/sessions?X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		libxml_use_internal_errors(true);
+		if ($response->success) {
+			$items = array();
+			$plex = simplexml_load_string($response->body);
+			foreach ($plex as $child) {
+				if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+					$items[] = $this->resolvePlexItem($child);
+				}
+			}
+			$api['content'] = ($resolve) ? $items : $plex;
+			$api['plexID'] = $this->config['plexID'];
+			$api['showNames'] = true;
+			$api['group'] = '1';
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		}
+	}
+	
+	public function getPlexHomepageRecent()
+	{
+		if (!$this->config['homepagePlexEnabled']) {
+			$this->setAPIResponse('error', 'Plex homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepagePlexRecent']) {
+			$this->setAPIResponse('error', 'Plex homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexRecentAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['plexURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexID'])) {
+			$this->setAPIResponse('error', 'Plex Id is not defined', 422);
+			return false;
+		}
+		$ignore = array();
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$urls['movie'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=1";
+		$urls['tv'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=2";
+		$urls['music'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=8";
+		foreach ($urls as $k => $v) {
+			$options = ($this->localURL($v)) ? array('verify' => false) : array();
+			$response = Requests::get($v, array(), $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
+				}
+				if (isset($api)) {
+					$api['content'] = array_merge($api['content'], ($resolve) ? $items : $plex);
+				} else {
+					$api['content'] = ($resolve) ? $items : $plex;
+				}
+			}
+		}
+		if (isset($api['content'])) {
+			usort($api['content'], function ($a, $b) {
+				return $b['addedAt'] <=> $a['addedAt'];
+			});
+		}
+		$api['plexID'] = $this->config['plexID'];
+		$api['showNames'] = true;
+		$api['group'] = '1';
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function getPlexHomepageMetadata($array)
+	{
+		if (!$this->config['homepagePlexEnabled']) {
+			$this->setAPIResponse('error', 'Plex homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepagePlexStreams']) {
+			$this->setAPIResponse('error', 'Plex homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexStreamsAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['plexURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexID'])) {
+			$this->setAPIResponse('error', 'Plex Id is not defined', 422);
+			return false;
+		}
+		$key = $array['key'] ?? null;
+		if (!$key) {
+			$this->setAPIResponse('error', 'Plex Metadata key is not defined', 422);
+			return false;
+		}
+		$ignore = array();
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/library/metadata/" . $key . "?X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		libxml_use_internal_errors(true);
+		if ($response->success) {
+			$items = array();
+			$plex = simplexml_load_string($response->body);
+			foreach ($plex as $child) {
+				if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+					$items[] = $this->resolvePlexItem($child);
+				}
+			}
+			$api['content'] = ($resolve) ? $items : $plex;
+			$api['plexID'] = $this->config['plexID'];
+			$api['showNames'] = true;
+			$api['group'] = '1';
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		}
+	}
+	
+	public function getPlexHomepagePlaylists()
+	{
+		if (!$this->config['homepagePlexEnabled']) {
+			$this->setAPIResponse('error', 'Plex homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepagePlexPlaylist']) {
+			$this->setAPIResponse('error', 'Plex homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexPlaylistAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['plexURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexID'])) {
+			$this->setAPIResponse('error', 'Plex Id is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/playlists?X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		libxml_use_internal_errors(true);
+		if ($response->success) {
+			$items = array();
+			$plex = simplexml_load_string($response->body);
+			foreach ($plex as $child) {
+				if ($child['playlistType'] == "video" && strpos(strtolower($child['title']), 'private') === false) {
+					$playlistTitleClean = preg_replace("/(\W)+/", "", (string)$child['title']);
+					$playlistURL = $this->qualifyURL($this->config['plexURL']);
+					$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $this->config['plexToken'];
+					$options = ($this->localURL($url)) ? array('verify' => false) : array();
+					$playlistResponse = Requests::get($playlistURL, array(), $options);
+					if ($playlistResponse->success) {
+						$playlistResponse = simplexml_load_string($playlistResponse->body);
+						$items[$playlistTitleClean]['title'] = (string)$child['title'];
+						foreach ($playlistResponse->Video as $playlistItem) {
+							$items[$playlistTitleClean][] = $this->resolvePlexItem($playlistItem);
+						}
+					}
+				}
+			}
+			$api['content'] = $items;
+			$api['plexID'] = $this->config['plexID'];
+			$api['showNames'] = true;
+			$api['group'] = '1';
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		} else {
+			$this->setAPIResponse('error', 'Plex API error', 500);
+			return false;
+		}
+		
+	}
+	
+	public function getPlexHomepageSearch($query)
+	{
+		if (!$this->config['homepagePlexEnabled']) {
+			$this->setAPIResponse('error', 'Plex homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagePlexAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['plexURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['plexID'])) {
+			$this->setAPIResponse('error', 'Plex Id is not defined', 422);
+			return false;
+		}
+		$query = $query ?? null;
+		if (!$query) {
+			$this->setAPIResponse('error', 'Plex Metadata key is not defined', 422);
+			return false;
+		}
+		$ignore = array('artist', 'episode');
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/search?query=" . rawurlencode($query) . "&X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		libxml_use_internal_errors(true);
+		if ($response->success) {
+			$items = array();
+			$plex = simplexml_load_string($response->body);
+			foreach ($plex as $child) {
+				if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+					$items[] = $this->resolvePlexItem($child);
+				}
+			}
+			$api['content'] = ($resolve) ? $items : $plex;
+			$api['plexID'] = $this->config['plexID'];
+			$api['showNames'] = true;
+			$api['group'] = '1';
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		}
+	}
+}

+ 124 - 0
api/homepage/qbittorrent.php

@@ -0,0 +1,124 @@
+<?php
+
+trait QBitTorrentHomepageItem
+{
+	public function testConnectionQBittorrent()
+	{
+		if (empty($this->config['qBittorrentURL'])) {
+			$this->setAPIResponse('error', 'qBittorrent URL is not defined', 422);
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['qBittorrentURL'], true);
+		$data = array('username' => $this->config['qBittorrentUsername'], 'password' => $this->decrypt($this->config['qBittorrentPassword']));
+		$apiVersionLogin = ($this->config['qBittorrentApiVersion'] == '1') ? '/login' : '/api/v2/auth/login';
+		$apiVersionQuery = ($this->config['qBittorrentApiVersion'] == '1') ? '/query/torrents?sort=' : '/api/v2/torrents/info?sort=';
+		$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionLogin;
+		try {
+			$options = ($this->localURL($this->config['qBittorrentURL'])) ? array('verify' => false) : array();
+			$response = Requests::post($url, array(), $data, $options);
+			$reflection = new ReflectionClass($response->cookies);
+			$cookie = $reflection->getProperty("cookies");
+			$cookie->setAccessible(true);
+			$cookie = $cookie->getValue($response->cookies);
+			if ($cookie) {
+				$headers = array(
+					'Cookie' => 'SID=' . $cookie['SID']->value
+				);
+				$reverse = $this->config['qBittorrentReverseSorting'] ? 'true' : 'false';
+				$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionQuery . $this->config['qBittorrentSortOrder'] . '&reverse=' . $reverse;
+				$response = Requests::get($url, $headers, $options);
+				if ($response) {
+					$torrents = json_decode($response->body, true);
+					if (is_array($torrents)) {
+						$this->setAPIResponse('success', 'API Connection succeeded', 200);
+						return true;
+					} else {
+						$this->setAPIResponse('error', 'qBittorrent Error Occurred - Check URL or Credentials', 500);
+						return true;
+					}
+				} else {
+					$this->setAPIResponse('error', 'qBittorrent Connection Error Occurred - Check URL or Credentials', 500);
+					return true;
+				}
+			} else {
+				$this->writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getQBittorrentHomepageQueue()
+	{
+		if (!$this->config['homepageqBittorrentEnabled']) {
+			$this->setAPIResponse('error', 'qBittorrent homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageqBittorrentAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['qBittorrentURL'])) {
+			$this->setAPIResponse('error', 'qBittorrent URL is not defined', 422);
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['qBittorrentURL'], true);
+		$data = array('username' => $this->config['qBittorrentUsername'], 'password' => $this->decrypt($this->config['qBittorrentPassword']));
+		$apiVersionLogin = ($this->config['qBittorrentApiVersion'] == '1') ? '/login' : '/api/v2/auth/login';
+		$apiVersionQuery = ($this->config['qBittorrentApiVersion'] == '1') ? '/query/torrents?sort=' : '/api/v2/torrents/info?sort=';
+		$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionLogin;
+		try {
+			$options = ($this->localURL($this->config['qBittorrentURL'])) ? array('verify' => false) : array();
+			$response = Requests::post($url, array(), $data, $options);
+			$reflection = new ReflectionClass($response->cookies);
+			$cookie = $reflection->getProperty("cookies");
+			$cookie->setAccessible(true);
+			$cookie = $cookie->getValue($response->cookies);
+			if ($cookie) {
+				$headers = array(
+					'Cookie' => 'SID=' . $cookie['SID']->value
+				);
+				$reverse = $this->config['qBittorrentReverseSorting'] ? 'true' : 'false';
+				$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionQuery . $this->config['qBittorrentSortOrder'] . '&reverse=' . $reverse;
+				$response = Requests::get($url, $headers, $options);
+				if ($response) {
+					$torrentList = json_decode($response->body, true);
+					if ($this->config['qBittorrentHideSeeding'] || $this->config['qBittorrentHideCompleted']) {
+						$filter = array();
+						$torrents = array();
+						if ($this->config['qBittorrentHideSeeding']) {
+							array_push($filter, 'uploading', 'stalledUP', 'queuedUP');
+						}
+						if ($this->config['qBittorrentHideCompleted']) {
+							array_push($filter, 'pausedUP');
+						}
+						foreach ($torrentList as $key => $value) {
+							if (!in_array($value['state'], $filter)) {
+								$torrents[] = $value;
+							}
+						}
+					} else {
+						$torrents = json_decode($response->body, true);
+					}
+					$api['content']['queueItems'] = $torrents;
+					$api['content']['historyItems'] = false;
+					$api['content'] = isset($api['content']) ? $api['content'] : false;
+					$this->setAPIResponse('success', null, 200, $api);
+					return $api;
+				}
+			} else {
+				$this->writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+}

+ 255 - 0
api/homepage/radarr.php

@@ -0,0 +1,255 @@
+<?php
+
+trait RadarrHomepageItem
+{
+	public function testConnectionRadarr()
+	{
+		if (empty($this->config['radarrURL'])) {
+			$this->setAPIResponse('error', 'Radarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['radarrToken'])) {
+			$this->setAPIResponse('error', 'Radarr Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getSystemStatus();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function getRadarrQueue()
+	{
+		if (!$this->config['homepageRadarrEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageRadarrQueueEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['radarrURL'])) {
+			$this->setAPIResponse('error', 'Radarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['radarrToken'])) {
+			$this->setAPIResponse('error', 'Radarr Token is not defined', 422);
+			return false;
+		}
+		$queueItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getQueue();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? '' : $downloadList;
+				} else {
+					$queue = '';
+				}
+				if (!empty($queue)) {
+					$queueItems = array_merge($queueItems, $queue);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$api['content']['queueItems'] = $queueItems;
+		$api['content']['historyItems'] = false;
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;;
+	}
+	
+	public function getRadarrCalendar($startDate = null, $endDate = null)
+	{
+		$startDate = ($startDate) ?? $_GET['start'];
+		$endDate = ($endDate) ?? $_GET['end'];
+		if (!$this->config['homepageRadarrEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageRadarrQueueEnabled']) {
+			$this->setAPIResponse('error', 'Radarr homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['radarrURL'])) {
+			$this->setAPIResponse('error', 'Radarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['radarrToken'])) {
+			$this->setAPIResponse('error', 'Radarr Token is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getCalendar($startDate, $endDate);
+				$result = json_decode($results, true);
+				if (is_array($result) || is_object($result)) {
+					$calendar = (array_key_exists('error', $result)) ? '' : $this->formatRadarrCalendar($results, $key, $value['url']);
+				} else {
+					$calendar = '';
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($calendar)) {
+				$calendarItems = array_merge($calendarItems, $calendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatRadarrCalendar($array, $number, $url)
+	{
+		$url = rtrim($url, '/'); //remove trailing slash
+		$url = $url . '/api';
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array as $child) {
+			if (isset($child['physicalRelease'])) {
+				$i++;
+				$movieName = $child['title'];
+				$movieID = $child['tmdbId'];
+				if (!isset($movieID)) {
+					$movieID = "";
+				}
+				$physicalRelease = $child['physicalRelease'];
+				$physicalRelease = strtotime($physicalRelease);
+				$physicalRelease = date("Y-m-d", $physicalRelease);
+				if (new DateTime() < new DateTime($physicalRelease)) {
+					$notReleased = "true";
+				} else {
+					$notReleased = "false";
+				}
+				$downloaded = $child['hasFile'];
+				if ($downloaded == "0" && $notReleased == "true") {
+					$downloaded = "text-info";
+				} elseif ($downloaded == "1") {
+					$downloaded = "text-success";
+				} else {
+					$downloaded = "text-danger";
+				}
+				$banner = "/plugins/images/cache/no-np.png";
+				foreach ($child['images'] as $image) {
+					if ($image['coverType'] == "banner" || $image['coverType'] == "fanart") {
+						if (strpos($image['url'], '://') === false) {
+							$imageUrl = $image['url'];
+							$urlParts = explode("/", $url);
+							$imageParts = explode("/", $image['url']);
+							if ($imageParts[1] == end($urlParts)) {
+								unset($imageParts[1]);
+								$imageUrl = implode("/", $imageParts);
+							}
+							$banner = $url . $imageUrl . '?apikey=' . $this->config['radarrToken'];
+						} else {
+							$banner = $image['url'];
+						}
+						
+					}
+				}
+				if ($banner !== "/plugins/images/cache/no-np.png" || (strpos($banner, 'apikey') !== false)) {
+					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+					$imageURL = $banner;
+					$cacheFile = $cacheDirectory . $movieID . '.jpg';
+					$banner = 'plugins/images/cache/' . $movieID . '.jpg';
+					if (!file_exists($cacheFile)) {
+						$this->cacheImage($imageURL, $movieID);
+						unset($imageURL);
+						unset($cacheFile);
+					}
+				}
+				$alternativeTitles = "";
+				foreach ($child['alternativeTitles'] as $alternative) {
+					$alternativeTitles .= $alternative['title'] . ', ';
+				}
+				$alternativeTitles = empty($child['alternativeTitles']) ? "" : substr($alternativeTitles, 0, -2);
+				$details = array(
+					"topTitle" => $movieName,
+					"bottomTitle" => $alternativeTitles,
+					"status" => $child['status'],
+					"overview" => $child['overview'],
+					"runtime" => $child['runtime'],
+					"image" => $banner,
+					"ratings" => $child['ratings']['value'],
+					"videoQuality" => $child["hasFile"] ? @$child['movieFile']['quality']['quality']['name'] : "unknown",
+					"audioChannels" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioChannels'] : "unknown",
+					"audioCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioFormat'] : "unknown",
+					"videoCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['videoCodec'] : "unknown",
+					"size" => $child["hasFile"] ? @$child['movieFile']['size'] : "unknown",
+					"genres" => $child['genres'],
+					"year" => isset($child['year']) ? $child['year'] : '',
+					"studio" => isset($child['studio']) ? $child['studio'] : '',
+				);
+				array_push($gotCalendar, array(
+					"id" => "Radarr-" . $number . "-" . $i,
+					"title" => $movieName,
+					"start" => $physicalRelease,
+					"className" => "inline-popups bg-calendar movieID--" . $movieID,
+					"imagetype" => "film " . $downloaded,
+					"imagetypeFilter" => "film",
+					"downloadFilter" => $downloaded,
+					"bgColor" => str_replace('text', 'bg', $downloaded),
+					"details" => $details
+				));
+			}
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 186 - 0
api/homepage/rtorrent.php

@@ -0,0 +1,186 @@
+<?php
+
+trait RTorrentHomepageItem
+{
+	public function testConnectionRTorrent()
+	{
+		if (empty($this->config['rTorrentURL']) && empty($this->config['rTorrentURLOverride'])) {
+			$this->setAPIResponse('error', 'rTorrent URL is not defined', 422);
+			return false;
+		}
+		try {
+			$digest = (empty($this->config['rTorrentURLOverride'])) ? $this->qualifyURL($this->config['rTorrentURL'], true) : $this->qualifyURL($this->checkOverrideURL($this->config['rTorrentURL'], $this->config['rTorrentURLOverride']), true);
+			$passwordInclude = ($this->config['rTorrentUsername'] !== '' && $this->config['rTorrentPassword'] !== '') ? $this->config['rTorrentUsername'] . ':' . $this->decrypt($this->config['rTorrentPassword']) . "@" : '';
+			$extraPath = (strpos($this->config['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
+			$extraPath = (empty($this->config['rTorrentURLOverride'])) ? $extraPath : '';
+			$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
+			$options = ($this->localURL($url, $this->config['rTorrentDisableCertCheck'])) ? array('verify' => false) : array();
+			if ($this->config['rTorrentUsername'] !== '' && $this->decrypt($this->config['rTorrentPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Digest(array($this->config['rTorrentUsername'], $this->decrypt($this->config['rTorrentPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$data = xmlrpc_encode_request("system.listMethods", null);
+			$response = Requests::post($url, array(), $data, $options);
+			if ($response->success) {
+				$methods = xmlrpc_decode(str_replace('i8>', 'i4>', $response->body));
+				if (count($methods) !== 0) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				}
+			}
+			$this->setAPIResponse('error', 'rTorrent error occurred', 500);
+			return false;
+		} catch
+		(Requests_Exception $e) {
+			$this->writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function checkOverrideURL($url, $override)
+	{
+		if (strpos($override, $url) !== false) {
+			return $override;
+		} else {
+			return $url . $override;
+		}
+	}
+	
+	public function rTorrentStatus($completed, $state, $status)
+	{
+		if ($completed && $state && $status == 'seed') {
+			$state = 'Seeding';
+		} elseif (!$completed && !$state && $status == 'leech') {
+			$state = 'Stopped';
+		} elseif (!$completed && $state && $status == 'leech') {
+			$state = 'Downloading';
+		} elseif ($completed && !$state && $status == 'seed') {
+			$state = 'Finished';
+		}
+		return ($state) ? $state : $status;
+	}
+	
+	public function getRTorrentHomepageQueue()
+	{
+		if (!$this->config['homepagerTorrentEnabled']) {
+			$this->setAPIResponse('error', 'rTorrent homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepagerTorrentAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['rTorrentURL']) && empty($this->config['rTorrentURLOverride'])) {
+			$this->setAPIResponse('error', 'rTorrent URL is not defined', 422);
+			return false;
+		}
+		try {
+			if ($this->config['rTorrentLimit'] == '0') {
+				$this->config['rTorrentLimit'] = '1000';
+			}
+			$torrents = array();
+			$digest = (empty($this->config['rTorrentURLOverride'])) ? $this->qualifyURL($this->config['rTorrentURL'], true) : $this->qualifyURL($this->checkOverrideURL($this->config['rTorrentURL'], $this->config['rTorrentURLOverride']), true);
+			$passwordInclude = ($this->config['rTorrentUsername'] !== '' && $this->config['rTorrentPassword'] !== '') ? $this->config['rTorrentUsername'] . ':' . $this->decrypt($this->config['rTorrentPassword']) . "@" : '';
+			$extraPath = (strpos($this->config['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
+			$extraPath = (empty($this->config['rTorrentURLOverride'])) ? $extraPath : '';
+			$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
+			$options = (localURL($url, $this->config['rTorrentDisableCertCheck'])) ? array('verify' => false) : array();
+			if ($this->config['rTorrentUsername'] !== '' && $this->decrypt($this->config['rTorrentPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Digest(array($this->config['rTorrentUsername'], $this->decrypt($this->config['rTorrentPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$data = xmlrpc_encode_request("d.multicall2", array(
+				"",
+				"main",
+				"d.name=",
+				"d.base_path=",
+				"d.up.total=",
+				"d.size_bytes=",
+				"d.down.total=",
+				"d.completed_bytes=",
+				"d.connection_current=",
+				"d.down.rate=",
+				"d.up.rate=",
+				"d.timestamp.started=",
+				"d.state=",
+				"d.group.name=",
+				"d.hash=",
+				"d.complete=",
+				"d.ratio=",
+				"d.chunk_size=",
+				"f.size_bytes=",
+				"f.size_chunks=",
+				"f.completed_chunks=",
+				"d.custom=",
+				"d.custom1=",
+				"d.custom2=",
+				"d.custom3=",
+				"d.custom4=",
+				"d.custom5=",
+			), array());
+			$response = Requests::post($url, array(), $data, $options);
+			if ($response->success) {
+				$torrentList = xmlrpc_decode(str_replace('i8>', 'string>', $response->body));
+				foreach ($torrentList as $key => $value) {
+					$tempStatus = $this->rTorrentStatus($value[13], $value[10], $value[6]);
+					if ($tempStatus == 'Seeding' && $this->config['rTorrentHideSeeding']) {
+						//do nothing
+					} elseif ($tempStatus == 'Finished' && $this->config['rTorrentHideCompleted']) {
+						//do nothing
+					} else {
+						$torrents[$key] = array(
+							'name' => $value[0],
+							'base' => $value[1],
+							'upTotal' => $value[2],
+							'size' => $value[3],
+							'downTotal' => $value[4],
+							'downloaded' => $value[5],
+							'connectionState' => $value[6],
+							'leech' => $value[7],
+							'seed' => $value[8],
+							'date' => $value[9],
+							'state' => ($value[10]) ? 'on' : 'off',
+							'group' => $value[11],
+							'hash' => $value[12],
+							'complete' => ($value[13]) ? 'yes' : 'no',
+							'ratio' => $value[14],
+							'label' => $value[20],
+							'status' => $tempStatus,
+							'temp' => $value[16] . ' - ' . $value[17] . ' - ' . $value[18],
+							'custom' => $value[19] . ' - ' . $value[20] . ' - ' . $value[21],
+							'custom2' => $value[22] . ' - ' . $value[23] . ' - ' . $value[24],
+						);
+					}
+				}
+				if (count($torrents) !== 0) {
+					usort($torrents, function ($a, $b) {
+						$direction = substr($this->config['rTorrentSortOrder'], -1);
+						$sort = substr($this->config['rTorrentSortOrder'], 0, strlen($this->config['rTorrentSortOrder']) - 1);
+						switch ($direction) {
+							case 'a':
+								return $a[$sort] <=> $b[$sort];
+								break;
+							case 'd':
+								return $b[$sort] <=> $a[$sort];
+								break;
+							default:
+								return $b['date'] <=> $a['date'];
+						}
+					});
+					$torrents = array_slice($torrents, 0, $this->config['rTorrentLimit']);
+				}
+				$api['content']['queueItems'] = $torrents;
+				$api['content']['historyItems'] = false;
+			}
+		} catch
+		(Requests_Exception $e) {
+			$this->writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 149 - 0
api/homepage/sabnzbd.php

@@ -0,0 +1,149 @@
+<?php
+
+trait SabNZBdHomepageItem
+{
+	public function testConnectionSabNZBd()
+	{
+		if (!empty($this->config['sabnzbdURL']) && !empty($this->config['sabnzbdToken'])) {
+			$url = $this->qualifyURL($this->config['sabnzbdURL']);
+			$url = $url . '/api?mode=queue&output=json&apikey=' . $this->config['sabnzbdToken'];
+			try {
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				$response = Requests::get($url, array(), $options);
+				if ($response->success) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			};
+		} else {
+			$this->setAPIResponse('error', 'URL and/or Token not setup', 422);
+			return 'URL and/or Token not setup';
+		}
+	}
+	
+	public function getSabNZBdHomepageQueue()
+	{
+		if (!$this->config['homepageSabnzbdEnabled']) {
+			$this->setAPIResponse('error', 'SabNZBd homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSabnzbdAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['sabnzbdURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sabnzbdToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$url = $url . '/api?mode=queue&output=json&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['queueItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$url = $url . '/api?mode=history&output=json&limit=100&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['historyItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function pauseSabNZBdQueue($target = null)
+	{
+		if (!$this->config['homepageSabnzbdEnabled']) {
+			$this->setAPIResponse('error', 'SabNZBd homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSabnzbdAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['sabnzbdURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sabnzbdToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=pause&value=' . $target . '&' : 'mode=pause';
+		$url = $url . '/api?' . $id . '&output=json&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function resumeSabNZBdQueue($target = null)
+	{
+		if (!$this->config['homepageSabnzbdEnabled']) {
+			$this->setAPIResponse('error', 'SabNZBd homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSabnzbdAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['sabnzbdURL'])) {
+			$this->setAPIResponse('error', 'Plex URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sabnzbdToken'])) {
+			$this->setAPIResponse('error', 'Plex Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=resume&value=' . $target . '&' : 'mode=resume';
+		$url = $url . '/api?' . $id . '&output=json&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 388 - 0
api/homepage/sickrage.php

@@ -0,0 +1,388 @@
+<?php
+
+trait SickRageHomepageItem
+{
+	public function testConnectionSickRage()
+	{
+		if (empty($this->config['sickrageURL'])) {
+			$this->setAPIResponse('error', 'SickRage URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sickrageToken'])) {
+			$this->setAPIResponse('error', 'SickRage Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['sickrageURL'], $this->config['sickrageToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\SickRage\SickRage($value['url'], $value['token']);
+				$results = $downloader->sb();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'SickRage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	
+	public function getSickRageCalendar($startDate = null, $endDate = null)
+	{
+		if (!$this->config['homepageSickrageEnabled']) {
+			$this->setAPIResponse('error', 'SickRage homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSickrageAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageRadarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['sickrageURL'])) {
+			$this->setAPIResponse('error', 'SickRage URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sickrageToken'])) {
+			$this->setAPIResponse('error', 'SickRage Token is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['sickrageURL'], $this->config['sickrageToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\SickRage\SickRage($value['url'], $value['token']);
+				$sickrageFuture = $this->formatSickrageCalendarWanted($downloader->future(), $key);
+				$sickrageHistory = $this->formatSickrageCalendarHistory($downloader->history("100", "downloaded"), $key);
+				if (!empty($sickrageFuture)) {
+					$calendarItems = array_merge($calendarItems, $sickrageFuture);
+				}
+				if (!empty($sickrageHistory)) {
+					$calendarItems = array_merge($calendarItems, $sickrageHistory);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'SickRage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatSickrageCalendarWanted($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array['data']['missed'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Miss-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		foreach ($array['data']['today'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Today-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		foreach ($array['data']['soon'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Soon-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		foreach ($array['data']['later'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Later-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+	
+	public function formatSickrageCalendarHistory($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array['data'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['date'];
+			$downloaded = "text-success";
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']);
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => '',
+				"runtime" => isset($child['series']['runtime']) ? $child['series']['runtime'] : 30,
+				"image" => $fanart,
+				"ratings" => isset($child['series']['ratings']['value']) ? $child['series']['ratings']['value'] : "unknown",
+				"videoQuality" => isset($child["quality"]) ? $child['quality'] : "unknown",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-History-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 249 - 0
api/homepage/sonarr.php

@@ -0,0 +1,249 @@
+<?php
+
+trait SonarrHomepageItem
+{
+	
+	public function testConnectionSonarr()
+	{
+		if (empty($this->config['sonarrURL'])) {
+			$this->setAPIResponse('error', 'Sonarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sonarrToken'])) {
+			$this->setAPIResponse('error', 'Sonarr Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['sonarrURL'], $this->config['sonarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getSystemStatus();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function getSonarrQueue()
+	{
+		if (!$this->config['homepageSonarrEnabled']) {
+			$this->setAPIResponse('error', 'Sonarr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageSonarrQueueEnabled']) {
+			$this->setAPIResponse('error', 'Sonarr homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSonarrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSonarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['sonarrURL'])) {
+			$this->setAPIResponse('error', 'Sonarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sonarrToken'])) {
+			$this->setAPIResponse('error', 'Sonarr Token is not defined', 422);
+			return false;
+		}
+		$queueItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['sonarrURL'], $this->config['sonarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getQueue();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? '' : $downloadList;
+				} else {
+					$queue = '';
+				}
+				if (!empty($queue)) {
+					$queueItems = array_merge($queueItems, $queue);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$api['content']['queueItems'] = $queueItems;
+		$api['content']['historyItems'] = false;
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;;
+	}
+	
+	public function getSonarrCalendar($startDate = null, $endDate = null)
+	{
+		$startDate = ($startDate) ?? $_GET['start'];
+		$endDate = ($endDate) ?? $_GET['end'];
+		if (!$this->config['homepageSonarrEnabled']) {
+			$this->setAPIResponse('error', 'Sonarr homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->config['homepageSonarrQueueEnabled']) {
+			$this->setAPIResponse('error', 'Sonarr homepage module is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSonarrAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSonarrQueueAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage module', 401);
+			return false;
+		}
+		if (empty($this->config['sonarrURL'])) {
+			$this->setAPIResponse('error', 'Sonarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sonarrToken'])) {
+			$this->setAPIResponse('error', 'Sonarr Token is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['sonarrURL'], $this->config['sonarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$sonarr = $sonarr->getCalendar($startDate, $endDate, $this->config['sonarrUnmonitored']);
+				$result = json_decode($sonarr, true);
+				if (is_array($result) || is_object($result)) {
+					$sonarrCalendar = (array_key_exists('error', $result)) ? '' : $this->formatSonarrCalendar($sonarr, $key);
+				} else {
+					$sonarrCalendar = '';
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($sonarrCalendar)) {
+				$calendarItems = array_merge($calendarItems, $sonarrCalendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatSonarrCalendar($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array as $child) {
+			$i++;
+			$seriesName = $child['series']['title'];
+			$seriesID = $child['series']['tvdbId'];
+			$episodeID = $child['series']['tvdbId'];
+			$monitored = $child['monitored'];
+			if (!isset($episodeID)) {
+				$episodeID = "";
+			}
+			//$episodeName = htmlentities($child['title'], ENT_QUOTES);
+			$episodeAirDate = $child['airDateUtc'];
+			$episodeAirDate = strtotime($episodeAirDate);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			if ($child['episodeNumber'] == "1") {
+				$episodePremier = "true";
+			} else {
+				$episodePremier = "false";
+				$date = new DateTime($episodeAirDate);
+				$date->add(new DateInterval("PT1S"));
+				$date->format(DateTime::ATOM);
+				$child['airDateUtc'] = gmdate('Y-m-d\TH:i:s\Z', strtotime($date->format(DateTime::ATOM)));
+			}
+			$downloaded = $child['hasFile'];
+			if ($downloaded == "0" && isset($unaired) && $episodePremier == "true") {
+				$downloaded = "text-primary animated flash";
+			} elseif ($downloaded == "0" && isset($unaired) && $monitored == "0") {
+				$downloaded = "text-dark";
+			} elseif ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$fanart = "/plugins/images/cache/no-np.png";
+			foreach ($child['series']['images'] as $image) {
+				if ($image['coverType'] == "fanart") {
+					$fanart = $image['url'];
+				}
+			}
+			if ($fanart !== "/plugins/images/cache/no-np.png" || (strpos($fanart, '://') === false)) {
+				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+				$imageURL = $fanart;
+				$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				if (!file_exists($cacheFile)) {
+					$this->cacheImage($imageURL, $seriesID);
+					unset($imageURL);
+					unset($cacheFile);
+				}
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
+			$details = array(
+				"seasonCount" => $child['series']['seasonCount'],
+				"status" => $child['series']['status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['overview']) ? $child['overview'] : '',
+				"runtime" => $child['series']['runtime'],
+				"image" => $fanart,
+				"ratings" => $child['series']['ratings']['value'],
+				"videoQuality" => $child["hasFile"] && isset($child['episodeFile']['quality']['quality']['name']) ? $child['episodeFile']['quality']['quality']['name'] : "unknown",
+				"audioChannels" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioChannels'] : "unknown",
+				"audioCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioCodec'] : "unknown",
+				"videoCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['videoCodec'] : "unknown",
+				"size" => $child["hasFile"] && isset($child['episodeFile']['size']) ? $child['episodeFile']['size'] : "unknown",
+				"genres" => $child['series']['genres'],
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sonarr-" . $number . "-" . $i,
+				"title" => $seriesName,
+				"start" => $child['airDateUtc'],
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 48 - 0
api/homepage/speedtest.php

@@ -0,0 +1,48 @@
+<?php
+
+trait SpeedTestHomepageItem
+{
+	public function getSpeedtestHomepageData()
+	{
+		if (!$this->config['homepageSpeedtestEnabled']) {
+			$this->setAPIResponse('error', 'SpeedTest homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageSpeedtestAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['speedtestURL'])) {
+			$this->setAPIResponse('error', 'SpeedTest URL is not defined', 422);
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['speedtestURL']);
+		$dataUrl = $url . '/api/speedtest/latest';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$api['data'] = [
+					'current' => $json['data'],
+					'average' => $json['average'],
+					'max' => $json['max'],
+				];
+				$api['options'] = [
+					'title' => $this->config['speedtestHeader'],
+					'titleToggle' => $this->config['speedtestHeaderToggle'],
+				];
+			} else {
+				$this->setAPIResponse('error', 'SpeedTest connection error', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Speedtest Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 138 - 0
api/homepage/tatutulli.php

@@ -0,0 +1,138 @@
+<?php
+
+trait TautulliHomepageItem
+{
+	public function testConnectionTautulli()
+	{
+		if (empty($this->config['tautulliURL'])) {
+			$this->setAPIResponse('error', 'Tautulli URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['tautulliApikey'])) {
+			$this->setAPIResponse('error', 'Tautulli Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['tautulliURL']);
+		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
+		try {
+			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
+			$homestats = Requests::get($homestatsUrl, [], []);
+			if ($homestats->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Tautulli Error Occurred - Check URL or Credentials', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getTautulliHomepageData()
+	{
+		if (!$this->config['homepageTautulliEnabled']) {
+			$this->setAPIResponse('error', 'Tautulli homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageTautulliAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['tautulliURL'])) {
+			$this->setAPIResponse('error', 'Tautulli URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['tautulliApikey'])) {
+			$this->setAPIResponse('error', 'Tautulli Token is not defined', 422);
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['tautulliURL']);
+		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		try {
+			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
+			$homestats = Requests::get($homestatsUrl, [], []);
+			if ($homestats->success) {
+				$homestats = json_decode($homestats->body, true);
+				$api['homestats'] = $homestats['response'];
+				// Cache art & thumb for first result in each tautulli API result
+				$categories = ['top_movies', 'top_tv', 'popular_movies', 'popular_tv'];
+				foreach ($categories as $cat) {
+					$key = array_search($cat, array_column($api['homestats']['data'], 'stat_id'));
+					$img = $api['homestats']['data'][$key]['rows'][0];
+					$this->cacheImage($url . '/pms_image_proxy?img=' . $img['art'] . '&rating_key=' . $img['rating_key'] . '&width=' . $nowPlayingWidth . '&height=' . $nowPlayingHeight, $img['rating_key'] . '-np');
+					$this->cacheImage($url . '/pms_image_proxy?img=' . $img['thumb'] . '&rating_key=' . $img['rating_key'] . '&width=' . $width . '&height=' . $height, $img['rating_key'] . '-list');
+					$img['art'] = 'plugins/images/cache/' . $img['rating_key'] . '-np.jpg';
+					$img['thumb'] = 'plugins/images/cache/' . $img['rating_key'] . '-list.jpg';
+					$api['homestats']['data'][$key]['rows'][0] = $img;
+				}
+				// Cache the platform icon
+				$key = array_search('top_platforms', array_column($api['homestats']['data'], 'stat_id'));
+				$platform = $api['homestats']['data'][$key]['rows'][0]['platform_name'];
+				$this->cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
+			}
+			$libstatsUrl = $apiURL . '&cmd=get_libraries';
+			$libstats = Requests::get($libstatsUrl, [], []);
+			if ($libstats->success) {
+				$libstats = json_decode($libstats->body, true);
+				$api['libstats'] = $libstats['response'];
+				$categories = ['movie.svg', 'show.svg', 'artist.svg'];
+				foreach ($categories as $cat) {
+					$parts = explode('.', $cat);
+					$this->cacheImage($url . '/images/libraries/' . $cat, 'tautulli-' . $parts[0], $parts[1]);
+				}
+			}
+			$api['options'] = [
+				'url' => $url,
+				'libraries' => $this->config['tautulliLibraries'],
+				'topMovies' => $this->config['tautulliTopMovies'],
+				'topTV' => $this->config['tautulliTopTV'],
+				'topUsers' => $this->config['tautulliTopUsers'],
+				'topPlatforms' => $this->config['tautulliTopPlatforms'],
+				'popularMovies' => $this->config['tautulliPopularMovies'],
+				'popularTV' => $this->config['tautulliPopularTV'],
+				'title' => $this->config['tautulliHeaderToggle'],
+			];
+			$ids = []; // Array of stat_ids to remove from the returned array
+			if (!$this->qualifyRequest($this->config['homepageTautulliLibraryAuth'])) {
+				$api['options']['libraries'] = false;
+				unset($api['libstats']);
+			}
+			if (!$this->qualifyRequest($this->config['homepageTautulliViewsAuth'])) {
+				$api['options']['topMovies'] = false;
+				$api['options']['topTV'] = false;
+				$api['options']['popularMovies'] = false;
+				$api['options']['popularTV'] = false;
+				$ids = array_merge(['top_movies', 'popular_movies', 'popular_tv', 'top_tv'], $ids);
+				$api['homestats']['data'] = array_values($api['homestats']['data']);
+			}
+			if (!$this->qualifyRequest($this->config['homepageTautulliMiscAuth'])) {
+				$api['options']['topUsers'] = false;
+				$api['options']['topPlatforms'] = false;
+				$ids = array_merge(['top_platforms', 'top_users'], $ids);
+				$api['homestats']['data'] = array_values($api['homestats']['data']);
+			}
+			$ids = array_merge(['top_music', 'popular_music', 'last_watched', 'most_concurrent'], $ids);
+			foreach ($ids as $id) {
+				if ($key = array_search($id, array_column($api['homestats']['data'], 'stat_id'))) {
+					unset($api['homestats']['data'][$key]);
+					$api['homestats']['data'] = array_values($api['homestats']['data']);
+				}
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 122 - 0
api/homepage/transmission.php

@@ -0,0 +1,122 @@
+<?php
+
+trait TransmissionHomepageItem
+{
+	public function testConnectionTransmission()
+	{
+		if (empty($this->config['transmissionURL'])) {
+			$this->setAPIResponse('error', 'Transmission URL is not defined', 422);
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['transmissionURL'], true);
+		$passwordInclude = ($this->config['transmissionUsername'] != '' && $this->config['transmissionPassword'] != '') ? $this->config['transmissionUsername'] . ':' . $this->decrypt($this->config['transmissionPassword']) . "@" : '';
+		$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . '/rpc';
+		try {
+			$options = ($this->localURL($this->config['transmissionURL'])) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->headers['x-transmission-session-id']) {
+				$headers = array(
+					'X-Transmission-Session-Id' => $response->headers['x-transmission-session-id'],
+					'Content-Type' => 'application/json'
+				);
+				$data = array(
+					'method' => 'torrent-get',
+					'arguments' => array(
+						'fields' => array(
+							"id", "name", "totalSize", "eta", "isFinished", "isStalled", "percentDone", "rateDownload", "status", "downloadDir", "errorString"
+						),
+					),
+					'tags' => ''
+				);
+				$response = Requests::post($url, $headers, json_encode($data), $options);
+				if ($response->success) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				} else {
+					$this->setAPIResponse('error', 'Transmission Connect Function - Error: Unknown', 500);
+					return false;
+				}
+			} else {
+				$this->writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getTransmissionHomepageQueue()
+	{
+		if (!$this->config['homepageTransmissionEnabled']) {
+			$this->setAPIResponse('error', 'Transmission homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageTransmissionAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['transmissionURL'])) {
+			$this->setAPIResponse('error', 'Transmission URL is not defined', 422);
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['transmissionURL'], true);
+		$passwordInclude = ($this->config['transmissionUsername'] != '' && $this->config['transmissionPassword'] != '') ? $this->config['transmissionUsername'] . ':' . $this->decrypt($this->config['transmissionPassword']) . "@" : '';
+		$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . '/rpc';
+		try {
+			$options = ($this->localURL($this->config['transmissionURL'])) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->headers['x-transmission-session-id']) {
+				$headers = array(
+					'X-Transmission-Session-Id' => $response->headers['x-transmission-session-id'],
+					'Content-Type' => 'application/json'
+				);
+				$data = array(
+					'method' => 'torrent-get',
+					'arguments' => array(
+						'fields' => array(
+							"id", "name", "totalSize", "eta", "isFinished", "isStalled", "percentDone", "rateDownload", "status", "downloadDir", "errorString"
+						),
+					),
+					'tags' => ''
+				);
+				$response = Requests::post($url, $headers, json_encode($data), $options);
+				if ($response->success) {
+					$torrentList = json_decode($response->body, true)['arguments']['torrents'];
+					if ($this->config['transmissionHideSeeding'] || $this->config['transmissionHideCompleted']) {
+						$filter = array();
+						$torrents = array();
+						if ($this->config['transmissionHideSeeding']) {
+							array_push($filter, 6, 5);
+						}
+						if ($this->config['transmissionHideCompleted']) {
+							array_push($filter, 0);
+						}
+						foreach ($torrentList as $key => $value) {
+							if (!in_array($value['status'], $filter)) {
+								$torrents[] = $value;
+							}
+						}
+					} else {
+						$torrents = json_decode($response->body, true);
+					}
+					$api['content']['queueItems'] = $torrents;
+					$api['content']['historyItems'] = false;
+				}
+			} else {
+				$this->writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 195 - 0
api/homepage/unifi.php

@@ -0,0 +1,195 @@
+<?php
+
+trait UnifiHomepageItem
+{
+	public function getUnifiSiteName()
+	{
+		if (empty($this->config['unifiURL'])) {
+			$this->setAPIResponse('error', 'Unifi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['unifiUsername'])) {
+			$this->setAPIResponse('error', 'Unifi Username is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['unifiPassword'])) {
+			$this->setAPIResponse('error', 'Unifi Password is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['unifiURL']);
+		try {
+			$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => false);
+			$data = array(
+				'username' => $this->config['unifiUsername'],
+				'password' => $this->decrypt($this->config['unifiPassword']),
+				'remember' => true,
+				'strict' => true
+			);
+			$response = Requests::post($url . '/api/login', array(), json_encode($data), $options);
+			if ($response->success) {
+				$cookie['unifises'] = ($response->cookies['unifises']->value) ?? false;
+				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
+				return false;
+			}
+			$headers = array(
+				'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
+			);
+			$response = Requests::get($url . '/api/self/sites', $headers, $options);
+			if ($response->success) {
+				$body = json_decode($response->body, true);
+				$this->setAPIResponse('success', null, 200, $body);
+				return $body;
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Error Occurred', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+		
+	}
+	
+	public function testConnectionUnifi()
+	{
+		if (empty($this->config['unifiURL'])) {
+			$this->setAPIResponse('error', 'Unifi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['unifiUsername'])) {
+			$this->setAPIResponse('error', 'Unifi Username is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['unifiPassword'])) {
+			$this->setAPIResponse('error', 'Unifi Password is not defined', 422);
+			return false;
+		}
+		$api['content']['unifi'] = array();
+		$url = $this->qualifyURL($this->config['unifiURL']);
+		$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => true);
+		$data = array(
+			'username' => $this->config['unifiUsername'],
+			'password' => $this->decrypt($this->config['unifiPassword']),
+			'remember' => true,
+			'strict' => true
+		);
+		try {
+			// Is this UnifiOs or Regular
+			$response = Requests::get($url, [], $options);
+			if ($response->success) {
+				$csrfToken = ($response->headers['x-csrf-token']) ?? false;
+				$data = ($csrfToken) ? $data : json_encode($data);
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Check URL', 409);
+				return false;
+			}
+			$urlLogin = ($csrfToken) ? $url . '/api/auth/login' : $url . '/api/login';
+			$urlStat = ($csrfToken) ? $url . '/proxy/network/api/s/default/stat/health' : $url . '/api/s/' . $this->config['unifiSiteName'] . '/stat/health';
+			$response = Requests::post($urlLogin, [], $data, $options);
+			if ($response->success) {
+				$cookie['unifises'] = ($response->cookies['unifises']->value) ?? false;
+				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
+				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
+				$options['cookies'] = $response->cookies;
+				
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
+				return false;
+			}
+			$headers = array(
+				'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
+			);
+			$response = Requests::get($urlStat, $headers, $options);
+			if ($response->success) {
+				$api['content']['unifi'] = json_decode($response->body, true);
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error3', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content']['unifi'] = isset($api['content']['unifi']) ? $api['content']['unifi'] : false;
+		$this->setAPIResponse('success', 'API Connection succeeded', 200);
+		return true;
+	}
+	
+	public function getUnifiHomepageData()
+	{
+		if (!$this->config['homepageUnifiEnabled']) {
+			$this->setAPIResponse('error', 'Unifi homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageUnifiAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['unifiURL'])) {
+			$this->setAPIResponse('error', 'Unifi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['unifiUsername'])) {
+			$this->setAPIResponse('error', 'Unifi Username is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['unifiPassword'])) {
+			$this->setAPIResponse('error', 'Unifi Password is not defined', 422);
+			return false;
+		}
+		$api['content']['unifi'] = array();
+		$url = $this->qualifyURL($this->config['unifiURL']);
+		$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => true);
+		$data = array(
+			'username' => $this->config['unifiUsername'],
+			'password' => $this->decrypt($this->config['unifiPassword']),
+			'remember' => true,
+			'strict' => true
+		);
+		try {
+			// Is this UnifiOs or Regular
+			$response = Requests::get($url, [], $options);
+			if ($response->success) {
+				$csrfToken = ($response->headers['x-csrf-token']) ?? false;
+				$data = ($csrfToken) ? $data : json_encode($data);
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Check URL', 409);
+				return false;
+			}
+			$urlLogin = ($csrfToken) ? $url . '/api/auth/login' : $url . '/api/login';
+			$urlStat = ($csrfToken) ? $url . '/proxy/network/api/s/default/stat/health' : $url . '/api/s/' . $this->config['unifiSiteName'] . '/stat/health';
+			$response = Requests::post($urlLogin, [], $data, $options);
+			if ($response->success) {
+				$cookie['unifises'] = ($response->cookies['unifises']->value) ?? false;
+				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
+				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
+				$options['cookies'] = $response->cookies;
+				
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
+				return false;
+			}
+			$headers = array(
+				'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
+			);
+			$response = Requests::get($urlStat, $headers, $options);
+			if ($response->success) {
+				$api['content']['unifi'] = json_decode($response->body, true);
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error3', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content']['unifi'] = isset($api['content']['unifi']) ? $api['content']['unifi'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 86 - 0
api/homepage/weather.php

@@ -0,0 +1,86 @@
+<?php
+
+trait WeatherHomepageItem
+{
+	public function searchCityForCoordinates($query)
+	{
+		try {
+			$query = $query ?? false;
+			if (!$query) {
+				$this->setAPIResponse('error', 'Query was not supplied', 422);
+				return false;
+			}
+			$url = $this->qualifyURL('https://api.mapbox.com/geocoding/v5/mapbox.places/' . urlencode($query) . '.json?access_token=pk.eyJ1IjoiY2F1c2VmeCIsImEiOiJjazhyeGxqeXgwMWd2M2ZydWQ4YmdjdGlzIn0.R50iYuMewh1CnUZ7sFPdHA&limit=5&fuzzyMatch=true');
+			$options = array('verify' => false);
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', null, 200, json_decode($response->body));
+				return json_decode($response->body);
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function getWeatherAndAirData()
+	{
+		if (!$this->config['homepageWeatherAndAirEnabled']) {
+			$this->setAPIResponse('error', 'Weather homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageWeatherAndAirAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['homepageWeatherAndAirLatitude']) && empty($this->config['homepageWeatherAndAirLongitude'])) {
+			$this->setAPIResponse('error', 'Weather Latitude and/or Longitude were not defined', 422);
+			return false;
+		}
+		$api['content'] = array(
+			'weather' => false,
+			'air' => false,
+			'pollen' => false
+		);
+		$apiURL = $this->qualifyURL('https://api.breezometer.com/');
+		$info = '&lat=' . $this->config['homepageWeatherAndAirLatitude'] . '&lon=' . $this->config['homepageWeatherAndAirLongitude'] . '&units=' . $this->config['homepageWeatherAndAirUnits'] . '&key=b7401295888443538a7ebe04719c8394';
+		try {
+			$headers = array();
+			$options = array();
+			if ($this->config['homepageWeatherAndAirWeatherEnabled']) {
+				$endpoint = '/weather/v1/forecast/hourly?hours=120&metadata=true';
+				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
+				if ($response->success) {
+					$apiData = json_decode($response->body, true);
+					$api['content']['weather'] = ($apiData['error'] === null) ? $apiData : false;
+					unset($apiData);
+				}
+			}
+			if ($this->config['homepageWeatherAndAirAirQualityEnabled']) {
+				$endpoint = '/air-quality/v2/current-conditions?features=breezometer_aqi,local_aqi,health_recommendations,sources_and_effects,dominant_pollutant_concentrations,pollutants_concentrations,pollutants_aqi_information&metadata=true';
+				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
+				if ($response->success) {
+					$apiData = json_decode($response->body, true);
+					$api['content']['air'] = ($apiData['error'] === null) ? $apiData : false;
+					unset($apiData);
+				}
+			}
+			if ($this->config['homepageWeatherAndAirPollenEnabled']) {
+				$endpoint = '/pollen/v2/forecast/daily?features=plants_information,types_information&days=1&metadata=true';
+				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
+				if ($response->success) {
+					$apiData = json_decode($response->body, true);
+					$api['content']['pollen'] = ($apiData['error'] === null) ? $apiData : false;
+					unset($apiData);
+				}
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Weather And Air Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}