Преглед изворни кода

Merge remote-tracking branch 'upstream/v2-develop' into homescreen-icon

Yann Jouanique пре 4 година
родитељ
комит
ca9baf55e1

+ 36 - 24
api/classes/organizr.class.php

@@ -10,6 +10,7 @@ class Organizr
 	use AuthFunctions;
 	use BackupFunctions;
 	use ConfigFunctions;
+	use DemoFunctions;
 	use HomepageConnectFunctions;
 	use HomepageFunctions;
 	use LogFunctions;
@@ -246,22 +247,25 @@ class Organizr
 		}
 	}
 	
-	public function checkDiskSpace()
+	public function checkDiskSpace($directory = './')
 	{
-		$disk = $this->checkDisk('/');
-		$diskLevels = [
-			'warn' => 1000000000,
-			'error' => 100000000
-		];
-		if ($disk['free'] <= $diskLevels['error']) {
-			die($this->showHTML('Low Disk Space', 'You are dangerously low on disk space.<br/>There is only ' . $disk['free']['human_readable'] . ' remaining.<br/><b>Percent Used = ' . $disk['used']['percent_used'] . '%</b>'));
-		} elseif ($disk['free'] <= $diskLevels['warn']) {
-			$this->warnings[] = 'You are low on disk space.  There is only ' . $disk['free']['human_readable'] . ' remaining.';
+		$readable = @is_readable($directory);
+		if ($readable) {
+			$disk = $this->checkDisk($directory);
+			$diskLevels = [
+				'warn' => 1000000000,
+				'error' => 100000000
+			];
+			if ($disk['free'] <= $diskLevels['error']) {
+				die($this->showHTML('Low Disk Space', 'You are dangerously low on disk space.<br/>There is only ' . $disk['free']['human_readable'] . ' remaining.<br/><b>Percent Used = ' . $disk['used']['percent_used'] . '%</b>'));
+			} elseif ($disk['free'] <= $diskLevels['warn']) {
+				$this->warnings[] = 'You are low on disk space.  There is only ' . $disk['free']['human_readable'] . ' remaining.';
+			}
 		}
 		return true;
 	}
 	
-	public function getFreeSpace($directory = '/')
+	public function getFreeSpace($directory = './')
 	{
 		$disk = disk_free_space($directory);
 		return [
@@ -270,7 +274,7 @@ class Organizr
 		];
 	}
 	
-	public function getDiskSpace($directory = '/')
+	public function getDiskSpace($directory = './')
 	{
 		$disk = disk_total_space($directory);
 		return [
@@ -279,7 +283,7 @@ class Organizr
 		];
 	}
 	
-	public function getUsedSpace($directory = '/')
+	public function getUsedSpace($directory = './')
 	{
 		$diskFree = $this->getFreeSpace($directory);
 		$diskTotal = $this->getDiskSpace($directory);
@@ -294,13 +298,23 @@ class Organizr
 		];
 	}
 	
-	public function checkDisk($directory = '/')
+	public function checkDisk($directory = './')
 	{
-		return [
-			'free' => $this->getFreeSpace('/'),
-			'used' => $this->getUsedSpace('/'),
-			'total' => $this->getDiskSpace('/'),
-		];
+		$readable = @is_readable($directory);
+		if ($readable) {
+			return [
+				'free' => $this->getFreeSpace($directory),
+				'used' => $this->getUsedSpace($directory),
+				'total' => $this->getDiskSpace($directory),
+			];
+		} else {
+			return [
+				'free' => 'error accessing path',
+				'used' => 'error accessing path',
+				'total' => 'error accessing path',
+			];
+		}
+		
 	}
 	
 	public function errorCodes($error = 000)
@@ -2255,7 +2269,7 @@ class Organizr
 					'class' => 'getPlexTokenAuth plexAuth switchAuth',
 					'icon' => 'fa fa-ticket',
 					'text' => 'Retrieve',
-					'attr' => 'onclick="showPlexTokenForm(\'#settings-main-form [name=plexToken]\')"'
+					'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, null, \'#settings-main-form [name=plexToken]\')"'
 				),
 				array(
 					'type' => 'password-alt',
@@ -2844,7 +2858,7 @@ class Organizr
 					'label' => 'Get Plex Token',
 					'icon' => 'fa fa-ticket',
 					'text' => 'Retrieve',
-					'attr' => 'onclick="showPlexTokenForm(\'#sso-form [name=plexToken]\')"'
+					'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, null, \'#sso-form [name=plexToken]\')"'
 				],
 				[
 					'type' => 'password-alt',
@@ -2905,17 +2919,15 @@ class Organizr
 				[
 					'type' => 'input',
 					'name' => 'overseerrFallbackUser',
-					'label' => 'Overseerr Fallback User',
+					'label' => 'Overseerr Fallback Email',
 					'value' => $this->config['overseerrFallbackUser'],
 					'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO.  Organizr will request a User Token based off of this user credentials',
-					'attr' => 'disabled'
 				],
 				[
 					'type' => 'password-alt',
 					'name' => 'overseerrFallbackPassword',
 					'label' => 'Overseerr Fallback Password',
 					'value' => $this->config['overseerrFallbackPassword'],
-					'attr' => 'disabled'
 				],
 				[
 					'type' => 'switch',

+ 15 - 1
api/functions/config-functions.php

@@ -2,8 +2,22 @@
 
 trait ConfigFunctions
 {
-	public function getConfigItem($item)
+	public function getConfigItem($item, $term = null)
 	{
+		if (strtolower($item) == 'search') {
+			$configItems = $this->config;
+			$results = [];
+			foreach ($configItems as $configItem => $configItemValue) {
+				if (stripos($configItem, $term) !== false) {
+					$results[$configItem] = $configItemValue;
+					if ($configItem == 'organizrHash') {
+						$results[$configItem] = '***Secure***';
+					}
+				}
+			}
+			$this->setAPIResponse('success', 'Search results for term: ' . $term, 200, $results);
+			return $results;
+		}
 		if ($this->config[$item]) {
 			$configItem = $this->config[$item];
 			if ($item == 'organizrHash') {

+ 24 - 0
api/functions/demo-functions.php

@@ -0,0 +1,24 @@
+<?php
+/** @noinspection PhpUndefinedFieldInspection */
+
+trait DemoFunctions
+{
+	public function demoData($file = null)
+	{
+		if (!$file) {
+			$this->setResponse(422, 'Demo file was not supplied');
+			return false;
+		}
+		$path = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'demo_data' . DIRECTORY_SEPARATOR . $file;
+		if (file_exists($path)) {
+			
+			$data = file_get_contents($path);
+			$data = json_decode($data, true);
+			$this->setResponse(200, 'Demo data for file: ' . $file, $data['response']['data']);
+			return $data;
+		} else {
+			$this->setResponse(404, 'Demo data was not found for file: ' . $file);
+			return false;
+		}
+	}
+}

+ 11 - 10
api/functions/sso-functions.php

@@ -26,7 +26,7 @@ trait SSOFunctions
 		$map = array(
 			'jellyfin' => 'username',
 			'ombi' => 'username',
-			'overseerr' => 'username',
+			'overseerr' => 'email',
 			'tautulli' => 'username',
 			'petio' => 'username'
 		);
@@ -62,7 +62,8 @@ trait SSOFunctions
 			}
 		}
 		if ($this->config['ssoOverseerr']) {
-			$overseerrToken = $this->getOverseerrToken($this->getSSOUserFor('overseerr', $userobj), $password, $token);
+			$fallback = ($this->config['overseerrFallbackUser'] !== '' && $this->config['overseerrFallbackPassword'] !== '');
+			$overseerrToken = $this->getOverseerrToken($this->getSSOUserFor('overseerr', $userobj), $password, $token, $fallback);
 			if ($overseerrToken) {
 				$this->coookie('set', 'connect.sid', $overseerrToken, $this->config['rememberMeDays'], false);
 			}
@@ -193,7 +194,7 @@ trait SSOFunctions
 		return ($token) ? $token : false;
 	}
 	
-	public function getOverseerrToken($username, $password, $oAuthToken = null, $fallback = false)
+	public function getOverseerrToken($email, $password, $oAuthToken = null, $fallback = false)
 	{
 		$token = null;
 		try {
@@ -203,26 +204,26 @@ trait SSOFunctions
 				"X-Forwarded-For" => $this->userIP()
 			);
 			$data = array(
-				//"username" => ($oAuthToken ? "" : $username), // not needed yet
-				//"password" => ($oAuthToken ? "" : $password), // not needed yet
+				"email" => ($oAuthToken ? "" : $email), // not needed yet
+				"password" => ($oAuthToken ? "" : $password), // not needed yet
 				"authToken" => $oAuthToken
 			);
-			$endpoint = '/api/v1/auth/plex';
+			$endpoint = ($oAuthToken ? '/api/v1/auth/plex' : '/api/v1/auth/local');
 			$options = $this->requestOptions($url, 60000);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 				$user = json_decode($response->body, true); // not really needed yet
 				$token = $response->cookies['connect.sid']->value;
-				$this->writeLog('success', 'Overseerr Token Function - Grabbed token', $user['plexUsername']);
+				$this->writeLog('success', 'Overseerr Token Function - Grabbed token', $user['plexUsername'] ?? $email);
 			} else {
 				if ($fallback) {
-					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token - Will retry using fallback credentials', $username);
+					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token - Will retry using fallback credentials', $email);
 				} else {
-					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token', $username);
+					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token', $email);
 				}
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Token Function - Error: ' . $e->getMessage(), $username);
+			$this->writeLog('error', 'Overseerr Token Function - Error: ' . $e->getMessage(), $email);
 		}
 		if ($token) {
 			return urldecode($token);

+ 9 - 7
api/homepage/overseerr.php

@@ -143,11 +143,13 @@ trait OverseerrHomepageItem
 		if (!$this->homepageItemPermissions($this->overseerrHomepagePermissions('main'), true)) {
 			return false;
 		}
+		$limit = is_numeric($limit) ? (int)$limit : 50;
+		$offset = is_numeric($offset) ? (int)$offset : 0;
 		$api['count'] = [
 			'movie' => 0,
 			'tv' => 0,
-			'limit' => (integer)$limit,
-			'offset' => (integer)$offset
+			'limit' => $limit,
+			'offset' => $offset
 		];
 		$headers = [
 			"Accept" => "application/json",
@@ -163,7 +165,7 @@ trait OverseerrHomepageItem
 				foreach ($requestsData['results'] as $key => $value) {
 					$requester = ($value['requestedBy']['username'] !== '') ? $value['requestedBy']['username'] : $value['requestedBy']['plexUsername'];
 					$requesterEmail = $value['requestedBy']['email'];
-					$proceed = (($this->config['overseerrLimitUser']) && strtolower($this->user['username']) == strtolower($requester)) || (strtolower($requester) == strtolower($this->config['ombiFallbackUser'])) || (!$this->config['ombiLimitUser']) || $this->qualifyRequest(1);
+					$proceed = (($this->config['overseerrLimitUser']) && strtolower($this->user['username']) == strtolower($requester)) || (strtolower($requester) == strtolower($this->config['overseerrFallbackUser'])) || (!$this->config['overseerrLimitUser']) || $this->qualifyRequest(1);
 					if ($proceed) {
 						$requestItem = Requests::get($url . '/api/v1/' . $value['type'] . '/' . $value['media']['tmdbId'], $headers, $options);
 						$requestsItemData = json_decode($requestItem->body, true);
@@ -338,13 +340,13 @@ trait OverseerrHomepageItem
 						'mediaId' => (int)$id,
 						'tvdbId' => $seriesInfo['externalIds']['tvdbId'],
 						'mediaType' => 'tv',
-						'is4k' => $serviceInfo['is4k'],
+						'is4k' => (bool)$serviceInfo['is4k'],
 						'seasons' => $seasons,
 						'serverId' => (int)$serviceInfo['id'],
 						'profileId' => (int)$serviceInfo['activeProfileId'],
 						'rootFolder' => $serviceInfo['activeDirectory'],
 						'languageProfileId' => (int)$serviceInfo['activeLanguageProfileId'],
-						'userId' => (int)$userInfo['id'],
+						//'userId' => (int)$userInfo['id'],
 						'tags' => []
 					];
 					break;
@@ -359,10 +361,10 @@ trait OverseerrHomepageItem
 					$add = [
 						'mediaId' => (int)$id,
 						'mediaType' => 'movie',
-						'is4k' => $serviceInfo['is4k'],
+						'is4k' => (bool)$serviceInfo['is4k'],
 						'serverId' => (int)$serviceInfo['id'],
 						'profileId' => (int)$serviceInfo['activeProfileId'],
-						'userId' => (int)$userInfo['id'],
+						//'userId' => (int)$userInfo['id'],
 						'tags' => []
 					];
 					break;

+ 1 - 1
api/homepage/plex.php

@@ -36,7 +36,7 @@ trait PlexHomepageItem
 					$this->settingsOption('disable-cert-check', 'plexDisableCertCheck'),
 					$this->settingsOption('use-custom-certificate', 'plexUseCustomCertificate'),
 					$this->settingsOption('token', 'plexToken'),
-					$this->settingsOption('button', '', ['label' => 'Get Plex Token', 'icon' => 'fa fa-ticket', 'text' => 'Retrieve', 'attr' => 'onclick="showPlexTokenForm(\'#homepage-Plex-form [name=plexToken]\')"']),
+					$this->settingsOption('button', '', ['label' => 'Get Plex Token', 'icon' => 'fa fa-ticket', 'text' => 'Retrieve', 'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, null, \'#homepage-Plex-form [name=plexToken]\')"']),
 					$this->settingsOption('password-alt', 'plexID', ['label' => 'Plex Machine']),
 					$this->settingsOption('button', '', ['label' => 'Get Plex Machine', 'icon' => 'fa fa-id-badge', 'text' => 'Retrieve', 'attr' => 'onclick="showPlexMachineForm(\'#homepage-Plex-form [name=plexID]\')"']),
 				],

+ 1 - 1
api/homepage/tautulli.php

@@ -28,7 +28,7 @@ trait TautulliHomepageItem
 				],
 				'Connection' => [
 					$this->settingsOption('multiple-url', 'tautulliURL'),
-					$this->settingsOption('api-key', 'tautulliApikey'),
+					$this->settingsOption('multiple-api-key', 'tautulliApikey'),
 					$this->settingsOption('disable-cert-check', 'tautulliDisableCertCheck'),
 					$this->settingsOption('use-custom-certificate', 'tautulliUseCustomCertificate'),
 				],

+ 1 - 1
api/plugins/invites/plugin.php

@@ -271,7 +271,7 @@ class Invites extends Organizr
 					'label' => 'Get Plex Token',
 					'icon' => 'fa fa-ticket',
 					'text' => 'Retrieve',
-					'attr' => 'onclick="showPlexTokenForm(\'#INVITES-settings-items [name=plexToken]\')"'
+					'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, null, \'#INVITES-settings-items [name=plexToken]\')"'
 				),
 				array(
 					'type' => 'password-alt',

+ 18 - 2
api/v2/routes/config.php

@@ -5,7 +5,7 @@
  *     description="Organizr Configuration Items"
  * )
  */
-$app->get('/config[/{item}]', function ($request, $response, $args) {
+$app->get('/config[/{item}[/{term}]]', function ($request, $response, $args) {
 	/**
 	 * @OA\Get(
 	 *     tags={"config"},
@@ -35,10 +35,26 @@ $app->get('/config[/{item}]', function ($request, $response, $args) {
 	 *     security={{ "api_key":{} }}
 	 * )
 	 */
+	/**
+	 * @OA\Get(
+	 *     tags={"config"},
+	 *     path="/api/v2/config/search/{term}",
+	 *     summary="Search Organizr Coniguration Items",
+	 *     @OA\Parameter(name="term",description="The term of the items you want to grab",@OA\Schema(type="string"),in="path",required=true,example="version"),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/success-message"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->qualifyRequest(1, true)) {
 		if (isset($args['item'])) {
-			$Organizr->getConfigItem($args['item']);
+			$search = ($args['term']) ?? null;
+			$Organizr->getConfigItem($args['item'], $search);
 		} else {
 			$GLOBALS['api']['response']['data'] = $Organizr->getConfigItems();
 		}

+ 9 - 9
api/v2/routes/homepage.php

@@ -366,15 +366,6 @@ $app->get('/homepage/overseerr/requests[/{type}[/{limit}[/{offset}]]]', function
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
-$app->post('/homepage/overseerr/requests/{type}/{id}[/{seasons}]', function ($request, $response, $args) {
-	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	$args['seasons'] = $args['seasons'] ?? null;
-	$Organizr->addOverseerrRequest($args['id'], $args['type'], $args['seasons']);
-	$response->getBody()->write(jsonE($GLOBALS['api']));
-	return $response
-		->withHeader('Content-Type', 'application/json;charset=UTF-8')
-		->withStatus($GLOBALS['responseCode']);
-});
 $app->post('/homepage/overseerr/requests/{type}/{id}/available', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'available');
@@ -423,6 +414,15 @@ $app->delete('/homepage/overseerr/requests/{type}/{id}', function ($request, $re
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->post('/homepage/overseerr/requests/{type}/{id}[/{seasons}]', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$args['seasons'] = $args['seasons'] ?? null;
+	$Organizr->addOverseerrRequest($args['id'], $args['type'], $args['seasons']);
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->get('/homepage/ombi/requests[/{type}[/{limit}[/{offset}]]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$args['type'] = $args['type'] ?? 'both';

+ 40 - 0
docs/api.json

@@ -1188,6 +1188,46 @@
                 ]
             }
         },
+        "/api/v2/config/search/{term}": {
+            "get": {
+                "tags": [
+                    "config"
+                ],
+                "summary": "Search Organizr Coniguration Items",
+                "parameters": [
+                    {
+                        "name": "term",
+                        "in": "path",
+                        "description": "The term of the items you want to grab",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        },
+                        "example": "version"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/success-message"
+                                }
+                            }
+                        }
+                    },
+                    "401": {
+                        "description": "Unauthorized"
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            }
+        },
         "/api/v2/test/ldap": {
             "post": {
                 "tags": [

+ 53 - 12
js/functions.js

@@ -5776,10 +5776,16 @@ function overseerrActions(id, action, type = null, extra = null){
 	organizrAPI2(method,apiUrl,data).success(function(data) {
 		try {
 			let response = data.response;
+			if(action == 'add'){
+				addTempRequest();
+				setTimeout(function(){
+						ajaxloader();
+					}, 2000
+				);
+			}
 			messageSingle(response.message,'',activeInfo.settings.notifications.position,"#FFF","success","5000");
 			homepageRequests('overseerr');
-			swal.close();
-			ajaxloader();
+			cleanCloseSwal();
 		}catch(e) {
 			organizrCatchError(e,data);
 		}
@@ -5824,6 +5830,9 @@ function ombiActions(id, action, type, extra = null){
 	organizrAPI2(method,apiUrl,data).success(function(data) {
         try {
             let response = data.response;
+	        if(action == 'add'){
+		        addTempRequest();
+	        }
 	        messageSingle(response.message,'',activeInfo.settings.notifications.position,"#FFF","success","5000");
 	        homepageRequests('ombi');
 	        ajaxloader();
@@ -5835,6 +5844,32 @@ function ombiActions(id, action, type, extra = null){
 		OrganizrApiError(xhr, 'Ombi Error');
 	});
 }
+
+function addTempRequest(){
+	let service = activeInfo.settings.homepage.requests.service;
+	let html = `
+	<div class="item lazyload recent-poster request-item request-adding  mouse" data-src="">
+		<div class="outside-request-div">
+			<div class="inside-over-request-div bg-danger"></div>
+			<div class="inside-request-div bg-info"></div>
+		</div>
+		<div class="hover-homepage-item"></div>
+		<span class="elip request-title-tv"><i class="fa fa-tv"></i></span>
+		<span class="elip recent-title">Adding Request</span>
+	</div>
+	`;
+	$('.request-items-' + service).trigger('add.owl', [html, 0]).trigger('refresh.owl');
+	setTimeout(function(){
+		ajaxloader('.request-adding', 'in');
+		}, 100
+	);
+}
+function cleanCloseSwal(){
+	let state = swal.getState().isOpen;
+	if(state === true){
+		swal.close();
+	}
+}
 function doneTyping () {
 	let title = $('#request-input').val();
 	if(title == ''){
@@ -9290,7 +9325,7 @@ getPlexOAuthPin = function () {
     return deferred;
 };
 var polling = null;
-function PlexOAuth(success, error, pre) {
+function PlexOAuth(success, error, pre, id = null) {
     if (typeof pre === "function") {
         pre()
     }
@@ -9325,7 +9360,7 @@ function PlexOAuth(success, error, pre) {
                     if (data.authToken){
                         closePlexOAuthWindow();
                         if (typeof success === "function") {
-                            success('plex',data.authToken)
+                            success('plex',data.authToken, id)
                         }
                     }
                 },
@@ -9364,15 +9399,21 @@ function encodeData(data) {
         return [key, data[key]].map(encodeURIComponent).join("=");
     }).join("&");
 }
-function oAuthSuccess(type,token){
+function oAuthSuccess(type,token, id = null){
     switch(type) {
         case 'plex':
-            $('#oAuth-Input').val(token);
-            $('#oAuthType-Input').val(type);
-            $('#login-username-Input').addClass('hidden');
-            $('#login-password-Input').addClass('hidden');
-            $('#oAuth-div').removeClass('hidden');
-            $('.login-button').first().trigger('click');
+        	if(id){
+		        $(id).val(token);
+		        $(id).change();
+		        messageSingle('',window.lang.translate('Grabbed Token - Please Save'),activeInfo.settings.notifications.position,'#FFF','success','5000');
+	        }else{
+		        $('#oAuth-Input').val(token);
+		        $('#oAuthType-Input').val(type);
+		        $('#login-username-Input').addClass('hidden');
+		        $('#login-password-Input').addClass('hidden');
+		        $('#oAuth-div').removeClass('hidden');
+		        $('.login-button').first().trigger('click');
+	        }
             break;
         default:
             break;
@@ -11185,4 +11226,4 @@ function launch(){
 			orgErrorAlert('<h3>Webserver Error:</h3>' + xhr.responseText);
 		}
 	});
-}
+}

BIN
plugins/images/tabs/overseerr.png