From 76cdaf6e6da348ba313fcb7f4d387767357aea1e Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 21 Feb 2026 00:36:47 +0300 Subject: [PATCH 1/4] refactor: update de-dup logic. --- frontend/app/pages/tools/sub_users.vue | 1 - frontend/bun.lock | 6 ++-- frontend/package.json | 2 +- src/Backends/Plex/Action/GetUsersList.php | 41 +++++++++++------------ 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/frontend/app/pages/tools/sub_users.vue b/frontend/app/pages/tools/sub_users.vue index 5cfd8f95..8ac19d95 100644 --- a/frontend/app/pages/tools/sub_users.vue +++ b/frontend/app/pages/tools/sub_users.vue @@ -412,7 +412,6 @@ import { computed, nextTick, onMounted, ref, toRaw } from 'vue'; import { useStorage } from '@vueuse/core'; import { navigateTo, useRoute } from '#app'; import moment from 'moment'; -import draggable from 'vuedraggable'; import { makeConsoleCommand, notification, parse_api_response, request } from '~/utils'; import Message from '~/components/Message.vue'; import { useDialog } from '~/composables/useDialog'; diff --git a/frontend/bun.lock b/frontend/bun.lock index a0b98160..27c361fb 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -27,7 +27,7 @@ "vue": "^3.5.28", "vue-router": "^5.0.2", "vue-toastification": "^2.0.0-rc.5", - "vuedraggable": "^2.24.3", + "vuedraggable": "^4.1.0", }, "devDependencies": { "@types/bun": "^1.3.9", @@ -1518,7 +1518,7 @@ "smob": ["smob@1.6.1", "", {}, "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g=="], - "sortablejs": ["sortablejs@1.10.2", "", {}, "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="], + "sortablejs": ["sortablejs@1.14.0", "", {}, "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -1692,7 +1692,7 @@ "vue-tsc": ["vue-tsc@3.2.4", "", { "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.4" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g=="], - "vuedraggable": ["vuedraggable@2.24.3", "", { "dependencies": { "sortablejs": "1.10.2" } }, "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g=="], + "vuedraggable": ["vuedraggable@4.1.0", "", { "dependencies": { "sortablejs": "1.14.0" }, "peerDependencies": { "vue": "^3.0.1" } }, "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], diff --git a/frontend/package.json b/frontend/package.json index 0b5e4feb..be606422 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,7 @@ "vue": "^3.5.28", "vue-router": "^5.0.2", "vue-toastification": "^2.0.0-rc.5", - "vuedraggable": "^2.24.3", + "vuedraggable": "^4.1.0", "@nuxt/eslint": "^1.15.1", "@nuxt/eslint-config": "^1.15.1" }, diff --git a/src/Backends/Plex/Action/GetUsersList.php b/src/Backends/Plex/Action/GetUsersList.php index c94720d8..40cc5166 100644 --- a/src/Backends/Plex/Action/GetUsersList.php +++ b/src/Backends/Plex/Action/GetUsersList.php @@ -271,30 +271,29 @@ private function processHomeUsers( ])); /** - * @TODO: Disabled the de-duplication for now. * De-duplicate users. * Plex in their infinite wisdom sometimes return home users as external users. */ - // foreach ($external as $user) { - // if ( - // null !== ($homeUser = array_find( - // $list, - // static fn($u) => (int) $u['id'] === (int) $user['id'] && $u['name'] === $user['name'], - // )) - // ) { - // $opts[Options::LOG_TO_WRITER](r("Skipping external user '{name}' with id '{id}' because match a home user with id '{userId}' and name '{userName}'.", [ - // 'id' => ag($user, 'id'), - // 'name' => ag($user, 'name'), - // 'userId' => ag($homeUser, 'id'), - // 'userName' => ag($homeUser, 'name'), - // ])); - // continue; - // } - - // $list[] = $user; - // } - - array_push($list, ...$external); + foreach ($external as $user) { + if ( + null !== ($homeUser = array_find( + $list, + static fn($u) => (int) $u['id'] === (int) $user['id'] && $u['name'] === $user['name'], + )) + ) { + $opts[Options::LOG_TO_WRITER](r("Skipping external user '{name}' with id '{id}' because match a home user with id '{userId}' and name '{userName}'.", [ + 'id' => ag($user, 'id'), + 'name' => ag($user, 'name'), + 'userId' => ag($homeUser, 'id'), + 'userName' => ag($homeUser, 'name'), + ])); + continue; + } + + $list[] = $user; + } + + // array_push($list, ...$external); return new Response(status: true, response: $list); } From edfd49524f5bb759d596179492d7678cdb20625f Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 21 Feb 2026 01:26:43 +0300 Subject: [PATCH 2/4] refactor: normalize user names in GetUsersList --- src/Backends/Plex/Action/GetUsersList.php | 6 ++---- tests/Backends/Plex/GetUsersListTest.php | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Backends/Plex/Action/GetUsersList.php b/src/Backends/Plex/Action/GetUsersList.php index 40cc5166..8e9db31e 100644 --- a/src/Backends/Plex/Action/GetUsersList.php +++ b/src/Backends/Plex/Action/GetUsersList.php @@ -233,7 +233,7 @@ private function processHomeUsers( 'id' => ag($data, 'id'), 'type' => 'H', 'uuid' => ag($data, 'uuid'), - 'name' => ag($data, ['friendlyName', 'username', 'title', 'email', 'id'], '??'), + 'name' => normalize_name(ag($data, ['friendlyName', 'username', 'title', 'email', 'id'], '??')), 'admin' => (bool) ag($data, 'admin'), 'guest' => (bool) ag($data, 'guest'), 'restricted' => (bool) ag($data, 'restricted'), @@ -293,8 +293,6 @@ private function processHomeUsers( $list[] = $user; } - // array_push($list, ...$external); - return new Response(status: true, response: $list); } @@ -439,7 +437,7 @@ private function processExternalUsers(iResponse $response, Context $context, iUr 'id' => (int) ag($user, 'id'), 'type' => 'E', 'uuid' => 1 === $uuidStatus ? ag($matches, 'uuid') : ag($user, 'invited_user'), - 'name' => ag($user, ['username', 'title', 'email', 'id'], '??'), + 'name' => normalize_name(ag($user, ['username', 'title', 'email', 'id'], '??')), 'admin' => false, 'guest' => 1 !== (int) ag($user, 'home'), 'restricted' => 1 === (int) ag($user, 'restricted'), diff --git a/tests/Backends/Plex/GetUsersListTest.php b/tests/Backends/Plex/GetUsersListTest.php index 463b06fa..2eb24565 100644 --- a/tests/Backends/Plex/GetUsersListTest.php +++ b/tests/Backends/Plex/GetUsersListTest.php @@ -62,7 +62,7 @@ public function test_get_users_list_home_users(): void $result = $action($context); $this->assertTrue($result->isSuccessful()); - $this->assertSame('Test User', $result->response[0]['name']); + $this->assertSame('test_user', $result->response[0]['name'], 'Name normalization failed'); $this->assertTrue($result->response[0]['admin']); } From 18a6ef579900676c4af6a7f8108a4920d3fa3fda Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 21 Feb 2026 02:10:17 +0300 Subject: [PATCH 3/4] feat: add configuration option to disable de-duplication for Plex clients --- config/config.php | 4 ++- config/env.spec.php | 33 ++++++++++++++--------- src/Backends/Plex/Action/GetUsersList.php | 8 +++++- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/config/config.php b/config/config.php index ee4f07c0..11f0fca4 100644 --- a/config/config.php +++ b/config/config.php @@ -244,7 +244,9 @@ ]; $config['clients'] = [ - strtolower(PlexClient::CLIENT_NAME) => [], + strtolower(PlexClient::CLIENT_NAME) => [ + 'disable_dedup' => (bool) env('WS_CLIENTS_PLEX_DISABLE_DEDUP', false), + ], strtolower(EmbyClient::CLIENT_NAME) => [], strtolower(JellyfinClient::CLIENT_NAME) => [ 'fix_played' => (bool) env('WS_CLIENTS_JELLYFIN_FIX_PLAYED', false), diff --git a/config/env.spec.php b/config/env.spec.php index 97fa8c0e..242e0018 100644 --- a/config/env.spec.php +++ b/config/env.spec.php @@ -260,13 +260,13 @@ throw new ValidationException('Invalid progress threshold. Must be a number.'); } - $cmp = (int)$value; + $cmp = (int) $value; if (0 !== $cmp && $cmp < 180) { throw new ValidationException('Invalid progress threshold. Must be at least 180 seconds.'); } - return (string)$value; + return (string) $value; }, ], [ @@ -294,7 +294,7 @@ if (false === is_valid_url($value)) { throw new ValidationException('Invalid remote logger URL. Must be a valid URL.'); } - return (string)$value; + return (string) $value; }, 'mask' => true, ], @@ -316,10 +316,10 @@ if (false === is_valid_name($value)) { throw new ValidationException( - 'Invalid username. Username can only contains [lower case a-z, 0-9 and _].' + 'Invalid username. Username can only contains [lower case a-z, 0-9 and _].', ); } - return (string)$value; + return (string) $value; }, 'mask' => true, 'protected' => true, @@ -337,15 +337,16 @@ $prefix = Config::get('password.prefix', 'ws_hash@:'); if (true === str_starts_with($value, $prefix)) { - return (string)$value; + return (string) $value; } try { - return $prefix . password_hash( - $value, - Config::get('password.algo'), - Config::get('password.options', []) - ); + return $prefix + . password_hash( + $value, + Config::get('password.algo'), + Config::get('password.options', []), + ); } catch (ValueError $e) { throw new ValidationException('Invalid password. Password hashing failed.', $e->getCode(), $e); } @@ -396,13 +397,13 @@ throw new ValidationException('Invalid minimum progress. Must be a number.'); } - $cmp = (int)$value; + $cmp = (int) $value; if ($cmp < 60) { throw new ValidationException('Invalid minimum progress. Must be at least 60 seconds.'); } - return (string)$value; + return (string) $value; }, ], [ @@ -411,6 +412,12 @@ 'description' => 'Enable partial fix for Jellyfin marking items as played.', 'type' => 'bool', ], + [ + 'key' => 'WS_CLIENTS_PLEX_DISABLE_DEDUP', + 'config' => 'clients.plex.disable_dedup', + 'description' => 'Disable de-duplication of plex users.', + 'type' => 'bool', + ], ]; $validateCronExpression = function (string $value, array $spec = []): string { diff --git a/src/Backends/Plex/Action/GetUsersList.php b/src/Backends/Plex/Action/GetUsersList.php index 8e9db31e..1af97ca6 100644 --- a/src/Backends/Plex/Action/GetUsersList.php +++ b/src/Backends/Plex/Action/GetUsersList.php @@ -9,6 +9,7 @@ use App\Backends\Common\Error; use App\Backends\Common\Levels; use App\Backends\Common\Response; +use App\Libs\Config; use App\Libs\Container; use App\Libs\Enums\Http\Status; use App\Libs\Exceptions\Backends\InvalidArgumentException; @@ -270,9 +271,14 @@ private function processHomeUsers( 'count' => count($list), ])); + if (true === (bool) Config::get('clients.plex.disable_dedup', false)) { + array_push($list, ...$external); + return new Response(status: true, response: $list); + } + /** * De-duplicate users. - * Plex in their infinite wisdom sometimes return home users as external users. + * Plex in their infinite wisdom sometimes return home users as external users and vice versa. */ foreach ($external as $user) { if ( From 01365b7c7f98ee265a6377d8255546c41e43c777 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 21 Feb 2026 02:36:13 +0300 Subject: [PATCH 4/4] docs: add FAQ entry for missing Plex users and deduplication option --- FAQ.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/FAQ.md b/FAQ.md index 992eaf12..e24df8ca 100644 --- a/FAQ.md +++ b/FAQ.md @@ -615,3 +615,11 @@ creation failing to load. To fix the issue it's recommended to change the backen * For Plex: `Library/Application Support/Plex Media Server/Preferences.xml` key: `ProcessedMachineIdentifier`. Those values need to be unique per instance. + +--- + +# Some Plex users are not showing up in the user list? + +Sometimes, the plex user list may not fully load all users, Please **Env** page and add the following environment variable: +`WS_CLIENTS_PLEX_DISABLE_DEDUP` and turn it on. This will disable the deduplication logic for plex users. Once you enable this option please restart watchstate +and try again. If the user still doesn't show up, please open a bug report with the relevant logs and we will investigate the issue.