Improved Adding backend via webUI workflow to be more clear on errors.

This commit is contained in:
Abdulmhsen B. A. A.
2024-06-23 13:58:28 +03:00
parent c357c3df5a
commit a487a593bf
6 changed files with 323 additions and 284 deletions

View File

@@ -1,5 +1,5 @@
<template>
<Message title="Important" message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle">
<Message title="Important" message_class="has-background-warning-80 has-text-dark" icon="fas fa-info-circle">
<ul>
<li>
WatchState is single user tool. It doesn't support syncing multiple users play state.
@@ -16,182 +16,164 @@
</li>
</ul>
</Message>
<form id="backend_add_form" @submit.prevent="addBackend" @change="changeStage">
<form id="backend_add_form" @submit.prevent="stage<4 ? changeStep() : addBackend()">
<div class="card">
<div class="card-header">
<p class="card-header-title is-justify-center">Add Backend</p>
</div>
<div class="card-content">
<div class="field">
<label class="label">Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.type" class="is-capitalized" required>
<option v-for="type in supported" :key="'type-'+type" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
<p class="help">
Select the type of backend you want to add. Supported backends are: <code>{{
supported.join(', ')
}}</code>.
</p>
</div>
<div class="field" v-if="error">
<Message title="Backend Error" id="backend_error" message_class="has-background-danger-80 has-text-dark"
icon="fas fa-exclamation-triangle" useClose @close="error=null">
<p>{{ error }}</p>
</Message>
</div>
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name" required>
<div class="icon is-left">
<i class="fas fa-id-badge"></i>
</div>
<p class="help">
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
case a-z, 0-9 and _</code> only.
</p>
</div>
</div>
<div class="field">
<label class="label">
<template v-if="'plex' !== backend.type">API Key</template>
<template v-else>X-Plex-Token</template>
</label>
<div class="field-body">
<div class="field">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<input class="input" v-model="backend.token" required
:type="false === exposeToken ? 'password' : 'text'">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
</div>
<div class="control">
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
v-tooltip="'Toggle token'">
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
</button>
</div>
<template v-if="stage>=0">
<div class="field">
<label class="label">Type</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.type" class="is-capitalized" required :disabled="stage > 0">
<option v-for="type in supported" :key="'type-'+type" :value="type">
{{ type }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
</div>
<p class="help">
<template v-if="'plex'===backend.type">
Enter the <code>X-Plex-Token</code>.
<NuxtLink target="_blank" to="https://support.plex.tv/articles/204059436"
v-text="'Visit This article for more information.'"/>
</template>
<template v-else>
Generate a new API Key from <code>Dashboard > Settings > API Keys</code>.
</template>
Select the type of backend you want to add. Supported backends are: <code>{{
supported.join(', ')
}}</code>.
</p>
</div>
</div>
</div>
<div class="field" v-if="'plex' === backend.type && stage >=1">
<label class="label">Plex Server URL</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" v-if="servers.length > 0">
<select v-model="backend.url" class="is-capital" @change="updateIdentifier" required>
<option v-for="server in servers" :key="'server-'+server.uuid" :value="server.uri">
{{ server.name }} - {{ server.uri }}
</option>
</select>
</div>
<input class="input" type="text" v-model="backend.url" v-else required>
<div class="icon is-left">
<i class="fas fa-link" v-if="!serversLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<p class="help">
<template v-if="servers.length<1">
Enter the URL of the backend. For example <code>http://localhost:32400</code>.&nbsp;
</template>
<NuxtLink @click="getServers" v-text="'Attempt to discover servers associated with the token.'"/>
Try to use non <code>.plex.direct</code> urls if possible, as they are often have problems working in
docker. If you use custom domain for your plex server and it's not showing in the list, you can add it
via Plex settings page. <code>Plex > Settings > Network > <strong>Custom server access
URLs:</strong></code>. For more information
<NuxtLink target="_blank"
to="https://support.plex.tv/articles/200430283-network/#Custom-server-access-URLs"
v-text="'Visit this link'"/>
.
</p>
</div>
</div>
<div class="field" v-if="'plex' !== backend.type">
<label class="label">URL</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.url" required>
<div class="icon is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
Enter the URL of the backend. For example <code>http://localhost:32400</code>.
</p>
</div>
</div>
<template v-if="stage >= 2">
<div class="field" v-if="'plex' !== backend.type">
<label class="label">Unique Identifier</label>
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.uuid" required>
<input class="input" type="text" v-model="backend.name" required :disabled="stage > 0">
<div class="icon is-left">
<i class="fas fa-server" v-if="!uuidLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
<i class="fas fa-id-badge"></i>
</div>
<p class="help">
The backend unique ID is a random string generated on server setup. It is used to identify the backend
uniquely. This is used for webhook matching and filtering.
<NuxtLink @click="getUUid" v-text="'Load automatically.'"/>
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
case a-z, 0-9 and _</code> only.
</p>
</div>
</div>
<div class="field">
<label class="label">
<template v-if="users.length>0">Associated User</template>
<template v-else>User ID</template>
<template v-if="'plex' !== backend.type">API Key</template>
<template v-else>X-Plex-Token</template>
</label>
<div class="field-body">
<div class="field">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<input class="input" v-model="backend.token" required :disabled="stage > 1"
:type="false === exposeToken ? 'password' : 'text'">
<div class="icon is-left">
<i class="fas fa-key"></i>
</div>
</div>
<div class="control">
<button type="button" class="button is-primary" @click="exposeToken = !exposeToken"
v-tooltip="'Toggle token'">
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"></i></span>
<span class="icon" v-else><i class="fas fa-eye-slash"></i></span>
</button>
</div>
</div>
<p class="help">
<template v-if="'plex'===backend.type">
Enter the <code>X-Plex-Token</code>.
<NuxtLink target="_blank" to="https://support.plex.tv/articles/204059436"
v-text="'Visit This article for more information.'"/>
</template>
<template v-else>
Generate a new API Key from <code>Dashboard > Settings > API Keys</code>.
</template>
</p>
</div>
</div>
</div>
</template>
<template v-if="stage>=1">
<div class="field" v-if="'plex' !== backend.type">
<label class="label">URL</label>
<div class="control has-icons-left">
<div class="select is-fullwidth" v-if="users.length>0">
<select v-model="backend.user" class="is-capitalized">
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<input class="input" type="text" v-model="backend.user" v-else>
<div class="icon is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<input class="input" type="text" v-model="backend.url" required :disabled="stage > 1">
<div class="icon is-left"><i class="fas fa-link"></i></div>
<p class="help">
<span v-if="'plex' === backend.type">
Plex doesn't use standard API practice for identifying users. They use <code>X-Plex-Token</code> to
identify the user. The user selected here will only be used for webhook matching and filtering.
</span>
<span v-else>
Which <code>{{ ucFirst(backend.type) }}</code> user should this backend use? The User ID will
determine the
data we get from the backend. And for webhook matching and filtering.
</span>
This tool is meant for single user use.
<NuxtLink @click="getUsers" v-text="'Retrieve User ids from backend.'"/>
Enter the URL of the backend. For example <code>http://192.168.8.200:8096</code>.
</p>
</div>
</div>
<div class="field" v-else>
<label class="label">Plex Server URL</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.url" class="is-capital" @change="stage=1; backend.uuid = ''" required
:disabled="stage > 1">
<option value="" disabled>Select Server URL</option>
<option v-for="server in servers" :key="'server-'+server.uuid" :value="server.uri">
{{ server.name }} - {{ server.uri }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-link" v-if="!serversLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<p class="help">
<NuxtLink @click="getServers" v-text="'Attempt to discover servers associated with the token.'"
v-if="stage<2"/>
Try to use non <code>.plex.direct</code> urls if possible, as they are often have problems working in
docker. If you use custom domain for your plex server and it's not showing in the list, you can add it
via Plex settings page. <code>Plex > Settings > Network > <strong>Custom server access
URLs:</strong></code>. For more information
<NuxtLink target="_blank"
to="https://support.plex.tv/articles/200430283-network/#Custom-server-access-URLs"
v-text="'Visit this link'"/>
.
</p>
</div>
</div>
</template>
<div class="field" v-if="stage >= 2">
<label class="label">
Associated User
</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.user" class="is-capitalized" :disabled="stage > 2">
<option value="" disabled>Select User</option>
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<div class="icon is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<p class="help">
Which user we should associate this backend with?
<NuxtLink @click="getUsers" v-text="'Retrieve User ids from backend.'" v-if="stage < 3"/>
</p>
</div>
</div>
<template v-if="stage >= 3">
<div class="field" v-if="backend.import">
<label class="label" for="backend_import">Import data from this backend</label>
<div class="control">
@@ -255,7 +237,15 @@
</div>
<div class="card-footer">
<div class="card-footer-item">
<div class="card-footer-item" v-if="stage < 4">
<button class="button is-fullwidth is-primary" type="submit" @click="changeStep()">
<span class="icon">
<i class="fas fa-arrow-right"></i>
</span>
<span>Next Step</span>
</button>
</div>
<div class="card-footer-item" v-else>
<button class="button is-fullwidth is-primary" type="submit">
<span class="icon"><i class="fas fa-plus"></i></span>
<span>Add Backend</span>
@@ -268,7 +258,8 @@
<script setup>
import 'assets/css/bulma-switch.css'
import {notification, ucFirst} from '~/utils/index.js'
import request from '~/utils/request'
import {awaitElement, notification} from '~/utils/index'
const emit = defineEmits(['addBackend'])
@@ -309,6 +300,7 @@ const usersLoading = ref(false)
const uuidLoading = ref(false)
const serversLoading = ref(false)
const exposeToken = ref(false)
const error = ref()
const getUUid = async () => {
const required_values = ['type', 'token', 'url'];
@@ -318,25 +310,32 @@ const getUUid = async () => {
return
}
uuidLoading.value = true
try {
error.value = null
uuidLoading.value = true
const response = await request(`/backends/uuid/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify({
token: backend.value.token,
url: backend.value.url
const response = await request(`/backends/uuid/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify({
name: backend.value?.name,
token: backend.value.token,
url: backend.value.url
})
})
})
const json = await response.json()
uuidLoading.value = false
const json = await response.json()
if (!response.ok) {
notification('error', 'Error', 'Failed to get the UUID from the backend.')
return
if (200 !== response.status) {
n_proxy('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
}
backend.value.uuid = json.identifier
} catch (e) {
n_proxy('error', 'Error', `Request error. ${e.message}`, e)
} finally {
uuidLoading.value = false
}
backend.value.uuid = json.identifier
}
const getUsers = async (showAlert = true) => {
@@ -344,93 +343,127 @@ const getUsers = async (showAlert = true) => {
if (required_values.some(v => !backend.value[v])) {
if (showAlert) {
required_values.forEach(v => {
if (!backend.value[v]) {
notification('error', 'Error', `Please fill the required field: ${v}.`)
}
})
notification('error', 'Error', `Please fill all the required fields. ${required_values.join(', ')}.`)
}
return
}
usersLoading.value = true
try {
error.value = null
usersLoading.value = true
let data = {
token: backend.value.token,
url: backend.value.url,
uuid: backend.value.uuid,
};
let data = {
name: backend.value?.name,
token: backend.value.token,
url: backend.value.url,
uuid: backend.value.uuid,
};
if (backend.value.options && backend.value.options.ADMIN_TOKEN) {
data.options = {
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
if (backend.value.options && backend.value.options.ADMIN_TOKEN) {
data.options = {
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
}
}
const response = await request(`/backends/users/${backend.value.type}?tokens=1`, {
method: 'POST',
body: JSON.stringify(data)
})
const json = await response.json()
if (200 !== response.status) {
n_proxy('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
}
users.value = json
} catch (e) {
n_proxy('error', 'Error', `Request error. ${e.message}`, e)
} finally {
usersLoading.value = false
}
const response = await request(`/backends/users/${backend.value.type}?tokens=1`, {
method: 'POST',
body: JSON.stringify(data)
})
const json = await response.json()
usersLoading.value = false
if (200 !== response.status) {
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
}
users.value = json
backend.value.user = users.value[0].id
}
watch(stage, async (value) => {
if (value >= 1 && 'plex' === backend.value.type && servers.value.length < 1) {
await getServers()
}
if (value >= 2) {
if ('plex' !== backend.value.type && !backend.value.uuid) {
await getUUid();
}
if (users.value.length < 1) {
await getUsers()
}
}
})
onMounted(async () => {
const response = await request('/system/supported')
supported.value = await response.json()
backend.value.type = supported.value[0]
})
const changeStage = async () => {
const required = ['name', 'type', 'token']
const changeStep = async () => {
console.log('was called');
stage.value = 0
if ('plex' !== backend.value.type) {
required.push('url')
}
if (stage.value >= 0) {
if (required.some(v => !backend.value[v])) {
stage.value = 0
return
}
// -- basic validation.
const required = ['name', 'type', 'token']
if (required.some(v => !backend.value[v])) {
required.forEach(v => {
if (!backend.value[v]) {
notification('error', 'Error', `Please fill the required field: ${v}.`)
}
})
return
}
if (props.backends.find(b => b.name === backend.value.name)) {
notification('error', 'Error', `Backend with name ${backend.value.name} already exists.`)
stage.value = 0
return
}
if (props.backends.find(b => b.name === backend.value.name)) {
notification('error', 'Error', `Backend with name '${backend.value.name}' already exists.`)
return
}
if ('plex' === backend.value.type && !backend.value.uuid) {
stage.value = 1
return
console.log('stage 1')
}
stage.value = 2
if (stage.value >= 1) {
if ('plex' === backend.value.type && servers.value.length < 1) {
await getServers()
if (servers.value.length < 1) {
stage.value = 0
return
}
}
if (!backend.value.url) {
return
}
if (!backend.value.uuid) {
await getUUid();
if (!backend.value.uuid) {
stage.value = 1
return
}
}
stage.value = 2
console.log('stage 2')
}
if (stage.value >= 2) {
if (users.value.length < 1) {
await getUsers()
if (users.value.length < 1) {
stage.value = 1
return
}
}
if (!backend.value.user) {
return
}
stage.value = 3
console.log('stage 3')
}
if (stage.value >= 3) {
stage.value = 4
console.log('stage 4')
}
}
const addBackend = async () => {
@@ -481,36 +514,55 @@ const getServers = async () => {
notification('error', 'Error', `Token is required to get list of servers.`)
return
}
try {
serversLoading.value = true
serversLoading.value = true
let data = {
name: backend.value?.name,
token: backend.value.token,
url: window.location.origin,
};
let data = {
token: backend.value.token,
url: window.location.origin,
};
const response = await request(`/backends/discover/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify(data)
})
const response = await request(`/backends/discover/${backend.value.type}`, {
method: 'POST',
body: JSON.stringify(data)
})
serversLoading.value = false
serversLoading.value = false
const json = await response.json()
const json = await response.json()
if (200 !== response.status) {
n_proxy('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
}
if (200 !== response.status) {
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
servers.value = json
} catch (e) {
n_proxy('error', 'Error', `Request error. ${e.message}`, e)
} finally {
serversLoading.value = false
}
servers.value = json
backend.value.url = servers.value[0].uri
backend.value.uuid = servers.value[0].identifier
await changeStage()
}
const updateIdentifier = async () => {
backend.value.uuid = servers.value.find(s => s.uri === backend.value.url).identifier
await getUsers()
if (backend.value.uuid) {
await getUsers()
}
}
const n_proxy = (type, title, message, e = null) => {
if ('error' === type) {
error.value = message
}
if (e) {
console.error(e)
}
return notification(type, title, message)
}
watch(error, v => v ? awaitElement('#backend_error', (_, e) => e.scrollIntoView({behavior: 'smooth'})) : null)
</script>

View File

@@ -128,37 +128,21 @@
</footer>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
<strong>Import</strong> means pulling data from the backends into the local database.
</li>
<li>
<strong>Export</strong> means pushing data from the local database to the backends.
</li>
<li>You can delete a backend by visiting the backend page and clicking the delete button.</li>
</ul>
</Message>
</div>
</template>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
<strong>Import</strong> means pulling data from the backends into the local database.
</li>
<li>
<strong>Export</strong> means pushing data from the local database to the backends.
</li>
<li>
WatchState is single user tool. It doesn't support syncing multiple users play state.
<NuxtLink target="_blank" v-text="'Visit this link'"
to="https://github.com/arabcoders/watchstate/blob/master/FAQ.md#is-there-support-for-multi-user-setup"/>
to learn more.
</li>
<li>
If you are adding new backend that is fresh and doesn't have your correct watch state, you should
turn off import and enable only metadata import at the start to prevent overriding your current play
state.
<NuxtLink
to="https://github.com/arabcoders/watchstate/blob/master/FAQ.md#my-new-backend-overriding-my-old-backend-state--my-watch-state-is-not-correct"
target="_blank" v-text="'Visit this link'"/>
to learn more.
</li>
<li>You can delete a backend by visiting the backend page and clicking the delete button.</li>
</ul>
</Message>
</div>
</div>
</template>

View File

@@ -26,8 +26,11 @@ final class UUid
try {
$client = $this->getBasicClient($type, DataUtil::fromRequest($request, true));
$info = $client->getInfo();
return api_response(HTTP_STATUS::HTTP_OK, [
'identifier' => $client->getIdentifier(true)
'type' => strtolower((string)ag($info, 'type')),
'identifier' => ag($info, 'identifier'),
]);
} catch (InvalidArgumentException $e) {
return api_error($e->getMessage(), HTTP_STATUS::HTTP_BAD_REQUEST);

View File

@@ -36,7 +36,7 @@ trait CommonTrait
return new Response(
status: false,
error: new Error(
message: 'Exception [{error.kind}] was thrown unhandled in [{client}: {backend}] {action}. Error [{error.message} @ {error.file}:{error.line}].',
message: "Exception '{error.kind}' was thrown unhandled in '{client}: {backend}' {action}. '{error.message}' at '{error.file}:{error.line}'.",
context: [
'action' => $action ?? 'not_set',
'backend' => $context->backendName,

View File

@@ -65,7 +65,7 @@ class GetInfo
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] {action} returned with unexpected [{status_code}] status code.',
message: "Request for '{backend}' {action} returned with unexpected '{status_code}' status code.",
context: [
'action' => $this->action,
'client' => $context->clientName,
@@ -83,7 +83,7 @@ class GetInfo
return new Response(
status: false,
error: new Error(
message: 'Request for [{backend}] {action} returned with empty response. Please make sure the container can communicate with the backend.',
message: "Request for '{backend}' {action} returned with empty response. Please make sure the container can communicate with the backend.",
context: [
'action' => $this->action,
'client' => $context->clientName,
@@ -103,7 +103,7 @@ class GetInfo
);
if (true === $context->trace) {
$this->logger->debug('Processing [{client}: {backend}] {action} payload.', [
$this->logger->debug("Processing '{client}: {backend}' {action} payload.", [
'action' => $this->action,
'client' => $context->clientName,
'backend' => $context->backendName,

View File

@@ -124,7 +124,7 @@ trait APITraits
return $instance->withContext(
new Context(
clientName: $type,
backendName: 'basic_' . $type,
backendName: $data->get('name', 'basic_' . $type),
backendUrl: new Uri($data->get('url')),
cache: Container::get(BackendCache::class),
backendId: $data->get('uuid'),
@@ -214,7 +214,7 @@ trait APITraits
$item['not_reported_by'] = array_values(
array_filter($this->_backendsNames, fn($key) => false === in_array($key, ag($item, 'reported_by', [])))
);
$item['isTainted'] = $entity->isTainted();
if (true === $includeContext) {