Merge pull request #675 from arabcoders/dev

Fix assertion failure in production.
This commit is contained in:
Abdulmohsen
2025-05-19 02:12:53 +03:00
committed by GitHub
10 changed files with 403 additions and 331 deletions

View File

@@ -34,16 +34,16 @@ RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezo
# Cache fonts.
fc-cache -f && fc-list | sort
# Copy frankenphp (caddy+php) to the container.
#
COPY --chown=app:app --from=ghcr.io/arabcoders/franken_builder:latest /usr/local/bin/frankenphp /opt/bin/
# Copy source code to container.
COPY ./ /opt/app
# Copy frontend to public directory.
COPY --chown=app:app --from=npm_builder /frontend/exported/ /opt/app/public/exported/
# Copy frankenphp (caddy+php) to the container.
#
COPY --chown=app:app --from=ghcr.io/arabcoders/franken_builder:latest /usr/local/bin/frankenphp /opt/bin/
# install composer & packages.
#
RUN echo '' && \

View File

@@ -234,9 +234,7 @@ return (function () {
'opcache.max_wasted_percentage' => 5,
'expose_php' => 0,
'date.timezone' => ag($config, 'tz', 'UTC'),
// 'mbstring.http_input' => ag($config, 'charset', 'UTF-8'),
// 'mbstring.http_output' => ag($config, 'charset', 'UTF-8'),
// 'mbstring.internal_encoding' => ag($config, 'charset', 'UTF-8'),
'zend.assertions' => -1
],
'fpm' => [
'global' => [

View File

@@ -177,7 +177,13 @@ return [
'key' => 'options.plex_external_user',
'type' => 'bool',
'visible' => false,
'description' => 'Mark the plex user as external user.',
'description' => 'Mark the plex user as home user.',
],
[
'key' => 'options.plex_guest_user',
'type' => 'bool',
'visible' => true,
'description' => 'Mark the plex user as invited guest.',
],
[
'key' => 'options.ADMIN_PLEX_USER_PIN',

View File

@@ -35,7 +35,6 @@
</Message>
</div>
<template v-if="stage >= 0">
<div class="field">
<label class="label">Local User</label>
<div class="control has-icons-left">
@@ -45,7 +44,7 @@
</select>
</div>
<div class="icon is-left">
<i class="fas fa-users"></i>
<i class="fas fa-users"/>
</div>
</div>
<p class="help">
@@ -65,7 +64,7 @@
</select>
</div>
<div class="icon is-left">
<i class="fas fa-server"></i>
<i class="fas fa-server"/>
</div>
</div>
<p class="help">The backend type.</p>
@@ -76,13 +75,13 @@
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.name" required :disabled="stage > 0">
<div class="icon is-left">
<i class="fas fa-id-badge"></i>
<i class="fas fa-id-badge"/>
</div>
<p class="help">
Choose a unique name for this backend. <b class="has-text-danger">You CANNOT change it later</b>.
Backend name must be in <code>lower case a-z, 0-9 and _</code> and cannot start with number.
</p>
</div>
<p class="help">
Choose a unique name for this backend. <strong>You CANNOT change it later</strong>.
Backend name must be in <strong>lower case a-z, 0-9 and _</strong> and cannot start with number.
</p>
</div>
<div class="field">
@@ -97,30 +96,29 @@
<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>
<i class="fas fa-key"/>
</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>
<span class="icon" v-if="!exposeToken"><i class="fas fa-eye"/></span>
<span class="icon" v-else><i class="fas fa-eye-slash"/></span>
</button>
</div>
</div>
<p class="help">
<template v-if="'plex' === backend.type">
Enter the <code>X-Plex-Token</code>.
Enter the <strong>X-Plex-Token</strong>.
<NuxtLink target="_blank" to="https://support.plex.tv/articles/204059436"
v-text="'Visit This link'"/>
to learn how to get the token. <span
class="is-bold has-text-danger">If you plan to add sub-users, YOU MUST use admin level
token.</span>
to learn how to get the token. <span class="is-bold">If you plan to add sub-users, YOU MUST use
admin level token.</span>
</template>
<template v-else>
Generate a new API Key from <code>Dashboard > Settings > API Keys</code>.<br>
<span class="icon has-text-warning"><i class="fas fa-info-circle"></i></span>
You can use <code>username:password</code> as API key and we will automatically generate limited
Generate a new API Key from <strong>Dashboard > Settings > API Keys</strong>.<br>
<span class="icon has-text-warning"><i class="fas fa-info-circle"/></span>
You can use <strong>username:password</strong> as API key and we will automatically generate limited
token if you are unable to generate API Key. This should be used as last resort. and it's mostly
untested. and things might not work as expected.
<span class="is-bold has-text-danger">If you plan to add sub-users, YOU MUST use API KEY and not
@@ -169,17 +167,17 @@
</div>
</div>
<template v-if="'plex' === backend.type">
<div class="field" v-if="'plex' === backend.type">
<label class="label">User PIN</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.options.PLEX_USER_PIN" :disabled="stage > 1">
<div class="icon is-left"><i class="fas fa-key"></i></div>
<p class="help">
If the user you are going to select has <code>PIN</code> enabled, you need to enter the pin here.
Otherwise it will fail to authenticate.
</p>
<div class="icon is-left"><i class="fas fa-key"/></div>
</div>
</template>
<p class="help">
If the user you are going to select has <strong>PIN</strong> enabled, you need to enter the pin here.
Otherwise it will fail to authenticate.
</p>
</div>
</template>
<template v-if="stage >= 1">
@@ -187,105 +185,146 @@
<label class="label">URL</label>
<div class="control has-icons-left">
<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">
Enter the URL of the backend. For example <code>http://192.168.8.200:8096</code>.
</p>
<div class="icon is-left"><i class="fas fa-link"/></div>
</div>
<p class="help">
Enter the URL of the backend. For example <strong>http://192.168.8.200:8096</strong>.
</p>
</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; updateIdentifier()" 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>
<template v-else>
<div class="field">
<label class="label">Plex Server URL</label>
<div class="field-body">
<div class="field">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.url" class="is-capital" @change="stage = 1; updateIdentifier()"
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 class="fas fa-spinner fa-pulse" v-else/>
</div>
</div>
<div class="control">
<button class="button is-primary" type="button" :disabled="serversLoading || stage > 2"
@click="getServers">
<span class="icon"><i class="fa"
:class="{'fa-spinner fa-spin': serversLoading,'fa-refresh' : !serversLoading }"/></span>
<span class="is-hidden-mobile">Reload</span>
</button>
</div>
</div>
<p class="help">
Try to use non <strong>.plex.direct</strong> 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. <strong>Plex > Settings > Network > Custom server access
URLs:</strong>. 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="icon is-left">
<i class="fas fa-link" v-if="!serversLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
</div>
<div class="field">
<label class="label" for="backend_ownership">Are you invited guest to this backend?</label>
<div class="control">
<input id="backend_ownership" type="checkbox" class="switch is-success"
v-model="backend.options.plex_guest_user" :disabled="stage > 2">
<label for="backend_ownership" class="is-unselectable">
{{ backend.options?.plex_guest_user ? 'Yes' : 'No' }}
</label>
</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'"/>
.
This stops WatchState from attempting to generate access-tokens for different users.
</p>
</div>
</div>
</template>
</template>
<div class="field" v-if="stage >= 3">
<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 > 3">
<option value="" disabled>Select User</option>
<option v-for="user in users" :key="'uid-' + user.id" :value="user.id">
{{ user.name }}
</option>
</select>
<label class="label">User</label>
<div class="field-body">
<div class="field">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
<select v-model="backend.user" class="is-capitalized" :disabled="stage > 3">
<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 class="fas fa-spinner fa-pulse" v-else/>
</div>
</div>
<div class="control">
<button class="button is-primary" type="button" :disabled="usersLoading || stage > 3"
@click="getUsers">
<span class="icon"><i class="fa"
:class="{'fa-spinner fa-spin': usersLoading,'fa-refresh' : !usersLoading }"/></span>
<span class="is-hidden-mobile">Reload</span>
</button>
</div>
</div>
<p class="help">Which user we should associate this backend with?</p>
</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 < 4"/>
</p>
</div>
</div>
<template v-if="stage >= 4">
<div class="field" v-if="backend.import">
<label class="label" for="backend_import">Import data from this backend?</label>
<label class="label" for="backend_import">Import play and progress updates from this backend?</label>
<div class="control">
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled">
<label for="backend_import" class="is-unselectable">{{ backend.import.enabled ? 'Yes' : 'No' }}</label>
<p class="help is-bold">
Import means to get the data from the backend and store it in WatchState.
</p>
</div>
<p class="help is-bold has-text-danger">
<span class="icon"><i class="fas fa-info-circle"/></span>
Get play state and progress from this backend.
</p>
</div>
<div class="field" v-if="backend.import && !backend.import.enabled">
<label class="label" for="backend_import_metadata">Import metadata only from from this backend?</label>
<label class="label" for="backend_import_metadata">Import metadata from this backend?</label>
<div class="control">
<input id="backend_import_metadata" type="checkbox" class="switch is-success"
v-model="backend.options.IMPORT_METADATA_ONLY">
<label for="backend_import_metadata" class="is-unselectable">
{{ backend.options?.IMPORT_METADATA_ONLY ? 'Yes' : 'No' }}
</label>
<p class="help has-text-danger">
For best performance, we need at least to import metadata from the backend. This will not alter your
play state.
</p>
</div>
<p class="help has-text-danger is-bold">
<span class="icon"><i class="fas fa-info-circle"/></span>
As you have disabled the state import, you should enable this option for efficient and fast updates
to this backend.
</p>
</div>
<div class="field" v-if="backend.export">
<label class="label" for="backend_export">Export data to this backend?</label>
<label class="label" for="backend_export">Send play and progress updates to this backend?</label>
<div class="control">
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
<label for="backend_export" class="is-unselectable">{{ backend.export.enabled ? 'Yes' : 'No' }}</label>
<p class="help is-bold">
Export mean sending data from WatchState to this backend.
</p>
</div>
<p class="help is-bold has-text-danger">
<span class="icon"><i class="fas fa-info-circle"/></span>
The backend will not receive any data from WatchState if this is disabled.
</p>
</div>
<div class="field" v-if="backend.webhook">
@@ -296,10 +335,10 @@
<label for="webhook_match_user" class="is-unselectable">
{{ backend.webhook.match.user ? 'Yes' : 'No' }}
</label>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
</div>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
</div>
<div class="field" v-if="backend.webhook">
@@ -310,56 +349,58 @@
<label for="webhook_match_uuid" class="is-unselectable">
{{ backend.webhook.match.uuid ? 'Yes' : 'No' }}
</label>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
</div>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
</div>
<hr>
<div class="field">
<h1 class="title is-4">One Time Operations</h1>
</div>
<div class="field">
<hr>
<label class="label has-text-danger" for="backup_data">
Create backup for this backend data?
</label>
<div class="control">
<input id="backup_data" type="checkbox" class="switch is-success" v-model="backup_data">
<label for="backup_data" class="is-unselectable">{{ backup_data ? 'Yes' : 'No' }}</label>
<p class="help">
This will run a one time backup for the backend data.
</p>
</div>
<p class="help">
This will run a one time backup for the backend data.
</p>
</div>
<div class="field" v-if="backends.length < 1">
<hr>
<label class="label" for="force_import">
Force one time import from this backend?
</label>
<div class="control">
<input id="force_import" type="checkbox" class="switch is-success" v-model="force_import">
<label for="force_import" class="is-unselectable">{{ force_import ? 'Yes' : 'No' }}</label>
<p class="help">
<span class="icon"><i class="fas fa-info-circle"></i></span>
Run a one time import from this backend after adding it.
</p>
</div>
<p class="help">
<span class="icon"><i class="fas fa-info-circle"/></span>
Run a one time import from this backend after adding it.
</p>
</div>
<div class="field" v-if="backends.length > 0">
<hr>
<label class="label has-text-danger" for="force_export">
Force Export local data to this backend?
</label>
<div class="control">
<input id="force_export" type="checkbox" class="switch is-success" v-model="force_export">
<label for="force_export" class="is-unselectable">{{ force_export ? 'Yes' : 'No' }}</label>
<p class="help has-text-danger">
<span class="icon"><i class="fas fa-info-circle"></i></span>
THIS OPTION WILL OVERRIDE THE BACKEND DATA with locally stored data.
</p>
</div>
<p class="help has-text-danger is-bold">
<span class="icon"><i class="fas fa-info-circle"/></span>
THIS OPTION WILL OVERRIDE THE BACKEND DATA with locally stored data.
</p>
</div>
</template>
</div>
@@ -367,20 +408,20 @@
<div class="card-footer-item" v-if="stage >= 1">
<button class="button is-fullwidth is-warning" type="button" @click="stage = stage - 1">
<span class="icon"><i class="fas fa-arrow-left"></i></span>
<span class="icon"><i class="fas fa-arrow-left"/></span>
<span>Previous Step</span>
</button>
</div>
<div class="card-footer-item" v-if="stage < maxStages">
<button class="button is-fullwidth is-info" type="button" @click="changeStep()">
<span class="icon"><i class="fas fa-arrow-right"></i></span>
<span class="icon"><i class="fas fa-arrow-right"/></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 class="icon"><i class="fas fa-plus"/></span>
<span>Add Backend</span>
</button>
</div>
@@ -685,19 +726,14 @@ const getUsers = async (showAlert = true) => {
token: backend.value.token,
url: backend.value.url,
uuid: backend.value.uuid,
options: {},
};
if (backend.value.options && backend.value.options.ADMIN_TOKEN) {
data.options = {
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
['ADMIN_TOKEN', 'plex_guest_user', 'PLEX_USER_PIN', 'is_limited_token'].forEach(v => {
if (backend.value.options && backend.value.options[v]) {
data.options[v] = backend.value.options[v]
}
}
if (backend.value.options && backend.value.options.PLEX_USER_PIN) {
data.options = {
PLEX_USER_PIN: backend.value.options.PLEX_USER_PIN
}
}
})
const response = await request(`/backends/users/${backend.value.type}?tokens=1`, {
method: 'POST',
@@ -873,7 +909,7 @@ const addBackend = async () => {
}
const getServers = async () => {
if ('plex' !== backend.value.type || servers.value.length > 0) {
if ('plex' !== backend.value.type) {
return
}
@@ -881,6 +917,7 @@ const getServers = async () => {
notification('error', 'Error', `Token is required to get list of servers.`)
return
}
try {
serversLoading.value = true
@@ -916,9 +953,9 @@ const getServers = async () => {
const updateIdentifier = async () => {
backend.value.uuid = servers.value.find(s => s.uri === backend.value.url).identifier
if (backend.value.uuid) {
await getUsers()
}
// if (backend.value.uuid) {
// await getUsers()
// }
}
const n_proxy = (type, title, message, e = null) => {

View File

@@ -3,10 +3,10 @@
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<NuxtLink to="/backends" v-text="'Backends'" />
<span class="icon"><i class="fas fa-server"/>&nbsp;</span>
<NuxtLink to="/backends" v-text="'Backends'"/>
-
<NuxtLink :to="'/backend/' + id" v-text="id" />
<NuxtLink :to="'/backend/' + id" v-text="id"/>
: Edit
</span>
@@ -21,13 +21,11 @@
<div class="column is-12" v-if="isLimitedToken">
<Message title="For your information" message_class="has-background-warning-90 has-text-dark"
icon="fas fa-info-circle">
icon="fas fa-info-circle">
<p>
This backend is using accesstoken instead of API keys, And this method untested and may not work as
expected.
Please make sure you know what you are doing. Simple operations like <code>Import</code>,
<code>Export</code>
should work fine.
expected. Please make sure you know what you are doing. Simple operations like <strong>Import</strong>,
<strong>Export</strong> should work fine.
</p>
<p>
How the access token interact with the rest of the API is undefined and untested by us. Please use with
@@ -38,7 +36,7 @@
<div class="column is-12" v-if="isLoading">
<Message message_class="is-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading backend settings. Please wait..." />
message="Loading backend settings. Please wait..."/>
</div>
<div v-else class="column is-12">
@@ -53,30 +51,19 @@
<div class="field">
<label class="label">Local User</label>
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select class="is-capitalized" disabled>
<option v-text="api_user" />
</select>
</div>
<div class="icon is-left">
<i class="fas fa-users"></i>
</div>
<p class="help">The local user which this backend is associated with.</p>
<input type="text" class="input is-capitalized" :value="api_user" required readonly disabled>
<div class="icon is-left"><i class="fas fa-user"/></div>
</div>
<p class="help is-unselectable">The local user which this backend is associated with.</p>
</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 readonly disabled>
<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 class="icon is-left"><i class="fas fa-id-badge"/></div>
</div>
<p class="help is-unselectable">The backend name in WatchState.</p>
</div>
<div class="field">
@@ -84,43 +71,46 @@
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.type" readonly disabled>
<div class="icon is-left">
<i class="fas fa-server"></i>
<i class="fas fa-server"/>
</div>
<p class="help">
The backend server type. The supported types are <code>{{
supported.map(v => ucFirst(v)).join(', ')
}}</code>.
</p>
</div>
<p class="help is-unselectable">Backend Type.</p>
</div>
<div class="field">
<label class="label">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 value="" disabled>Select Server</option>
<option v-for="server in servers" :key="server.uuid" :value="server.uri">
{{ server.name }} - {{ server.uri }}
</option>
</select>
<div class="field-body">
<div class="field">
<div class="field has-addons">
<div class="control is-expanded 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 value="" disabled>Select Server</option>
<option v-for="server in servers" :key="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"/></div>
</div>
<div class="control" v-if="servers.length > 0">
<button class="button is-primary" type="button" :disabled="serversLoading" @click="getServers">
<span class="icon"><i class="fa"
:class="{'fa-spinner fa-spin': serversLoading,'fa-refresh' : !serversLoading }"/></span>
<span class="is-hidden-mobile">Reload</span>
</button>
</div>
</div>
</div>
<input class="input" type="text" v-model="backend.url" v-else required>
<div class="icon is-left">
<i class="fas fa-link"></i>
</div>
<p class="help">
<template v-if="servers.length < 1">
Enter the URL of the backend. For example
<code v-if="'plex' === backend.type">http://192.168.8.11:32400</code>
<code v-else>http://192.168.8.100:8096</code>
.
</template>
<template v-else>
Those are the servers associated with the Plex Token. Select the server you want to use.
</template>
</p>
</div>
<p class="help">
<template v-if="servers.length < 1">
Enter the URL of the backend. For example
<strong>http://192.168.8.100:{{ 'plex' === backend.type ? '32400' : '8096' }}</strong>.
</template>
<template v-else>Select the server you want to use.</template>
</p>
</div>
<div class="field">
@@ -133,28 +123,25 @@
<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>
:type="false === exposeToken ? 'password' : 'text'">
<div class="icon is-left"><i class="fas fa-key"/></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>
v-tooltip="'Toggle token'">
<span class="icon"><i class="fas" :class="exposeToken ? 'fa-eye-slash' : 'fa-eye'"/></span>
</button>
</div>
</div>
<p class="help">
<template v-if="'plex' === backend.type">
Enter the <code>X-Plex-Token</code>.
Enter the <strong>X-Plex-Token</strong>.
<NuxtLink target="_blank"
to="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"
v-text="'Visit This article for more information.'" />
to="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"
v-text="'Visit This article for more information.'"/>
</template>
<template v-else>
You can generate a new API key from <code>Dashboard > Settings > API Keys</code>.
You can generate a new API key from <strong>Dashboard > Settings > API Keys</strong>.
</template>
</p>
</div>
@@ -163,134 +150,166 @@
<div class="field">
<label class="label">Backend Unique ID</label>
<div class="control has-icons-left">
<input class="input" type="text" v-model="backend.uuid" required :disabled="isLimitedToken">
<div class="icon is-left">
<i class="fas fa-cloud" v-if="!uuidLoading"></i>
<i class="fas fa-spinner fa-pulse" v-else></i>
<div class="field-body">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<input type="text" class="input is-fullwidth" v-model="backend.uuid" required
:disabled="isLimitedToken">
<div class="icon is-left">
<i class="fas fa-cloud" v-if="!uuidLoading"/>
<i class="fas fa-spinner fa-pulse" v-else/>
</div>
</div>
<div class="control" v-if="!isLimitedToken">
<button class="button is-primary" type="button" :disabled="uuidLoading" @click="getUUid">
<span class="icon"><i class="fa"
:class="{'fa-spinner fa-spin': uuidLoading,'fa-refresh' : !uuidLoading }"/></span>
<span class="is-hidden-mobile">Reload</span>
</button>
</div>
</div>
<p class="help">
<span v-if="'plex' === backend.type">
The backend unique ID is random string generated on server setup, In Plex case it used to inquiry
about the users associated with the server to generate limited <code>X-Plex-Token</code> for them.
It
used by webhooks as a filter to match the backend. in-case you are member of multiple servers.
</span>
<span v-else>
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.
</span>
<NuxtLink @click="getUUid" v-if="!isLimitedToken" v-text="'Get from the backend.'" />
</p>
</div>
<p class="help">
<span v-if="'plex' === backend.type">
The backend unique ID is random string generated on server setup, In Plex case it used to inquiry
about the users associated with the server to generate limited <strong>X-Plex-Token</strong> for
them.
It used by webhooks as a filter to match the backend. in-case you are member of multiple servers.
</span>
<span v-else>
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.
</span>
</p>
</div>
<div class="field">
<label class="label">
<template v-if="users.length > 0">Associated User</template>
<template v-else>User ID</template>
</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" :disabled="isLimitedToken">
<option v-for="user in users" :key="'uid-' + user.id" :value="user.id">
{{ user.name }}
</option>
</select>
<label class="label">{{ users.length > 0 ? 'User' : 'User ID' }}</label>
<div class="field-body">
<div class="field has-addons">
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth" v-if="users.length > 0">
<select v-model="backend.user" class="is-capitalized" :disabled="isLimitedToken">
<option v-for="user in users" :key="'uid-' + user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<input class="input is-fullwidth" type="text" v-model="backend.user" v-else>
<div class="icon is-left">
<i class="fas fa-user-tie" v-if="!usersLoading"/>
<i class="fas fa-spinner fa-pulse" v-else/>
</div>
</div>
<div class="control" v-if="!isLimitedToken">
<button class="button is-primary" type="button" :disabled="usersLoading" @click="getUsers">
<span class="icon"><i class="fa"
:class="{'fa-spinner fa-spin': usersLoading,'fa-refresh' : !usersLoading }"/></span>
<span class="is-hidden-mobile">Reload</span>
</button>
</div>
</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>
<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 list can only be populated if the user is admin or has
<code>ADMIN_TOKEN</code> set in Additional options.
</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>
<a href="javascript:void(0)" @click="getUsers" v-if="!isLimitedToken">
Get users.
</a>
</p>
</div>
<p class="help">
<span v-if="'plex' === backend.type">
Plex doesn't use standard API practice for identifying users. They use <strong>X-Plex-Token</strong>
to identify the user. The list can only be populated if the user is admin or has
<strong>ADMIN_TOKEN</strong> set in additional options.
</span>
<span v-else>
Which user should this backend configuration use? The User will determine the data we get from
the backend. And for webhook matching and filtering.
</span>
</p>
</div>
<div class="field" v-if="backend.import">
<label class="label" for="backend_import">Import data from this backend?</label>
<label class="label" for="backend_import">Import play and progress updates from this backend?</label>
<div class="control">
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled">
<label for="backend_import">Enable</label>
<p class="help">
Import means to get the data from the backend and store it in the database.
</p>
<label for="backend_import" class="is-unselectable">
{{ backend.import.enabled ? 'Yes' : 'No' }}
</label>
</div>
<p class="help is-bold has-text-danger">
<span class="icon"><i class="fas fa-info-circle"/></span>
Get play state and progress from this backend.
</p>
</div>
<div class="field" v-if="backend.import && !backend.import.enabled">
<label class="label" for="backend_import_metadata">Import metadata only from this backend?</label>
<label class="label" for="backend_import_metadata">Import metadata from this backend?</label>
<div class="control">
<input id="backend_import_metadata" type="checkbox" class="switch is-success"
v-model="backend.options.IMPORT_METADATA_ONLY">
<label for="backend_import_metadata">Enable</label>
<p class="help has-text-danger">
To efficiently push changes to the backend we need relation map and this require
us to get metadata from the backend. You have Importing disabled, as such this option
allow us to import this backend metadata without altering your play state.
</p>
v-model="backend.options.IMPORT_METADATA_ONLY">
<label for="backend_import_metadata" class="is-unselectable">
{{ backend.options.IMPORT_METADATA_ONLY ? 'Yes' : 'No' }}
</label>
</div>
<p class="help has-text-danger is-bold">
<span class="icon"><i class="fas fa-info-circle"/></span>
As you have disabled the state import, you should enable this option for efficient and fast updates
to this backend.
</p>
</div>
<div class="field" v-if="backend.export">
<label class="label" for="backend_export">Export data to this backend?</label>
<label class="label" for="backend_export">Send play and progress updates to this backend?</label>
<div class="control">
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
<label for="backend_export">Enable</label>
<p class="help">
Export means to send the data from the database to this backend.
</p>
<label for="backend_export" class="is-unselectable">
{{ backend.export.enabled ? 'Yes' : 'No' }}
</label>
</div>
<p class="help is-bold has-text-danger">
<span class="icon"><i class="fas fa-info-circle"/></span>
The backend will not receive any data from WatchState if this is disabled.
</p>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_user">Webhook match user</label>
<label class="label" for="webhook_match_user">Enable match user for webhook?</label>
<div class="control">
<input id="webhook_match_user" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.user">
<label for="webhook_match_user">Enable</label>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
v-model="backend.webhook.match.user">
<label for="webhook_match_user" class="is-unselectable">
{{ backend.webhook.match.user ? 'Yes' : 'No' }}
</label>
</div>
<p class="help">
Check webhook payload for user id match. if it does not match, the payload will be ignored.
</p>
</div>
<div class="field" v-if="backend.webhook">
<label class="label" for="webhook_match_uuid">Webhook match backend id</label>
<label class="label" for="webhook_match_uuid">Enable match backend id for webhook?</label>
<div class="control">
<input id="webhook_match_uuid" type="checkbox" class="switch is-success"
v-model="backend.webhook.match.uuid">
<label for="webhook_match_uuid">Enable</label>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
v-model="backend.webhook.match.uuid">
<label for="webhook_match_uuid">
{{ backend.webhook.match.uuid ? 'Yes' : 'No' }}
</label>
</div>
<p class="help">
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
</p>
</div>
<hr>
<div class="field">
<label class="label is-clickable" @click="showOptions = !showOptions">
<label class="label is-clickable is-unselectable" @click="showOptions = !showOptions">
<span class="icon-text">
<span class="icon">
<i v-if="showOptions" class="fas fa-arrow-up"></i>
<i v-else class="fas fa-arrow-down"></i>
<i v-if="showOptions" class="fas fa-arrow-up"/>
<i v-else class="fas fa-arrow-down"/>
</span>
<span>Additional options...</span>
</span>
</label>
<p class="help is-unselectable">
These are advanced options. Please only change them, if you are told to do so by the developers.
</p>
<template v-if="showOptions">
<div class="columns is-multiline is-mobile">
<template v-for="_option in flatOptionPaths" :key="'bo-'+_option">
@@ -298,18 +317,18 @@
<input type="text" class="input" :value="_option" readonly disabled>
<p class="help is-unselectable">
<span class="icon has-text-info">
<i class="fas fa-info-circle" :class="{ 'fa-bounce': newOptions[_option] }"></i>
<i class="fas fa-info-circle" :class="{ 'fa-bounce': newOptions[_option] }"/>
</span>
{{ option_describe(_option) }}
</p>
</div>
<div class="column is-6">
<input type="text" class="input" :value="option_get(_option)"
@input="e => option_set(_option, e.target.value)" required>
@input="e => option_set(_option, e.target.value)" required>
</div>
<div class="column is-1">
<button class="button is-danger" @click.prevent="removeOption(_option)">
<span class="icon"><i class="fas fa-trash" /></span>
<span class="icon"><i class="fas fa-trash"/></span>
</button>
</div>
</template>
@@ -317,7 +336,7 @@
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<span class="icon-text">
<span class="icon"><i class="fas fa-plus"></i></span>
<span class="icon"><i class="fas fa-plus"/></span>
<span>Add new option</span>
</span>
</div>
@@ -326,7 +345,7 @@
<select v-model="selectedOption">
<option value="">Select Option</option>
<option v-for="option in filteredOptions(optionsList)" :key="'opt-' + option.key"
:value="option.key">
:value="option.key">
{{ option.key }}
</option>
</select>
@@ -338,7 +357,7 @@
<div class="column is-1">
<button class="button is-primary" @click.prevent="addOption">
<span class="icon">
<i class="fas fa-add"></i>
<i class="fas fa-add"/>
</span>
</button>
</div>
@@ -348,11 +367,11 @@
</div>
<div class="card-footer">
<button class="button card-footer-item is-fullwidth is-primary" type="submit">
<span class="icon"><i class="fas fa-save"></i></span>
<span class="icon"><i class="fas fa-save"/></span>
<span>Save Settings</span>
</button>
<NuxtLink class="card-footer-item button is-fullwidth is-danger" :to="`/backend/${id}`">
<span class="icon"><i class="fas fa-cancel"></i></span>
<span class="icon"><i class="fas fa-cancel"/></span>
<span>Cancel changes</span>
</NuxtLink>
</div>
@@ -365,9 +384,9 @@
<script setup>
import 'assets/css/bulma-switch.css'
import { notification, ucFirst } from '~/utils/index'
import {notification} from '~/utils/index'
import Message from '~/components/Message'
import { useStorage } from "@vueuse/core";
import {useStorage} from "@vueuse/core";
import request from "~/utils/request.js";
const id = useRoute().params.backend
@@ -380,9 +399,9 @@ const backend = ref({
token: '',
uuid: '',
user: '',
import: { enabled: false },
export: { enabled: false },
webhook: { match: { user: false, uuid: false } },
import: {enabled: false},
export: {enabled: false},
webhook: {match: {user: false, uuid: false}},
options: {}
})
@@ -406,7 +425,7 @@ const selectedOptionHelp = computed(() => {
return option ? option.description : ''
});
useHead({ title: 'Backends - Edit: ' + id })
useHead({title: 'Backends - Edit: ' + id})
const loadContent = async () => {
supported.value = await (await request('/system/supported')).json()
@@ -442,7 +461,7 @@ const saveContent = async () => {
try {
const response = await request(`/backend/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(json_text)
})
@@ -454,7 +473,7 @@ const saveContent = async () => {
notification('success', 'Success', `Successfully updated '${id}' settings.`)
const to = !redirect.startsWith('/') ? `/backend/${id}` : redirect
await navigateTo({ path: to })
await navigateTo({path: to})
} catch (e) {
notification('error', 'Error', `Request error. ${e.message}`)
}
@@ -471,7 +490,7 @@ const removeOption = async (key) => {
return
}
const response = await request(`/backend/${id}/option/options.${key}`, { method: 'DELETE' })
const response = await request(`/backend/${id}/option/options.${key}`, {method: 'DELETE'})
if (!response.ok) {
const json = await response.json()
@@ -540,20 +559,15 @@ const getUsers = async (showAlert = true) => {
token: backend.value.token,
url: backend.value.url,
uuid: backend.value.uuid,
user: backend.value.user
user: backend.value.user,
options: {},
};
if (backend.value.options && backend.value.options?.ADMIN_TOKEN) {
data.options = {
ADMIN_TOKEN: backend.value.options.ADMIN_TOKEN
['ADMIN_TOKEN', 'plex_guest_user', 'PLEX_USER_PIN', 'is_limited_token'].forEach(v => {
if (backend.value.options && backend.value.options[v]) {
data.options[v] = backend.value.options[v]
}
}
if (backend.value.options && backend.value.options?.is_limited_token) {
data.options = {
is_limited_token: Boolean(backend.value.options.is_limited_token)
}
}
})
const response = await request(`/backends/users/${backend.value.type}?tokens=1`, {
method: 'POST',
@@ -599,7 +613,7 @@ const filteredOptions = options => {
}
const getServers = async () => {
if ('plex' !== backend.value.type || servers.value.length > 0) {
if ('plex' !== backend.value.type) {
return
}

View File

@@ -16,8 +16,10 @@ environment without modifying the original data.
### Adding your backend with has the most accurate data
First, go to <!--i:fa-server--> **Backends** and click on the <!--i:fa-plus--> **Add Backend** button. Follow the
interactive setup guide. When you reach the step asking *`Export data to this backend?`*, disable it, this applies to
your main backend as you dont want to alter its data. Keep *`Import data from this backend?`* enabled, which will allow
interactive setup guide. When you reach the step asking *`Send play and progress updates to this backend?`*, disable it,
this applies to
your main backend as you dont want to alter its data. Keep *`Import play and progress updates from this backend?`*
enabled, which will allow
you to import data from the backend.
Enable the *`Force one time import from this backend?`* option to import your current data into WatchState. This will
@@ -62,16 +64,17 @@ For each backend, follow these steps:
Do exactly as you did for the main backend, but make the following changes:
- *`Import data from this backend?`*: No
- *`Import metadata only from this backend?`*: Yes
- *`Export data to this backend?`*: Yes
- *`Import play and progress updates from this backend?`*: No
- *`Import metadata from this backend?`*: Yes
- *`Send play and progress updates to this backend?`*: Yes
- *`Force Export local data to this backend?`*: This depends on your setup:
- If you have only one extra backend and have already imported your main backend data, select *Yes* and skip Step 2.
- If you have multiple backends, keep this option disabled and proceed to Step 2.
> [!IMPORTANT]
> Selecting the correct options is crucial to avoid altering the data in the main backend. The
> *`Import metadata only from this backend?`* option will only appear if you have *`Import data from this backend?`*
> *`Import metadata from this backend?`* option will only appear if you have
*`Import play and progress updates from this backend?`*
> disabled.
### Step 2

View File

@@ -19,8 +19,8 @@ On the export side, we compare the backend's last sync date with any local chang
items that need updating for each backend. If there are only a few changes, we trigger a quick sync operation
`push mode`. If the changes are more extensive, we perform a full export, which compares all remote data with the local
data. This full export only happens when there are many changes and/or metadata is missing from the backend, which is
why it's crucial to keep the `Import data from this backend?` or, at a minimum, the
`Import metadata only from this backend?` option enabled.
why it's crucial to keep the `Import play and progress updates from this backend?` or, at a minimum, the
`Import metadata from this backend?` option enabled.
# Setting Up Two-Way Sync

View File

@@ -36,7 +36,6 @@ final class GetUsersList
private array $rawRequests = [];
public function __construct(iHttp $http, protected iLogger $logger)
{
$this->http = new RetryableHttpClient(client: $http, maxRetries: $this->maxRetry, logger: $this->logger);
@@ -156,6 +155,10 @@ final class GetUsersList
*/
private function getExternalUsers(Context $context, array $opts = []): Response
{
if (true === (bool)ag($context->options, Options::PLEX_GUEST_USER, false)) {
return new Response(status: true, response: []);
}
$url = Container::getNew(iUri::class)->withPort(443)->withScheme('https')->withHost('plex.tv')
->withPath('/api/users/');
@@ -201,7 +204,7 @@ final class GetUsersList
$url = $url->withQuery(http_build_query(['pin' => $pin]));
}
$this->logger->debug("Requesting '{user}@{backend}' external users accesstokens.", [
$this->logger->debug("Requesting '{user}@{backend}' external users access-tokens.", [
'user' => $context->userContext->name,
'backend' => $context->backendName,
'url' => (string)$url,
@@ -240,21 +243,27 @@ final class GetUsersList
*
* @return array Return processed response.
* @throws iException if an error occurs during the request.
* @throws JsonException if an error occurs during the JSON parsing.
*/
private function processExternalUsers(iResponse $response, Context $context, iUri $url): array
{
$data = json_decode(
json: json_encode(simplexml_load_string($response->getContent(false))),
associative: true,
flags: JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
);
$content = simplexml_load_string($response->getContent(false));
$data = [];
foreach ($content->User ?? [] as $_user) {
$user = [];
// @INFO: This workaround is needed, for some reason array_map() doesn't work correctly on xml objects.
/** @noinspection PhpLoopCanBeConvertedToArrayMapInspection */
foreach ($_user->attributes() as $k => $v) {
$user[$k] = (string)$v;
}
$data[] = $user;
}
if ($this->logRequests) {
$this->rawRequests[] = [
'url' => (string)$url,
'headers' => $response->getHeaders(false),
'body' => $data,
'body' => json_decode(json_encode($content), true),
];
}
@@ -267,15 +276,12 @@ final class GetUsersList
}
$list = [];
foreach (ag($data, 'User', []) as $data) {
$user = ag($data, '@attributes', []);
foreach ($data as $user) {
$uuidStatus = preg_match('/\/users\/(?<uuid>.+?)\/avatar/', ag($user, 'thumb', ''), $matches);
$list[] = [
'id' => ag($user, 'id'),
'uuid' => 1 === $uuidStatus ? ag($matches, 'uuid') : ag($user, 'invited_user'),
'name' => ag($user, ['username', 'title', 'email'], '??'),
'name' => ag($user, ['username', 'title', 'email', 'id'], '??'),
'admin' => false,
'guest' => 1 !== (int)ag($user, 'home'),
'restricted' => 1 === (int)ag($user, 'restricted'),
@@ -502,7 +508,6 @@ final class GetUsersList
'backend' => $context->backendName,
'status_code' => $response->getStatusCode(),
'body' => $response->getContent(false),
'parsed' => $response->toArray(false),
'extra_msg' => !$extra_msg ? '' : ". $extra_msg",
'tokenType' => ag_exists(
$context->options,

View File

@@ -28,6 +28,7 @@ final class Options
public const string PLEX_USER_UUID = 'plex_user_uuid';
public const string PLEX_USER_NAME = 'plex_user_name';
public const string PLEX_EXTERNAL_USER = 'plex_external_user';
public const string PLEX_GUEST_USER = 'plex_guest_user';
public const string NO_THROW = 'NO_THROW';
public const string NO_LOGGING = 'NO_LOGGING';
public const string MAX_EPISODE_RANGE = 'MAX_EPISODE_RANGE';

View File

@@ -98,6 +98,10 @@ trait APITraits
$backend['options'] = [];
}
if (false === ag_exists($backend, 'webhook')) {
$backend['webhook'] = ['match' => ['user' => false, 'uuid' => false]];
}
$backends[] = $backend;
}
@@ -154,6 +158,10 @@ trait APITraits
$options[Options::IS_LIMITED_TOKEN] = (bool)$data->get('options.' . Options::IS_LIMITED_TOKEN, false);
}
if (null !== $data->get('options.' . Options::PLEX_GUEST_USER)) {
$options[Options::PLEX_GUEST_USER] = (bool)$data->get('options.' . Options::PLEX_GUEST_USER, false);
}
$instance = Container::getNew($class);
assert($instance instanceof ClientInterface, new InvalidArgumentException('Invalid client class.'));
return $instance->withContext(