diff --git a/FAQ.md b/FAQ.md index 53861ff2..d07d4e04 100644 --- a/FAQ.md +++ b/FAQ.md @@ -516,7 +516,7 @@ Click `Save Changes` > > If you use multiple plex servers and use the same PlexPass account for all of them, You have to add each backend > using the same method above, while enabling `limit webhook events to` `selected user` and `backend unique id`. -> Essentially, this method replaced the old unified webhook.token for backends. +> Essentially, this method replaced the old unified webhook token for backends. ----- @@ -625,45 +625,37 @@ https://watchstate.example.org { ### WS_API_AUTO -The purpose of this environment variable is to automate the configuration process. It's mainly used for people who uses -many browsers -to access the `WebUI` and want to automate the configuration process. as it's requires the API settings to be configured -before it -can be used. This environment variable can be enabled by setting `WS_API_AUTO=true` in `${WS_DATA_PATH}/config/.env`. +The purpose of this environment variable is to automate the configuration process. It's mainly used for people who use +many browsers to access the `WebUI` and want to automate the configuration process. as it's requires the API settings to +be configured before it can be used. This environment variable can be enabled by setting `WS_API_AUTO=true` in +`${WS_DATA_PATH}/config/.env`. #### Why you should use it? You normally should not use it, as it's a **GREAT SECURITY RISK**. However, if you are using the tool in a secure -environment -and not worried about exposing your API key, you can use it to automate the configuration process. +environment and not worried about exposing your API key, you can use it to automate the configuration process. #### Why you should not use it? Because, by exposing your API key, you are also exposing every data you have in the tool. This is a **GREAT SECURITY -RISK**, -any person or bot that are able to access the `WebUI` will also be able to visit `/v1/api/system/auto` and get your API -key. And with this key -they can do anything they want with your data. including viewing your media servers api keys. - -So, please while we have this option available, we strongly recommend not to use it if `WatchState` is exposed to the -internet. +RISK**, any person or bot that are able to access the `WebUI` will also be able to visit `/v1/api/system/auto` and get +your API key. And with this key they can do anything they want with your data. including viewing your media servers API +keys. So, please while we have this option available, we strongly recommend not to use it if `WatchState` is exposed to +the internet. > [!IMPORTANT] > This environment variable is **GREAT SECURITY RISK**, and we strongly recommend not to use it if `WatchState` is -> exposed to the internet. -> I cannot stress this enough, please do not use it unless you are in a secure environment. +> exposed to the internet. I cannot stress this enough, please do not use it unless you are in a secure environment. --- ### How to disable the included cache server and use external cache server? Set this environment variable in your `compose.yaml` file `DISABLE_CACHE` with value of `1`. to use external redis -server -you need to alter the value of `WS_CACHE_URL` environment variable. the format for this variable -is `redis://host:port?password=auth&db=db_num`, -for example to use redis from another container you could use something -like `redis://172.23.1.10:6379?password=my_secert_password&db=8`. -We only support `redis` and API compatible alternative. +server you need to alter the value of `WS_CACHE_URL` environment variable. the format for this variable is +`redis://host:port?password=auth&db=db_num`, for example to use redis from another container you could use something +like `redis://172.23.1.10:6379?password=my_secert_password&db=8`. We only support `redis` and API compatible +alternative. Once that done, restart the container. @@ -784,14 +776,14 @@ their server, You can follow these steps. #### Requirements -* [PHP 8.3](http://https://www.php.net/downloads.php) with both the `CLI` and `fpm` mode. +* [PHP 8.4](http://https://www.php.net/downloads.php) with both the `CLI` and `fpm` mode. * PHP Extensions `pdo`, `pdo-sqlite`, `mbstring`, `json`, `ctype`, `curl`, `redis`, `sodium` and `simplexml`. * [Composer](https://getcomposer.org/download/) for dependency management. * [Redis-server](https://redis.io/) for caching or a compatible implementation that works with [php-redis](https://github.com/phpredis/phpredis). * [Caddy](https://caddyserver.com/) for frontend handling. However, you can use whatever you like. As long as it has support for fastcgi. -* [nodejs v20+](https://nodejs.org/en/download/) for `WebUI` compilation. +* [Node.js v20+](https://nodejs.org/en/download/) for `WebUI` compilation. #### Installation @@ -903,7 +895,7 @@ and treat them as not found. This helps with slow stat calls in network shares, or cloud storage. Everytime we do a stat call we cache it for 1 hour, so if we have multiple records reporting the same path, we only do -the stat check once. +the stat check once. Assuming all your media backends are using same path for the media files. --- @@ -957,7 +949,8 @@ 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? 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. +like using GUI, as such You can extend the parser by creating new file at `/config/config/guid.yaml` with the following +content. ```yaml # (Optional) The version of the guid file. If omitted, it will default to the latest version. @@ -1018,8 +1011,32 @@ As you can see from the config, it's roughly how we expected it to be. The `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 +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:index --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. + +--- + +### Sync watch progress. + +In order to sync the watch progress between media backends, you need to enable the following environment variable +`WS_SYNC_PROGRESS` in `(WebUI) > Env` page or via the cli using the following command: + +```bash +$ docker exec -ti watchstate console system:env -k WS_SYNC_PROGRESS -e true +``` + +For best experience, you should enable the `Webhooks` feature for the media backends you want to sync the watch +progress, +however, if you are unable to do so, the `Tasks > import` task will also generate progress watch events. However, it's +not as reliable as the `Webhooks` or as fast. using `Webhooks` is the recommended way and offers the best experience. + +To check if there is any watch progress events being registered, You can go to `(WebUI) > More > Events` and check +`on_progress` events, if you are seeing those, this means the progress is being synced. Check the `Tasks logs` to see +the event log. + + diff --git a/NEWS.md b/NEWS.md index bbfdcb03..bb58bce8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,16 @@ # Old Updates +### 2024-09-14 + +We have recently added support for extending WatchState with more GUIDs, as of now, the support for it is done via +editing a`/config/guid.yaml` file in the config directory. We plan to hopefully add management via WebUI in near the +future. 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). + +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. diff --git a/README.md b/README.md index c4b5e811..ffc4ffbb 100644 --- a/README.md +++ b/README.md @@ -9,22 +9,22 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers. ## Updates +### 2024-12-30 + +We have removed the old environment variables `WS_CRON_PROGRESS` and `WS_CRON_PUSH` in favor of the new ones +`WS_SYNC_PROGRESS` and `WS_PUSH_ENABLED`. please update your environment variables accordingly. We have also added +new FAQ entry about watch progress syncing via [this link](FAQ.md#sync-watch-progress). + ### 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 +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 -editing a`/config/guid.yaml` file in the config directory. We plan to hopefully add management via WebUI in near the future. 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). - -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. - --- Refer to [NEWS](NEWS.md) for old updates. @@ -38,7 +38,8 @@ Refer to [NEWS](NEWS.md) for old updates. * Search your backend for `title` or `item id`. * Display and filter your play state. Can be exported as `yaml` or `json`. * Check if your media servers reporting same data via the parity command. -* Track your watch progress via webhooks. +* Sync your watch progress via webhooks or scheduled tasks. +* Check if your media backends have stale references to old files. ---- diff --git a/config/config.php b/config/config.php index 1aa3cbd0..5493a617 100644 --- a/config/config.php +++ b/config/config.php @@ -74,10 +74,10 @@ return (function () { 'header' => (string)env('WS_TRUST_HEADER', 'X-Forwarded-For'), ], 'sync' => [ - 'progress' => (bool)env('WS_SYNC_PROGRESS', (bool)env('WS_CRON_PROGRESS', false)), + 'progress' => (bool)env('WS_SYNC_PROGRESS', false), ], 'push' => [ - 'enabled' => (bool)env('WS_PUSH_ENABLED', (bool)env('WS_CRON_PUSH', false)), + 'enabled' => (bool)env('WS_PUSH_ENABLED', false), ], ]; diff --git a/src/API/System/Env.php b/src/API/System/Env.php index de302d09..ff87da60 100644 --- a/src/API/System/Env.php +++ b/src/API/System/Env.php @@ -165,10 +165,20 @@ final class Env return []; } - private function setType($spec, mixed $value): mixed + private function setType($spec, mixed $value): string|int|bool|float { + if ('bool' === ag($spec, 'type', 'string')) { + if (is_bool($value)) { + return $value; + } + if (true === in_array(strtolower((string)$value), ['true', '1', 'yes', 'on'], true)) { + return true; + } + + return false; + } + return match (ag($spec, 'type', 'string')) { - 'bool' => (bool)$value, 'int' => (int)$value, 'float' => (float)$value, default => (string)$value, diff --git a/src/Commands/System/EnvCommand.php b/src/Commands/System/EnvCommand.php index 19c4d002..f1bd525d 100644 --- a/src/Commands/System/EnvCommand.php +++ b/src/Commands/System/EnvCommand.php @@ -148,7 +148,7 @@ final class EnvCommand extends Command { $key = strtoupper($input->getOption('key')); - if (!$input->getOption('set') && !$input->getOption('delete')) { + if (null === $input->getOption('set') && !$input->getOption('delete')) { $output->writeln((string)env($key, '')); return self::SUCCESS; } diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index f87fa375..27f47ffd 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -1150,6 +1150,7 @@ if (false === function_exists('isValidURL')) { function isValidURL(string $url): bool { // RFC 3987 For absolute IRIs (internationalized): + /** @noinspection all */ return (bool)@preg_match( '/^[a-z](?:[-a-z0-9\+\.])*:(?:\/\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:])*@)?(?:\[(?:(?:(?:[0-9a-f]{1,4}:){6}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|::(?:[0-9a-f]{1,4}:){5}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:[0-9a-f]{1,4}:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|v[0-9a-f]+[-a-z0-9\._~!\$&\'\(\)\*\+,;=:]+)\]|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}|(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=@])*)(?::[0-9]*)?(?:\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@]))*)*|\/(?:(?:(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@]))+)(?:\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@]))*)*)?|(?:(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@]))+)(?:\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@]))*)*|(?!(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@])))(?:\?(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@])|[\x{E000}-\x{F8FF}\x{F0000}-\x{FFFFD}|\x{100000}-\x{10FFFD}\/\?])*)?(?:\#(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!\$&\'\(\)\*\+,;=:@])|[\/\?])*)?$/iu', $url @@ -1461,15 +1462,19 @@ if (!function_exists('APIRequest')) { $server['QUERY_STRING'] = http_build_query($query); } - $response = $initializer->http( - $creator->fromArrays( - server: $server, - headers: $headers, - get: $query, - post: $json, - body: $body - )->withAttribute('INTERNAL_REQUEST', true) - ); + $request = $creator->fromArrays( + server: $server, + headers: $headers, + get: $query, + post: $json, + body: $body + )->withAttribute('INTERNAL_REQUEST', true); + + if (null !== ($callback = ag($opts, 'callback'))) { + $callback($request); + } + + $response = $initializer->http($request); $statusCode = Status::tryFrom($response->getStatusCode()) ?? Status::SERVICE_UNAVAILABLE;