Fully implemented Custom GUIDs support and added WebUI page to manage it.

This commit is contained in:
Abdulmhsen B. A. A.
2024-10-07 19:43:38 +03:00
parent 1b7ef714c0
commit 9845cc300b
12 changed files with 520 additions and 413 deletions

19
FAQ.md
View File

@@ -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
View File

@@ -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,

View File

@@ -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.

View File

@@ -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')())

View File

@@ -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()
}
})

View File

@@ -7,18 +7,14 @@
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="card-content">
<div class="field">
<label class="label is-unselectable" for="form_guid_name">Name</label>
<div class="control has-icons-left">
@@ -161,10 +157,9 @@
must be a minimum of 1 incorrect value.</span>
</p>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<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">
@@ -173,7 +168,7 @@
</span>
</button>
</div>
<div class="card-footer-item">
<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>
@@ -182,17 +177,8 @@
</button>
</div>
</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([])
@@ -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

View File

@@ -7,18 +7,16 @@
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">
@@ -128,8 +126,8 @@
</template>
</template>
<div class="card-footer">
<div class="card-footer-item">
<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}">
@@ -139,7 +137,7 @@
</span>
</button>
</div>
<div class="card-footer-item">
<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>
@@ -148,18 +146,8 @@
</button>
</div>
</div>
</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>

View File

@@ -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>&nbsp;</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>&nbsp;</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 () => {
try {
isLoading.value = true
data.value = await parse_api_response(await request(`/system/guids/custom`))
try {
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'})
if (!response.ok) {
const result = await parse_api_response(response)
if (response.ok) {
data.value.guids.splice(index, 1)
notification('success', 'Success', result.message)
} else {
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>

View File

@@ -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]));
}
}
}

View File

@@ -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.", [
$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;

View File

@@ -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;
}
}

View File

@@ -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', []);