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. 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/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..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; @@ -233,7 +234,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'), @@ -270,31 +271,33 @@ 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); + } + /** - * @TODO: Disabled the de-duplication for now. * 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 ( - // 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; + } return new Response(status: true, response: $list); } @@ -440,7 +443,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']); }