Fully implemented Custom GUIDs support and added WebUI page to manage it.
This commit is contained in:
19
FAQ.md
19
FAQ.md
@@ -952,11 +952,12 @@ Note: the tip about adding the group_add came from the user `binarypancakes` in
|
||||
|
||||
### Advanced: How to extend the GUID parser to support more GUIDs or custom ones?
|
||||
|
||||
You can extend the parser by creating new file at `/config/config/guid.yaml` with the following content.
|
||||
By going to `More > Custom GUIDs` in the WebUI, you can add custom GUIDs to the parser. We know not all people,
|
||||
like using GUI, as such You can extend the parser by creating new file at `/config/config/guid.yaml` with the following content.
|
||||
|
||||
```yaml
|
||||
# The version of the guid file. right now in beta so it's 0.0. not required to be present.
|
||||
version: 0.0
|
||||
# (Optional) The version of the guid file. If omitted, it will default to the latest version.
|
||||
version: 1.0
|
||||
|
||||
# The key must be in lower case. and it's an array.
|
||||
guids:
|
||||
@@ -982,10 +983,6 @@ links:
|
||||
type: plex # the client to link the guid to. plex, jellyfin, emby.
|
||||
options: # options used by the client.
|
||||
legacy: true # Tag the mapper as legacy GUID for mapping.
|
||||
# Required map object. to map the new guid to WatchState guid.
|
||||
map:
|
||||
from: com.plexapp.agents.foo # map.from this string.
|
||||
to: guid_mydb # map.to this guid.
|
||||
# (Optional) Replace helper. Sometimes you need to replace the guid identifier to another.
|
||||
# The replacement happens before the mapping, so if you replace the guid identifier, you should also
|
||||
# update the map.from to match the new identifier.
|
||||
@@ -993,6 +990,10 @@ links:
|
||||
replace:
|
||||
from: com.plexapp.agents.foobar:// # Replace from this string
|
||||
to: com.plexapp.agents.foo:// # Into this string.
|
||||
# Required map object. to map the new guid to WatchState guid.
|
||||
map:
|
||||
from: com.plexapp.agents.foo # map.from this string.
|
||||
to: guid_mydb # map.to this guid.
|
||||
|
||||
# mapping the foo guid from jellyfin backends into the guid_mydb in WatchState.
|
||||
- id: universally-unique-identifier # the link id. example, 1ef83f5d-1686-60f0-96d6-3eb5c18f2aed
|
||||
@@ -1010,11 +1011,11 @@ links:
|
||||
```
|
||||
|
||||
As you can see from the config, it's roughly how we expected it to be. The `guids` array is where you define your new
|
||||
guids. the `links` array is where you map from backends guids to the new guid into the WatchState guid.
|
||||
custom GUIDs. the `links` array is where you map from client/backends GUIDs to the custom GUID in `WatchState`.
|
||||
|
||||
Everything in this file should be in lower case. If error occurs, the tool will log a warning and ignore the guid,
|
||||
By default, we only show `ERROR` levels in log file, You can lower it by setting `WS_LOGGER_FILE_LEVEL` environment variable
|
||||
to `WARNING`.
|
||||
|
||||
If you added or removed a guid from the `guid.yaml` file, you should run `system:reindex --force-reindex` command to update the
|
||||
If you added or removed a guid from the `guid.yaml` file, you should run `system:index --force-reindex` command to update the
|
||||
database indexes with the new guids.
|
||||
|
||||
12
NEWS.md
12
NEWS.md
@@ -1,5 +1,17 @@
|
||||
# Old Updates
|
||||
|
||||
### 2024-08-19
|
||||
|
||||
We have migrated the `state:push` task into the new events system, as such the old task `state:push` is now gone.
|
||||
To enable the new event handler for push events, use the new environment variable `WS_PUSH_ENABLED` and set it to `true`.
|
||||
Right now, it's disabled by default. However, for people who had the old task enabled, it will reuse that setting.
|
||||
|
||||
Keep in mind, the new event handler is more efficient and will only push data when there is a change in the play state. And it's much faster
|
||||
than the old task. This event handler will push data within a minute of the change.
|
||||
|
||||
PS: Please enable the task by setting its new environment variable `WS_PUSH_ENABLED` to `true`. The old `WS_CRON_PUSH` is now gone.
|
||||
and will be removed in the future releases.
|
||||
|
||||
### 2024-08-18
|
||||
|
||||
We have started migrating the old events system to a new one, so far we have migrated the `progress` and `requests` to it. As such,
|
||||
|
||||
19
README.md
19
README.md
@@ -9,6 +9,13 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
|
||||
|
||||
## Updates
|
||||
|
||||
### 2024-10-07
|
||||
|
||||
We have added a WebUI page for Custom GUIDs and stabilized on `v1.0` for the `guid.yaml` file spec. We strongly recommend
|
||||
to use the `WebUI` to manage the GUIDs, as it's much easier to use than editing the `guid.yaml` file directly. and both the
|
||||
`WebUI` and `API` have safeguards to prevent you from breaking the parser. For more information please check out the associated
|
||||
FAQ entry about it at [this link](FAQ.md#advanced-how-to-extend-the-guid-parser-to-support-more-guids-or-custom-ones).
|
||||
|
||||
### 2024-09-14
|
||||
|
||||
We have recently added support for extending WatchState with more GUIDs, as of now, the support for it is done via
|
||||
@@ -18,18 +25,6 @@ FAQ entry about it at [this link](FAQ.md#advanced-how-to-extend-the-guid-parser-
|
||||
The mapping should work for all officially supported clients. If you have a client that is not supported, you have to manually add support for that client,
|
||||
or request the maintainer to add support for it.
|
||||
|
||||
### 2024-08-19
|
||||
|
||||
We have migrated the `state:push` task into the new events system, as such the old task `state:push` is now gone.
|
||||
To enable the new event handler for push events, use the new environment variable `WS_PUSH_ENABLED` and set it to `true`.
|
||||
Right now, it's disabled by default. However, for people who had the old task enabled, it will reuse that setting.
|
||||
|
||||
Keep in mind, the new event handler is more efficient and will only push data when there is a change in the play state. And it's much faster
|
||||
than the old task. This event handler will push data within a minute of the change.
|
||||
|
||||
PS: Please enable the task by setting its new environment variable `WS_PUSH_ENABLED` to `true`. The old `WS_CRON_PUSH` is now gone.
|
||||
and will be removed in the future releases.
|
||||
|
||||
---
|
||||
Refer to [NEWS](NEWS.md) for old updates.
|
||||
|
||||
|
||||
@@ -99,10 +99,18 @@
|
||||
<span>Log Suppression</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/custom" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-map"></i></span>
|
||||
<span>Custom GUIDs</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/backup" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-sd-card"></i></span>
|
||||
<span>Backups</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/reset" @click.native="(e) => changeRoute(e)">
|
||||
@@ -341,7 +349,6 @@ import 'assets/css/all.css'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
import request from '~/utils/request.js'
|
||||
import Markdown from '~/components/Markdown.vue'
|
||||
import {dEvent} from '~/utils/index.js'
|
||||
import TaskRunnerStatus from "~/components/TaskRunnerStatus.vue";
|
||||
|
||||
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
|
||||
|
||||
@@ -307,7 +307,9 @@ onMounted(async () => {
|
||||
allEnabled.value = false
|
||||
}
|
||||
|
||||
if (Boolean(route.query?.run ?? '0') || '' === command.value) {
|
||||
const run = route.query?.run ? Boolean(route.query.run) : false
|
||||
|
||||
if (run || '' === command.value) {
|
||||
await RunCommand()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,192 +7,178 @@
|
||||
Add Custom GUID
|
||||
</span>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle"></span>
|
||||
<span class="subtitle">
|
||||
This custom GUID allows you to extend <code>WatchState</code> GUID parser with your custom GUIDs. Using this
|
||||
feature, You are able to use more metadata databases for references between the backends.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<form id="page_form" @submit.prevent="addNewGuid">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-unselectable is-justify-center">Add Custom GUID</p>
|
||||
</header>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_guid_name">Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_guid_name" type="text" v-model="form.name" placeholder="foobar">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The internal GUID reference name. The rules are <code>lower case [a-z]</code>, <code>0-9</code>,
|
||||
<code>no space</code>.
|
||||
For example, <code>guid_imdb</code>. The guid name will be automatically prefixed with
|
||||
<code>guid_</code>.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_description">Description</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_description" type="text" v-model="form.description"
|
||||
placeholder="This GUID is based on ... db reference">
|
||||
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID description, For information purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_guid_name">Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_guid_name" type="text" v-model="form.name" placeholder="foobar">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The internal GUID reference name. The rules are <code>lower case [a-z]</code>, <code>0-9</code>,
|
||||
<code>no space</code>.
|
||||
For example, <code>guid_imdb</code>. The guid name will be automatically prefixed with
|
||||
<code>guid_</code>.
|
||||
</span>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_select_type">Type</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="form_select_type" v-model="form.type">
|
||||
<option value="" disabled>Select Type</option>
|
||||
<option value="string">String</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_description">Description</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_description" type="text" v-model="form.description"
|
||||
placeholder="This GUID is based on ... db reference">
|
||||
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID description, For information purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_select_type">Type</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="form_select_type" v-model="form.type">
|
||||
<option value="" disabled>Select Type</option>
|
||||
<option value="string">String</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>We currently only support <code>string</code> type.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_validation_pattern">Regex validation pattern</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_validation_pattern" type="text" v-model="form.validator.pattern"
|
||||
placeholder="/^[0-9\\/]+$/i">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
|
||||
website
|
||||
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_validation_example">Value example</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_validation_example" type="text" v-model="form.validator.example"
|
||||
placeholder="(number)">
|
||||
<div class="icon is-small is-left"><i class="fas fa-ear-deaf"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The example to show when invalid value was checked. For example, <code>(number)</code>. For
|
||||
information purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable">
|
||||
Correct values.
|
||||
<NuxtLink class="has-text-primary" @click="form.validator.tests.valid.push('')" v-text="'Add'"/>
|
||||
</label>
|
||||
<div class="columns is-multiline">
|
||||
<template v-for="(_, index) in form.validator.tests.valid" :key="`valid-${index}`">
|
||||
<div class="column is-11">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" :id="`valid-${index}`"
|
||||
v-model="form.validator.tests.valid[index]">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="button is-danger" type="button"
|
||||
@click="form.validator.tests.valid.splice(index, 1)"
|
||||
:disabled="index < 1 || form.validator.tests.valid < 1">
|
||||
<span class="icon"><i class="fas fa-trash"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
The values added here must match the pattern defined above. Example: <code>123</code>.
|
||||
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
|
||||
for relative GUIDs. The <code>(number)/1/1</code> refers to a relative GUID.
|
||||
There must be a minimum of 1 correct value.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable">
|
||||
Incorrect values.
|
||||
<NuxtLink class="has-text-danger" @click="form.validator.tests.invalid.push('')" v-text="'Add'"/>
|
||||
</label>
|
||||
<div class="columns is-multiline">
|
||||
<template v-for="(_, index) in form.validator.tests.invalid" :key="`valid-${index}`">
|
||||
<div class="column is-11">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" :id="`invalid-${index}`"
|
||||
v-model="form.validator.tests.invalid[index]">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="button is-danger" type="button"
|
||||
@click="form.validator.tests.invalid.splice(index, 1)"
|
||||
:disabled="index < 1 || form.validator.tests.invalid < 1">
|
||||
<span class="icon"><i class="fas fa-trash"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
|
||||
must be a minimum of 1 incorrect value.</span>
|
||||
</p>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>We currently only support <code>string</code> type.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === validForm || isSaving"
|
||||
:class="{'is-loading':isSaving}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
<span>Save</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="navigateTo('/custom')">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cancel"></i></span>
|
||||
<span>Cancel</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_validation_pattern">Regex validation pattern</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_validation_pattern" type="text" v-model="form.validator.pattern"
|
||||
placeholder="/^[0-9\\/]+$/i">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
|
||||
website
|
||||
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_validation_example">Value example</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_validation_example" type="text" v-model="form.validator.example"
|
||||
placeholder="(number)">
|
||||
<div class="icon is-small is-left"><i class="fas fa-ear-deaf"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The example to show when invalid value was checked. For example, <code>(number)</code>. For
|
||||
information purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable">
|
||||
Correct values.
|
||||
<NuxtLink class="has-text-primary" @click="form.validator.tests.valid.push('')" v-text="'Add'"/>
|
||||
</label>
|
||||
<div class="columns is-multiline">
|
||||
<template v-for="(_, index) in form.validator.tests.valid" :key="`valid-${index}`">
|
||||
<div class="column is-11">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" :id="`valid-${index}`"
|
||||
v-model="form.validator.tests.valid[index]">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="button is-danger" type="button"
|
||||
@click="form.validator.tests.valid.splice(index, 1)"
|
||||
:disabled="index < 1 || form.validator.tests.valid < 1">
|
||||
<span class="icon"><i class="fas fa-trash"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
The values added here must match the pattern defined above. Example: <code>123</code>.
|
||||
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
|
||||
for relative GUIDs. The <code>(number)/1/1</code> refers to a relative GUID.
|
||||
There must be a minimum of 1 correct value.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable">
|
||||
Incorrect values.
|
||||
<NuxtLink class="has-text-danger" @click="form.validator.tests.invalid.push('')" v-text="'Add'"/>
|
||||
</label>
|
||||
<div class="columns is-multiline">
|
||||
<template v-for="(_, index) in form.validator.tests.invalid" :key="`valid-${index}`">
|
||||
<div class="column is-11">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" :id="`invalid-${index}`"
|
||||
v-model="form.validator.tests.invalid[index]">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="button is-danger" type="button"
|
||||
@click="form.validator.tests.invalid.splice(index, 1)"
|
||||
:disabled="index < 1 || form.validator.tests.invalid < 1">
|
||||
<span class="icon"><i class="fas fa-trash"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
|
||||
must be a minimum of 1 incorrect value.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === validForm || isSaving"
|
||||
:class="{'is-loading':isSaving}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
<span>Save</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="navigateTo('/custom')">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cancel"></i></span>
|
||||
<span>Cancel</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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>-->
|
||||
<!-- </ul>-->
|
||||
<!-- </Message>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -218,6 +204,7 @@ const empty_form = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const show_page_tips = useStorage('show_page_tips', true)
|
||||
const form = ref(JSON.parse(JSON.stringify(empty_form)))
|
||||
const guids = ref([])
|
||||
@@ -256,7 +243,7 @@ const addNewGuid = async () => {
|
||||
notification('error', 'Error', `GUID name must not contain spaces.`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data.type = data.type.trim().toLowerCase();
|
||||
if (!['string'].includes(data.type)) {
|
||||
notification('error', 'Error', `Invalid GUID type.`, 5000)
|
||||
@@ -264,12 +251,12 @@ const addNewGuid = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
toRaw(guids.value).forEach(g => {
|
||||
const name = data.name.split('_')[1]
|
||||
if (g.guid === name) {
|
||||
throw new Error(`GUID with name '${data.name}' already exists.`)
|
||||
for (const g of guids.value) {
|
||||
if (g.guid === data.name) {
|
||||
notification('error', 'Error', `GUID with name '${data.name}' already exists.`, 5000)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `${e}`, 5000)
|
||||
return false
|
||||
|
||||
@@ -7,159 +7,147 @@
|
||||
Add new client GUID link
|
||||
</span>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle"></span>
|
||||
<span class="subtitle">
|
||||
This page allows you to add a new client GUID link. The client GUID link is used to link the client/backend
|
||||
GUID to the <code>WatchState</code> GUID or your custom GUID.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<form id="page_form" @submit.prevent="addNewLink">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-unselectable is-justify-center">Add new client GUID link</p>
|
||||
</header>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_select_type">Client</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="form_select_type" v-model="form.type">
|
||||
<option value="" disabled>Select client type</option>
|
||||
<option v-for="client in supported" :value="client" :key="`client-${client}`">
|
||||
{{ ucFirst(client) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>Select which client this link association for.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_map_from">Link client GUID</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_map_from" type="text" v-model="form.map.from">
|
||||
<div class="icon is-small is-left"><i class="fas fa-a"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>Write the <code>{{ form.type.length > 0 ? ucFirst(form.type) : 'client' }}</code> GUID
|
||||
identifier.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_map_to">To This GUID</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="form_map_to" v-model="form.map.to">
|
||||
<option value="" disabled>Select the associated GUID</option>
|
||||
<option v-for="(g) in guids" :value="g.guid" :key="`guid-${g.guid}`">
|
||||
{{ g.guid }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
Select which <code>WatchState</code> GUID should link with this
|
||||
<code>{{ form.type.length > 0 ? ucFirst(form.type) : 'client' }}</code> GUID identifier.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="'plex' === form.type">
|
||||
<label class="label" for="backend_import">Is this a Plex legacy agent GUID?</label>
|
||||
<div class="control">
|
||||
<input id="backend_import" type="checkbox" class="switch is-success" v-model="form.options.legacy">
|
||||
<label for="backend_import">Enable</label>
|
||||
<p class="help">Plex legacy agents starts with <code>com.plexapp.agents.</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="'plex' === form.type && true === form.options.legacy">
|
||||
<div class="field">
|
||||
<label class="label is-clickable is-unselectable" @click="toggleReplace = !toggleReplace">
|
||||
<span class="icon">
|
||||
<i class="fas" :class="{ 'fa-arrow-up': toggleReplace, 'fa-arrow-down': !toggleReplace }"></i>
|
||||
</span>
|
||||
Toggle Text replacement.
|
||||
</label>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>Text replacement only works for plex legacy agents.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="toggleReplace">
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_select_type">Client</label>
|
||||
<label class="label is-unselectable" for="form_replace_from">Search for</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="form_select_type" v-model="form.type">
|
||||
<option value="" disabled>Select client type</option>
|
||||
<option v-for="client in supported" :value="client" :key="`client-${client}`">
|
||||
{{ ucFirst(client) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<input class="input" id="form_replace_from" type="text" v-model="form.replace.from">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>Select which client this link association for.</span>
|
||||
<span>The text string to replace. Sometimes it's necessary to replace legacy agent GUID into
|
||||
something else. Leave it empty to ignore it.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_map_from">Link client GUID</label>
|
||||
<label class="label is-unselectable" for="form_replace_to">Replace with</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_map_from" type="text" v-model="form.map.from">
|
||||
<div class="icon is-small is-left"><i class="fas fa-a"></i></div>
|
||||
<input class="input" id="form_replace_to" type="text" v-model="form.replace.to">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>Write the <code>{{ form.type.length > 0 ? ucFirst(form.type) : 'client' }}</code> GUID
|
||||
identifier.</span>
|
||||
<span>The string replacement. If <code>replace.from</code> is empty this field will be
|
||||
ignored.</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_map_to">To This GUID</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="form_map_to" v-model="form.map.to">
|
||||
<option value="" disabled>Select the associated GUID</option>
|
||||
<option v-for="(g) in guids" :value="g.guid" :key="`guid-${g.guid}`">
|
||||
{{ g.guid }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-left">
|
||||
<i class="fas fa-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
Select which <code>WatchState</code> GUID should link with this
|
||||
<code>{{ form.type.length > 0 ? ucFirst(form.type) : 'client' }}</code> GUID identifier.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="'plex' === form.type">
|
||||
<label class="label" for="backend_import">Is this a Plex legacy agent GUID?</label>
|
||||
<div class="control">
|
||||
<input id="backend_import" type="checkbox" class="switch is-success" v-model="form.options.legacy">
|
||||
<label for="backend_import">Enable</label>
|
||||
<p class="help">Plex legacy agents starts with <code>com.plexapp.agents.</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="'plex' === form.type && true === form.options.legacy">
|
||||
<div class="field">
|
||||
<label class="label is-clickable is-unselectable" @click="toggleReplace = !toggleReplace">
|
||||
<span class="icon">
|
||||
<i class="fas" :class="{ 'fa-arrow-up': toggleReplace, 'fa-arrow-down': !toggleReplace }"></i>
|
||||
</span>
|
||||
Toggle Text replacement.
|
||||
</label>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>Text replacement only works for plex legacy agents.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="toggleReplace">
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_replace_from">Search for</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_replace_from" type="text" v-model="form.replace.from">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The text string to replace. Sometimes it's necessary to replace legacy agent GUID into
|
||||
something else. Leave it empty to ignore it.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_replace_to">Replace with</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_replace_to" type="text" v-model="form.replace.to">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The string replacement. If <code>replace.from</code> is empty this field will be
|
||||
ignored.</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" type="submit"
|
||||
:disabled="false === validForm || isSaving"
|
||||
:class="{'is-loading':isSaving}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
<span>Save</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="navigateTo('/custom')">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cancel"></i></span>
|
||||
<span>Cancel</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-primary" type="submit"
|
||||
:disabled="false === validForm || isSaving"
|
||||
:class="{'is-loading':isSaving}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
<span>Save</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="navigateTo('/custom')">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cancel"></i></span>
|
||||
<span>Cancel</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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>-->
|
||||
<!-- </ul>-->
|
||||
<!-- </Message>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -197,15 +185,15 @@ const toggleReplace = ref(false)
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
||||
/** @type {Array<Promise>} */
|
||||
/** @type {Array<Promise<Response>>} */
|
||||
const responses = await Promise.all([
|
||||
request('/system/guids'),
|
||||
request('/system/supported'),
|
||||
request('/system/guids/custom'),
|
||||
])
|
||||
|
||||
guids.value = await parse_api_response(responses[0])
|
||||
supported.value = await parse_api_response(responses[1])
|
||||
guids.value = await parse_api_response(responses[0]) ?? []
|
||||
supported.value = await parse_api_response(responses[1]) ?? []
|
||||
links.value = (await parse_api_response(responses[2])).links ?? []
|
||||
|
||||
} catch (e) {
|
||||
@@ -288,13 +276,5 @@ const addNewLink = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const validForm = computed(() => {
|
||||
const data = form.value
|
||||
|
||||
if (!data.map.to || !data.map.from || !data.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
const validForm = computed(() => !(!form.value.map.to || !form.value.map.from || !form.value.type))
|
||||
</script>
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
This page allow you to add custom GUIDs to the system and link them to the client GUIDs.
|
||||
</span>
|
||||
<span class="subtitle">User defined custom GUIDs.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,9 +35,9 @@
|
||||
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="!isLoading && data">
|
||||
<div class="columns is-multiline" v-if="data?.guids?.length >0">
|
||||
<div class="column is-3-tablet" v-for="(guid, index) in data.guids" :key="guid.name">
|
||||
<div class="column is-12" v-if="!isLoading && guids">
|
||||
<div class="columns is-multiline" v-if="guids?.length >0">
|
||||
<div class="column is-3-tablet" v-for="(guid, index) in guids" :key="guid.name">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-text-overflow pr-1">
|
||||
@@ -58,7 +56,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Message message_class="has-background-warning-90 has-text-dark" title="Information" icon="fas fa-check">
|
||||
<Message message_class="has-background-warning-90 has-text-dark" title="Information"
|
||||
icon="fas fa-exclamation">
|
||||
There are no custom GUIDs configured. You can add new GUIDs by clicking on the <i class="fa fa-add"></i>
|
||||
button.
|
||||
</Message>
|
||||
@@ -81,14 +80,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">This section contains the client <> WatchState GUID links.</span>
|
||||
<span class="subtitle">Client <--> WatchState GUID links.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="column is-12" v-if="!isLoading && data && data?.links?.length > 0">
|
||||
<div class="column is-12" v-if="!isLoading && links?.length > 0">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6-tablet" v-for="(link,index) in data.links" :key="link.id">
|
||||
<div class="column is-6-tablet" v-for="(link, index) in links" :key="link.id">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<template v-if="link.replace?.from">
|
||||
@@ -152,21 +151,56 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Message message_class="has-background-warning-90 has-text-dark" title="Information" icon="fas fa-xmark">
|
||||
There are no client links configured. You can add new links by clicking on the <i class="fa fa-add"></i>
|
||||
<Message message_class="has-background-warning-90 has-text-dark" title="Information"
|
||||
icon="fas fa-exclamation">
|
||||
There are no client GUID links configured. You can add new links by clicking on the <i
|
||||
class="fa fa-add"></i>
|
||||
button.
|
||||
</Message>
|
||||
</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>-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
<!-- </Message>-->
|
||||
<!-- </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>Using this feature allows you to extend <code>WatchState</code> to support more less known or regional
|
||||
specific metadata databases. We cannot add support directly to all databases, so this feature instead
|
||||
allow you to manually do it yourself.
|
||||
</li>
|
||||
<li>
|
||||
Adding Custom guid without a client/s links is useless as the parsing engine will not know what to do with
|
||||
it. So, make sure to add a client GUID link referencing the custom GUID.
|
||||
</li>
|
||||
<li>The guid names are unique. Therefore, you cannot reuse existing ones.</li>
|
||||
<li>You cannot add link from the same client GUID twice. For example you cannot add <code>jellyfin:foobar ->
|
||||
WatchState:guid_foobar</code> and another for <code>jellyfin:foobar -> guid_imdb</code>.
|
||||
</li>
|
||||
<li>Editing the <code>guid.yaml</code> file directly is unsupported and might lead to unexpected behavior.
|
||||
Please use the WebUI to manage the GUIDs. as we expose the entire functionality via the WebUI. with
|
||||
safeguards to prevent you from doing something that might break the system.
|
||||
</li>
|
||||
<li>If you added or removed Custom GUID, you should run
|
||||
<NuxtLink :to="makeConsoleCommand('system:index --force-reindex',false)">
|
||||
<span class="icon"><i class="fas fa-terminal"></i> </span>
|
||||
<span>system:index --force-reindex</span>
|
||||
</NuxtLink>
|
||||
command to rebuild the database indexes. While not required, it is recommended to ensure the database is
|
||||
up to date. and the indexing is correct and for speedy database operations.
|
||||
</li>
|
||||
<li>The links are global for each client, not the backend itself. So, For example, if you have NN jellyfin
|
||||
backends and you add new GUID link for jellyfin, it will be applied to all jellyfin backends. The backends
|
||||
themselves don't need to report it, however the support will be available for all backends.
|
||||
</li>
|
||||
<li>For more information please read the content in the <code>FAQ.md</code> page, or directly via
|
||||
<NuxtLink target="_blank"
|
||||
to="https://github.com/arabcoders/watchstate/blob/master/FAQ.md#advanced-how-to-extend-the-guid-parser-to-support-more-guids-or-custom-ones">
|
||||
<span class="icon"><i class="fas fa-external-link-alt"></i> </span>
|
||||
<span>this link</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -174,20 +208,27 @@
|
||||
<script setup>
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import request from '~/utils/request'
|
||||
import {notification, parse_api_response} from '~/utils/index'
|
||||
import {makeConsoleCommand, notification, parse_api_response} from '~/utils/index'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
import Message from '~/components/Message'
|
||||
|
||||
useHead({title: 'Custom Guids'})
|
||||
|
||||
const data = ref({})
|
||||
const guids = ref([])
|
||||
const links = ref([])
|
||||
const show_page_tips = useStorage('show_page_tips', true)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const loadContent = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
isLoading.value = true
|
||||
data.value = await parse_api_response(await request(`/system/guids/custom`))
|
||||
const response = await parse_api_response(await request(`/system/guids/custom`))
|
||||
for (const i of Object.keys(response.guids)) {
|
||||
guids.value.push(response.guids[i])
|
||||
}
|
||||
for (const i of Object.keys(response.links)) {
|
||||
links.value.push(response.links[i])
|
||||
}
|
||||
} catch (e) {
|
||||
isLoading.value = false
|
||||
return notification('error', 'Error', e.message)
|
||||
@@ -199,21 +240,45 @@ const loadContent = async () => {
|
||||
onMounted(async () => await loadContent())
|
||||
|
||||
const deleteGUID = async (index, guid) => {
|
||||
if (!confirm(`Are you sure you want to delete the GUID: '${guid.name}'?`)) {
|
||||
if (!confirm(`Delete '${guid.name}'? links using this GUID will be deleted as well.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(`/system/guids/custom/${guid.id}`, {method: 'DELETE'})
|
||||
const result = await parse_api_response(response)
|
||||
if (response.ok) {
|
||||
data.value.guids.splice(index, 1)
|
||||
notification('success', 'Success', result.message)
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
const result = await parse_api_response(response)
|
||||
notification('error', 'Error', result.error.message)
|
||||
return
|
||||
}
|
||||
|
||||
guids.value.splice(index, 1)
|
||||
links.value = links.value.filter(link => link.map.to !== guid.name)
|
||||
|
||||
notification('success', 'Success', `The GUID '${guid.name}' has been deleted.`)
|
||||
} catch (e) {
|
||||
return notification('error', 'Error', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteLink = async (index, link) => {
|
||||
if (!confirm(`Are you sure you want to delete the '${link.type}' - '${link.id}'?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(`/system/guids/custom/${link.type}/${link.id}`, {method: 'DELETE'})
|
||||
if (!response.ok) {
|
||||
const result = await parse_api_response(response)
|
||||
notification('error', 'Error', result.error.message)
|
||||
return
|
||||
}
|
||||
|
||||
links.value.splice(index, 1)
|
||||
notification('success', 'Success', `The link '${link.type}' - '${link.id}' has been deleted.`)
|
||||
} catch (e) {
|
||||
return notification('error', 'Error', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -56,20 +56,20 @@ final class Guids
|
||||
$params = DataUtil::fromRequest($request);
|
||||
|
||||
$requiredFields = [
|
||||
'name',
|
||||
'type',
|
||||
'description',
|
||||
'validator.pattern',
|
||||
'validator.example',
|
||||
'validator.tests.valid',
|
||||
'validator.tests.invalid'
|
||||
'name' => 'string',
|
||||
'type' => join('|', array_keys(Config::get('supported'))),
|
||||
'description' => 'string',
|
||||
'validator.pattern' => 'string',
|
||||
'validator.example' => 'string',
|
||||
'validator.tests.valid' => ['string'],
|
||||
'validator.tests.invalid' => ['string'],
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
foreach ($requiredFields as $field => $type) {
|
||||
if (!$params->get($field)) {
|
||||
return api_error(r("Field '{field}' is required. And is missing from request.", [
|
||||
'field' => $field
|
||||
]), Status::BAD_REQUEST);
|
||||
]), httpCode: Status::BAD_REQUEST, body: ['requiredFields' => $requiredFields]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,13 +146,15 @@ final class Guids
|
||||
]
|
||||
];
|
||||
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoCreate: true, autoBackup: true);
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoSave: false, autoCreate: true, autoBackup: true);
|
||||
|
||||
if (false === $file->has('guids') || false === is_array($file->get('guids'))) {
|
||||
$file->set('guids', []);
|
||||
}
|
||||
|
||||
$file->set('guids.' . count($file->get('guids', [])), $data)->persist();
|
||||
$guids = array_values($file->get('guids', []));
|
||||
$guids[] = $data;
|
||||
$file->set('guids', null)->set('guids', $guids)->persist();
|
||||
|
||||
return api_response(Status::OK, $data);
|
||||
}
|
||||
@@ -160,16 +162,15 @@ final class Guids
|
||||
#[Delete(self::URL . '/custom/{id:uuid}[/]', name: 'system.guids.custom.guid.remove')]
|
||||
public function custom_guid_remove(string $id): iResponse
|
||||
{
|
||||
$guids = ag($this->getData(), 'guids', []);
|
||||
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoCreate: true, autoBackup: true);
|
||||
|
||||
$data = [];
|
||||
$found = false;
|
||||
foreach ($guids as $index => $guid) {
|
||||
|
||||
foreach ($file->get('guids', []) as $index => $guid) {
|
||||
if ($guid['id'] === $id) {
|
||||
$data = $guid;
|
||||
$file->delete('guids.' . $index)->persist();
|
||||
$file->delete('guids.' . $index);
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
@@ -179,6 +180,22 @@ final class Guids
|
||||
return api_error(r("The GUID '{id}' is not found.", ['id' => $id]), Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
$guids = $file->get('guids', []);
|
||||
$file->set('guids', null)->set('guids', array_values($guids))->persist();
|
||||
|
||||
$linkRemoved = false;
|
||||
foreach ($file->get('links', []) as $index => $link) {
|
||||
if (ag($link, 'map.to') === ag($data, 'name')) {
|
||||
$file->delete('links.' . $index);
|
||||
$linkRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $linkRemoved) {
|
||||
$links = $file->get('links', []);
|
||||
$file->set('links', null)->set('links', array_values($links));
|
||||
}
|
||||
|
||||
$file->persist();
|
||||
|
||||
return api_response(Status::OK, $data);
|
||||
@@ -197,8 +214,8 @@ final class Guids
|
||||
);
|
||||
}
|
||||
|
||||
#[Put(self::URL . '/custom/{client:word}[/]', name: 'system.guids.custom.client.add')]
|
||||
public function custom_client_guid_add(iRequest $request, string $client): iResponse
|
||||
#[Put(self::URL . '/custom/{client:word}[/]', name: 'system.guids.custom.client.link.add')]
|
||||
public function custom_client_link_add(iRequest $request, string $client): iResponse
|
||||
{
|
||||
$params = DataUtil::fromRequest($request);
|
||||
|
||||
@@ -251,7 +268,7 @@ final class Guids
|
||||
'type' => $client,
|
||||
'map' => [
|
||||
'from' => $params->get('map.from'),
|
||||
'to' => $params->get('map.to'),
|
||||
'to' => $mapTo,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -268,21 +285,51 @@ final class Guids
|
||||
}
|
||||
}
|
||||
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoCreate: true, autoBackup: true);
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoSave: false, autoCreate: true, autoBackup: true);
|
||||
|
||||
if (false === $file->has('links') || false === is_array($file->get('links'))) {
|
||||
$file->set('links', []);
|
||||
}
|
||||
|
||||
$file->set('links.' . count($file->get('links', [])), $link)->persist();
|
||||
$links = array_values($file->get('links', []));
|
||||
$links[] = $link;
|
||||
|
||||
$file->set('links', null)->set('links', $links)->persist();
|
||||
|
||||
return api_response(Status::OK, $link);
|
||||
}
|
||||
|
||||
#[Delete(self::URL . '/custom/{client:word}/{index:number}[/]', name: 'system.guids.custom.client.remove')]
|
||||
public function custom_client_guid_remove(iRequest $request): iResponse
|
||||
#[Delete(self::URL . '/custom/{client:word}/{id:uuid}[/]', name: 'system.guids.custom.client.remove')]
|
||||
public function custom_client_guid_remove(string $client, string $id): iResponse
|
||||
{
|
||||
return api_response(Status::OK, $request->getParsedBody());
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoCreate: true, autoBackup: true);
|
||||
$refIndex = $refData = null;
|
||||
|
||||
foreach ($file->get('links', []) as $i => $link) {
|
||||
if (ag($link, 'type') !== $client || ag($link, 'id') !== $id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$refIndex = $i;
|
||||
$refData = $link;
|
||||
break;
|
||||
}
|
||||
|
||||
if (null === $refIndex) {
|
||||
return api_error(r("The client '{client}' link id '{id}' is not found.", [
|
||||
'client' => $client,
|
||||
'id' => $id
|
||||
]), Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
$file->delete('links.' . $refIndex);
|
||||
|
||||
$links = $file->get('links', []);
|
||||
$file->set('links', null)
|
||||
->set('links', array_values($links))
|
||||
->persist();
|
||||
|
||||
return api_response(Status::OK, $refData);
|
||||
}
|
||||
|
||||
#[Get(self::URL . '/custom/{client:word}/{index:number}[/]', name: 'system.guids.custom.client.guid.view')]
|
||||
@@ -325,18 +372,22 @@ final class Guids
|
||||
|
||||
private function validateName(string $name): void
|
||||
{
|
||||
$name = after($name, 'guid_');
|
||||
$guidName = after($name, 'guid_');
|
||||
|
||||
if (false === preg_match('/^[a-z0-9_]+$/i', $name)) {
|
||||
if (false === preg_match('/^[a-z0-9_]+$/i', $guidName)) {
|
||||
throw new InvalidArgumentException('Name must be alphanumeric and underscores only.');
|
||||
}
|
||||
|
||||
if (strtolower($name) !== $name) {
|
||||
if (strtolower($guidName) !== $guidName) {
|
||||
throw new InvalidArgumentException('Name must be lowercase.');
|
||||
}
|
||||
|
||||
if (str_contains($name, ' ')) {
|
||||
if (str_contains($guidName, ' ')) {
|
||||
throw new InvalidArgumentException('Name must not contain spaces.');
|
||||
}
|
||||
|
||||
if (true === array_key_exists($name, Guid::getSupported())) {
|
||||
throw new InvalidArgumentException(r("GUID name '{name}' is already in use.", ['name' => $name]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +174,10 @@ final class PlexGuid implements iGuid
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== ($replace = ag($map, 'replace', null))) {
|
||||
if (null !== ($replace = ag($map, 'options.replace', null))) {
|
||||
if (false === is_array($replace)) {
|
||||
$this->logger->warning(
|
||||
"Ignoring 'links.{key}'. replace value must be an object. '{given}' is given.",
|
||||
"Ignoring 'links.{key}'. options.replace value must be an object. '{given}' is given.",
|
||||
[
|
||||
'key' => $key,
|
||||
'given' => get_debug_type($replace),
|
||||
@@ -190,14 +190,17 @@ final class PlexGuid implements iGuid
|
||||
$to = ag($replace, 'to', null);
|
||||
|
||||
if (empty($from) || false === is_string($from)) {
|
||||
$this->logger->warning("Ignoring 'links.{key}'. replace.from field is empty or not a string.", [
|
||||
'key' => $key,
|
||||
]);
|
||||
$this->logger->warning(
|
||||
"Ignoring 'links.{key}'. options.replace.from field is empty or not a string.",
|
||||
[
|
||||
'key' => $key,
|
||||
]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === is_string($to)) {
|
||||
$this->logger->warning("Ignoring 'links.{key}'. replacer.to field is not a string.", [
|
||||
$this->logger->warning("Ignoring 'links.{key}'. options.replace.to field is not a string.", [
|
||||
'key' => $key,
|
||||
]);
|
||||
continue;
|
||||
|
||||
@@ -86,6 +86,10 @@ final class IndexCommand extends Command
|
||||
'force-reindex' => (bool)$input->getOption('force-reindex'),
|
||||
]);
|
||||
|
||||
if ($input->getOption('force-reindex')) {
|
||||
$output->writeln('<info>Indexes have been recreated successfully.</info>');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,10 +206,10 @@ class PlexGuidTest extends TestCase
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'Value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
'Assert link value is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'links.0.replace', 'foo');
|
||||
$yaml = ag_set($yaml, 'links.0.options.replace', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
@@ -217,23 +217,23 @@ class PlexGuidTest extends TestCase
|
||||
'Assert replace key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'links.0.replace', []);
|
||||
$yaml = ag_set($yaml, 'links.0.options.replace', []);
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'replace.from field is empty or not a string.', true),
|
||||
$this->logged(Level::Warning, 'options.replace.from field is empty or not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'links.0.replace.from', 'foo');
|
||||
$yaml = ag_set($yaml, 'links.0.options.replace.from', 'foo');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'replacer.to field is not a string.', true),
|
||||
$this->logged(Level::Warning, 'options.replace.to field is not a string.', true),
|
||||
'Assert to field is a string.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'links.0.replace.to', 'bar');
|
||||
$yaml = ag_set($yaml, 'links.0.options.replace.to', 'bar');
|
||||
file_put_contents($tmpFile, Yaml::dump($yaml));
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertCount(0, $this->handler->getRecords(), "There should be no error messages logged.");
|
||||
@@ -252,7 +252,7 @@ class PlexGuidTest extends TestCase
|
||||
$this->getClass()->parseGUIDFile($tmpFile);
|
||||
$this->assertTrue(
|
||||
$this->logged(Level::Warning, 'map value must be an object.', true),
|
||||
'Assert replace key is an object.'
|
||||
'Assert map key is an object.'
|
||||
);
|
||||
|
||||
$yaml = ag_set($yaml, 'links.0.map', []);
|
||||
|
||||
Reference in New Issue
Block a user