diff --git a/FAQ.md b/FAQ.md index ae0709bb..ddb803d6 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,22 +1,23 @@ # FAQ -### Q: How to update watched status for newly added server without overwriting my current watch state? +### Q: How to update play state for newly added media backend without overwriting my current play state? -First, add the server and when asked, answer `n` for allow import from this server. when you finish, run the following +First, add the media backend and when asked, answer `n` for allow import from this server. when you finish, run the +following command: ```bash -$ docker exec -ti watchstate console state:export -vvrm --ignore-date --force-full --servers-filter [SERVER_NAME] +$ docker exec -ti watchstate console state:export -vvr --ignore-date --force-full --servers-filter [SERVER_NAME] ``` -this command will force export your current database state back to the selected server. If the operation is successful -you can then enable the import feature if you want. +this command will force export your current local play state to the selected media backend. If the operation is +successful you can then enable the import feature if you want. --- ### Q: Is there support for Multi-user setup? -No, The database design centered on single user. However, It's possible to run container for each user. +No, The tool is designed as to work for single user. However, It's possible to run container for each user. Note: for Plex managed users run the following command to extract each managed user token. @@ -28,19 +29,20 @@ For jellyfin/emby, you can use same api-token and just replace the userId. --- -### Q: Sometimes episodes or movies don't make it to webhook server? +### Q: Sometimes newly added episodes or movies don't make it to webhook server? -As stated in webhook limitation sometimes servers don't make it easy to receive those events, as such, to complement -webhooks, its good idea enable the scheduled tasks of import/export and let them run once in a while to re-sync the -state of map of server guids, as webhook push support rely entirely on local data of each server. +As stated in webhook limitation section sometimes media backends don't make it easy to receive those events, as such, to +complement webhooks, its good idea enable the scheduled tasks of import/export and let them run once in a while to +remap the data. ---- ### Q: Can this tool run without docker? Yes, if you have the required PHP version and the needed extensions. to run this tool you need the following `php8.1`, -and the following extensions `php8.1-pdo`, `php8.1-mbstring`, `php8.1-ctype`, `php8.1-curl`, `php8.1-sqlite3` and -[composer](https://getcomposer.org/). once you have the required runtime for first time run: +and `php8.1-fpm` and the following extensions `php8.1-pdo`, `php8.1-mbstring`, `php8.1-ctype`, `php8.1-curl`, +`php8.1-sqlite3` and[composer](https://getcomposer.org/). once you have the required runtime dependencies, for first +time run: ```bash cd ~/watchstate @@ -55,31 +57,41 @@ $ php console The app should save your data into `./var` directory. If you want to change the directory you can export the environment variable `WS_DATA_PATH` for console and browser. you can add a file called `.env` in main tool directory with the -environment variables. take look at the files inside docker directory to know how to run the scheduled tasks and ofc if -you want a webhook support you would need a frontend proxy for `php8.1-fpm` like nginx, caddy or apache. +environment variables. take look at the files inside `docker/files` directory to know how to run the scheduled tasks and +ofc if you want a webhook support you would need a frontend proxy for `php8.1-fpm` like nginx, caddy or apache. --- ### Q: Some records keep getting updated even when the state did not change? -This most likely relates to incorrect GUID reported from servers, in our testing we noticed that at least few hundred -records in thetvdb that get reported by plex have incorrect imdb, which in turns conflicts sometimes with jellyfin/emby -there is nothing we can do beside have the problematic records reported to thetvdb site mods to fix their db entries. +This most likely means your media backends have conflicting external ids for the reported items, and thus triggering an +update as the tool see different external ids on each event from the backends. In our testing we noticed that at least +few hundred records in thetvdb that get reported by plex have incorrect imdb external id, which in turns conflicts +sometimes with jellyfin/emby there is nothing we can do beside have the problematic records reported to thetvdb site +mods to fix their db entries. ---- -### Q: I keep on seeing "No supported GUID was given." in logs? +### Q: I keep on seeing "Ignoring 'XXX'. No valid/supported external ids." in logs? -This most likely means, the item being reported by the media server is not matched, in jellyfin/emby edit metadata and -make sure there are External IDs listed in the metadata. like tvdb/imdb etc. For plex click the (...), and click Fix +This most likely means that the episode/movie is not matched in your backend server, In jellyfin/emby edit metadata and +make sure there are external ids listed in the metadata. like tvdb/imdb etc. For plex click the (...), and click Fix match. +If this relates to plex, and you are using custom agents then please refer +to [Supported Custom plex agents](#q-can-this-tool-work-with-alternative-plex-agents) + --- -### Q: I enabled strict user match to allow only my user to update the state, webhook requests are failing? +### Q: I enabled strict user match to allow only my user to update the play state, and now webhook requests are failing to be processed? -If this relates to jellyfin, then please make sure you have selected "Send All Properties (ignores template)", if it's -plex and your account is main account then update the `user` to `1` by running the following command, just change +#### For Jellyfin backend + +If this relates to jellyfin backend, then please make sure you have selected "Send All Properties (ignores template)". + +#### For Plex backend + +if your account is the admin account then update the `user` to `1` by running the following command, just change the `[SERVER_NAME]` to your server config name. ```bash @@ -91,25 +103,25 @@ $ docker exec -ti watchstate console servers:edit --key user --set 1 -- [SERVER_ ### Q: Does this tool require webhooks to work? No, You can use the task scheduler or on demand sync if you want. However, we recommend the webhook method as it's the -most efficient method to update watch state. +most efficient method to update play state. --- ### Q: When i use jellyfin, i sometimes see double events? -This likely a bug in the plugin [jf webhook. #113](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/113), -Just reload the page make sure there is only one watchstate event webhook. +This most likely a bug in the plugin [jf-webhook #113](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/113), +Just reload the page make sure there is only one added watchstate endpoint. --- ### Q: I keep on seeing "..., entity state is tainted." what does that means? -Tainted events are events that are not used to update the watch state, but they are interesting enough for us to keep -around for other benefits like updating the GUID mapping for items. It's normal do not worry about it. +Tainted events are events that are not used to update the play state, but are interesting enough for us to keep around +for other benefits like updating the external ids mapping for movies/episodes. It's normal do not worry about it. --- -### Q: How can I see the database history? +### Q: How can I see my play state list? ```bash $ docker exec -ti watchstate console db:list @@ -145,7 +157,7 @@ $ docker exec -ti watchstate console servers:edit --delete --key options.ignore --- -### Q: I get tired of writing the whole command everytime is there an easy way to do the commands? +### Q: I get tired of writing the whole command everytime is there an easy way run the commands? Since there is no way to access the command interface outside docker, you can create small shell script to at least omit part of command that you have to write for example create new file named @@ -159,9 +171,10 @@ after that you can do `ws command` for example, `ws db:list` --- -### Q: I am using media servers hosted behind TLS/SSL, and see errors related to http2 +### Q: I am using media backends hosted behind HTTPS, and see errors related to HTTP/2? -Sometimes there are problems related to http/2.0, so before reporting bug please try running the following command +Sometimes there are problems related to HTTP/2 in the underlying library we use, so before reporting bug please try +running the following command ```bash $ docker exec -ti watchstate console servers:edit --key options.client.http_version --set 1.0 -- [SERVER_NAME] @@ -169,12 +182,16 @@ $ docker exec -ti watchstate console servers:edit --key options.client.http_vers if it does not fix your problem, please open issue about it. +--- + +### Q: My sync operations are failing due to timeout can I increase that? + We use [symfony/httpClient](https://symfony.com/doc/current/http_client.html) internally, So any options available in [ configuration](https://symfony.com/doc/current/http_client.html#configuration) section, can be used under `options.client.` key for example if you want to increase the timeout you can do ```bash -$ docker exec -ti watchstate console servers:edit --key options.client.timeout --set 300 -- [SERVER_NAME] +$ docker exec -ti watchstate console servers:edit --key options.client.timeout --set 600 -- [SERVER_NAME] ``` --- @@ -203,10 +220,10 @@ These are the agents we support for plex media server. * tvdb://(id) `New Plex Agent` * imdb://(id) `New Plex Agent` * tmdb://(id) `New Plex Agent` -* com.plexapp.agents.imdb://(id)?lang=en `(Old plex agents)` -* com.plexapp.agents.tmdb://(id)?lang=en `(Old plex agents)` -* com.plexapp.agents.themoviedb://(id)?lang=en `(Old plex agents "id" can be movie id or series id)` -* com.plexapp.agents.thetvdb://(seriesId)?lang=en `(old plex agents)` -* com.plexapp.agents.xbmcnfo://(id)?lang=en `(xbmc nfo parser agent)` -* com.plexapp.agents.xbmcnfotv://(seriesId)?lang=en `(xbmc nfo agent for tv)` -* com.plexapp.agents.hama://(db)-(seriesId)?lang=en `(hama anime agent support anidb/tvdb subkeys only)` +* com.plexapp.agents.imdb://(id)?lang=en `(Lagecy plex agent "id" can be movie or series id)` +* com.plexapp.agents.tmdb://(id)?lang=en `(Lagecy plex agent "id" can be movie or series id)` +* com.plexapp.agents.themoviedb://(id)?lang=en `(Lagecy plex agent "id" can be movie or series id)` +* com.plexapp.agents.thetvdb://(seriesId)?lang=en `(Lagecy plex agent "id" can be movie or series id)` +* com.plexapp.agents.xbmcnfo://(id)?lang=en `(XBMC NFO parser agent, "id" refers to movie id in imdb)` +* com.plexapp.agents.xbmcnfotv://(id)?lang=en `(XBMC NFO parser agent for tv. "id" refers to series id)` +* com.plexapp.agents.hama://(db)-(id)?lang=en `(Anime agent "anidb, tvdb" as db source only. "id" refers to series id)` diff --git a/README.md b/README.md index ba5dafff..b2aae44e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # WatchState -WatchState is a CLI based tool to sync your watch state between different media servers, without relying on 3rd parties +WatchState is a CLI based tool to sync your watch state between different media backends, without relying on 3rd parties services, like trakt.tv, This tool support `Plex Media Server`, `Emby` and `Jellyfin` out of the box currently, with -plans for future expansion for other media servers. +plans for future expansion for other media backends. # Install @@ -15,10 +15,11 @@ services: image: arabcoders/watchstate:latest container_name: watchstate restart: unless-stopped + # For more environment variables please read at the bottom of this page. + # works for both global and container specific environment variables. environment: - # For more environment variables please read at the bottom of this page. - WS_UID: ${UID:-1000} # Set container operation user id. - WS_GID: ${GID:-1000} # Set container operation group id. + - WS_UID=${UID:-1000} # Set container user id. + - WS_GID=${GID:-1000} # Set container group id. ports: - "8081:80" # webhook listener port volumes: @@ -28,7 +29,7 @@ services: After creating your docker-compose file, start the container. ```bash -$ docker-compose up -d +$ docker-compose pull && docker-compose up -d ``` # First time @@ -45,31 +46,32 @@ $ docker exec -ti watchstate console help state:import --- -After starting the container, you have to add your media servers, to do so run the following command +After starting the container, you have to add your media backends, to do so run the following command: ```bash $ docker exec -ti watchstate console servers:manage --add -- [SERVER_NAME] ``` -This command will ask you for some questions to add your servers, you can run the command as many times as you want, if +This command will ask you for some questions to add your backend, you can run the command as many times as you want, if you want to edit the config again or if you made mistake just run the same command without `--add` flag. -After adding your servers, You should import your current watch state by running the following command. +After adding your backends, You should import your current watch state by running the following command. ```bash -$ docker exec -ti watchstate console state:import -vvrm +$ docker exec -ti watchstate console state:import -vvr ``` --- # Pulling watch state. -now that you have imported your watch state, you can stop manually running the command again. and rely on the webhooks -to update the watch state. To start receiving webhook events from servers you need to do few more steps. +now that you have imported your current play state, you can stop manually running the command, and rely on the tasks +scheduler and webhooks to keep update your play state. To start receiving webhook events from backends you need to do +few more steps. -### Enable webhooks events for specific server. +### Enable webhooks events for specific backend. -To see the server specific api key run the following command +To see the backend specific webhook api key run the following command: ```bash $ docker exec -ti watchstate console servers:view --servers-filter [SERVER_NAME] -- webhook.token @@ -85,8 +87,8 @@ $ docker exec -ti watchstate console servers:edit --regenerate-api-key -- [SERVE #### TIP: -If you have multiple plex servers and use the same plex account for all of them, you have to unify the API key, by -running the following command +If you have multiple plex servers and use the same PlexPass account for all of them, you have to unify the API key, by +running the following command: ```bash $ docker exec -ti watchstate console servers:unify plex @@ -101,23 +103,20 @@ information run. $ docker exec -ti watchstate console help servers:unify ``` -This command is not limited to plex, you can unify API key for all supported backend servers. - --- -If you don't want to use webhooks and want to rely only on scheduled task for importing, then set the value +If you don't want to/can't use webhooks and want to rely solely on task scheduler importing, then set the value of `WS_CRON_IMPORT` to `1`. By default, we run the import command every hour. However, you can change the scheduled task timer by adding another variable `WS_CRON_IMPORT_AT` and set its value to valid cron expression. for -example, `0 */2 * * *` it will run every two hours instead of 1 hour. beware, this operation is somewhat costly as it's -pulls the entire server library. +example, `0 */2 * * *` it will run every two hours instead of 1 hour. If your backends and this tool are not on same +server it might consume a lot of bandwidth as it's pulls the entire server library listing. --- #### TIP You should still have `WS_CRON_IMPORT` enabled as sometimes plex does not really report new items, or report them in a -way that is not compatible with the way we handle webhooks events. running the import command regularly helps keep -healthy `GUIDS <> serverInternalID mapping` relations. +way that is not compatible with the way we handle webhooks events. --- @@ -129,7 +128,7 @@ To manually export your watch state back to servers you can run the following co $ docker exec -ti watchstate console state:export -vvr ``` -to sync specific server/s, use the `--servers-filter` which accept comma seperated list of server names. +to sync specific server/s, use the `[-s, --servers-filter]` which accept comma seperated list of server names. ```bash $ docker exec -ti watchstate console state:export -vvr --servers-filter 'server1,server2' @@ -165,9 +164,10 @@ server { } ``` -### Adding webhook to server +### Adding webhook -to your server the url will be dependent on how you expose the server, but typically it will be like this: +To add webhook for your server the URL will be dependent on how you expose the tool http frontend, but typically it will +be like this: #### Webhook URL @@ -175,7 +175,8 @@ Via reverse proxy : `https://watchstate.domain.example/?apikey=[WEBHOOK_TOKEN]`. Directly to container: `https://localhost:8081/?apikey=[WEBHOOK_TOKEN]` -If your server support sending headers then omit the query parameter '?apikey=[WEBHOOK_TOKEN]', and add new this header +If your media backend support sending headers then omit the query parameter '?apikey=[WEBHOOK_TOKEN]', and add new this +header ```http request X-apikey: [WEBHOOK_TOKEN] @@ -185,19 +186,19 @@ it's more secure that way. #### [WEBHOOK_TOKEN] -Should match the server specific ``webhook.token`` value. Refer to the steps described -at **[Steps to enable webhook servers](#enable-webhooks-events-for-specific-server)**. +Should match the backend specific ``webhook.token`` value. Refer to the steps described +at **[Steps to enable webhook servers](#enable-webhooks-events-for-specific-backend)**. -# Configuring Media servers to send webhook events. +# Configuring media backends to send webhook events. #### Jellyfin (Free) go to your jellyfin dashboard > plugins > Catalog > install: Notifications > Webhook, restart your jellyfin. After that -go back again to dashboard > plugins > webhook. Add A `Add Generic Destination`, +go back again to dashboard > plugins > webhook. Add `Add Generic Destination`, ##### Webhook Name: -Choose whatever name you want. +Choose whatever name you want. For example, `Watchstate-Webhook` ##### Webhook Url: @@ -212,6 +213,10 @@ Select the following events * Playback Start * Playback Stop +##### User Filter: + +* Select your user. + ##### Item Type: * Movies @@ -225,17 +230,17 @@ Toggle this checkbox. Key: `X-apikey` -Value: `[YOUR_API_KEY]` +Value: `[WEBHOOK_TOKEN]` Click `save` -#### Emby (you need emby premiere to use webhooks) +#### Emby (you need "Emby Premiere" to use webhooks) Go to your Manage Emby Server > Server > Webhooks > (Click Add Webhook) ##### Webhook Url: -`http://localhost:8081/?apikey=[YOUR_API_KEY]` +`http://localhost:8081/?apikey=[WEBHOOK_TOKEN]` ##### Webhook Events @@ -246,44 +251,39 @@ Select the following events Click `Add Webhook` -#### Plex (you need PlexPass to use webhooks) +#### Plex (you need "Plex Pass" to use webhooks) -Go to your plex WebUI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK) +Go to your plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK) ##### URL: -`http://localhost:8081/?apikey=[YOUR_API_KEY]` +`http://localhost:8081/?apikey=[WEBHOOK_TOKEN]` Click `Save Changes` -# Webhook limitations +--- + +# Known Webhook limitations # Plex -Does not send webhooks events for "marked as watched/unwatched", or you added more than 1 item at time i.e. folder -import. - -If you have multi-user setup, Plex will still report the admin account user id as 1 even though when you get the list -of users ids it shows completely different user ID, so when you initially set up your server for multi-user, select your -admin account and after finishing you have to set the value manually to `1`. to do so please do the following - -```bash -$ docker exec -ti watchstate console servers:edit --key user --set 1 -- [SERVER_NAME] -``` - -This only for the main admin account, other managed/home/external users, you should leave the user as it is reported by -plex. +* Plex does not send webhooks events for "marked as Played/Unplayed". +* Sometimes does not send events if you add more than one item at time. +* If you have multi-user setup, Plex will still report the admin account user id as `1`. +* When you mark items as unwatched, Plex reset the date on the object, which renders the comparison as invalid. # Emby -Emby does not send webhooks events for newly added items. +* Emby does not send webhooks events for newly added items. [See feature request](https://emby.media/community/index.php?/topic/97889-new-content-notification-webhook/) # Jellyfin -If you don't select a user id there, sometimes the plugin will send itemAdd event without user info, and thus will fail -the check if you happen to enable `match user id`. +* If you don't select a user id there, sometimes the webhook plugin will send `itemAdd` event without user info, and thus will fail +the check if you happen to enable `strict user match` for jellyfin. -# Tool specific environment variables. +---- + +# Environment variables. - (string) `WS_DATA_PATH` Where key data stored (config|db). - (string) `WS_TMP_DIR` Where temp data stored. (logs|cache). Defaults to `WS_DATA_PATH` if not set. @@ -318,9 +318,9 @@ the check if you happen to enable `match user id`. # Container specific environment variables. -- (int) `WS_NO_CHOWN` do not change ownership needed paths inside container. -- (int) `WS_DISABLE_HTTP` disable included HTTP Server. -- (int) `WS_DISABLE_CRON` disable included Task Scheduler. +- (int) `WS_NO_CHOWN` do not change ownership for `/app/, /config/` directories inside the container. +- (int) `WS_DISABLE_HTTP` disable included HTTP server. +- (int) `WS_DISABLE_CRON` disable included Task scheduler. - (int) `WS_UID` Container app user ID. - (int) `WS_GID` Container app group ID. diff --git a/src/Commands/Servers/EditCommand.php b/src/Commands/Servers/EditCommand.php index 0c826afa..e9d80afe 100644 --- a/src/Commands/Servers/EditCommand.php +++ b/src/Commands/Servers/EditCommand.php @@ -93,6 +93,8 @@ final class EditCommand extends Command if (null !== $value) { if (true === ctype_digit($value)) { $value = (int)$value; + } elseif (true === is_numeric($value) && true === str_contains($value, '.')) { + $value = (float)$value; } elseif ('true' === strtolower((string)$value) || 'false' === strtolower((string)$value)) { $value = 'true' === $value; } else { @@ -108,7 +110,7 @@ final class EditCommand extends Command $output->writeln( sprintf( - 'Updated server:\'%s\' key \'%s\' with value of \'%s\'.', + '%s: Set key \'%s\' to value of \'%s\'.', $name, $key, is_bool($value) ? (true === $value ? 'true' : 'false') : $value, diff --git a/src/Commands/State/ExportCommand.php b/src/Commands/State/ExportCommand.php index b71db3b2..0cd03058 100644 --- a/src/Commands/State/ExportCommand.php +++ b/src/Commands/State/ExportCommand.php @@ -117,21 +117,21 @@ class ExportCommand extends Command $type = strtolower(ag($server, 'type', 'unknown')); if ($isCustom && !in_array($name, $selected, true)) { - $this->logger->info(sprintf('Ignoring \'%s\' as requested by --servers-filter.', $name)); + $this->logger->info(sprintf('%s: Ignoring this server as requested by --servers-filter.', $name)); continue; } if (true !== ag($server, 'export.enabled')) { - $this->logger->info(sprintf('Ignoring \'%s\' as requested by user config option.', $name)); + $this->logger->info(sprintf('%s: Ignoring this as requested by user config.', $name)); continue; } if (!isset($supported[$type])) { $this->logger->error( sprintf( - 'Unexpected type for server \'%s\'. Was Expecting one of [%s], but got \'%s\' instead.', + '%s: Unexpected backend type. Was expecting \'%s\', but got \'%s\' instead.', $name, - implode('|', array_keys($supported)), + implode(', ', array_keys($supported)), $type ) ); @@ -139,7 +139,7 @@ class ExportCommand extends Command } if (null === ag($server, 'url')) { - $this->logger->error(sprintf('Server \'%s\' has no URL.', $name)); + $this->logger->error(sprintf('%s: Backend does not have valid URL.', $name)); return self::FAILURE; } @@ -151,7 +151,7 @@ class ExportCommand extends Command $output->writeln( sprintf( '%s', - $isCustom ? '[-s, --servers-filter] Filter did not match any server.' : 'No server were found.' + $isCustom ? '[-s, --servers-filter] Filter did not match any server.' : 'No servers were found.' ) ); return self::FAILURE; @@ -200,21 +200,19 @@ class ExportCommand extends Command $after = true === $input->getOption('force-full') ? null : ag($server, 'export.lastSync', null); if (null === $after) { - $this->logger->notice( - sprintf('Exporting \'%s\' play state changes since beginning.', $name) - ); + $this->logger->notice(sprintf('%s: Exporting all local play state to this backend.', $name)); } else { $after = makeDate($after); $this->logger->notice( - sprintf('Exporting \'%s\' play state changes since \'%s\'.', $name, $after) + sprintf('%s: Exporting play state changes since \'%s\' to this backend.', $name, $after) ); } array_push($requests, ...$server['class']->export($this->mapper, $after)); - if (true === Data::get(sprintf('%s.no_export_update', $name))) { + if (true === (bool)Data::get(sprintf('%s.no_export_update', $name))) { $this->logger->notice( - sprintf('Not updating \'%s\' export date, as the server reported an error.', $name) + sprintf('%s: Not updating last export date. Backend reported an error.', $name) ); } else { Config::save(sprintf('servers.%s.export.lastSync', $name), time()); @@ -223,7 +221,7 @@ class ExportCommand extends Command unset($server); - $this->logger->notice(sprintf('Waiting on (%d) (Compare State) Requests.', count($requests))); + $this->logger->notice(sprintf('HTTP: Waiting on \'%d\' state comparison requests.', count($requests))); foreach ($requests as $response) { $requestData = $response->getInfo('user_data'); @@ -234,33 +232,34 @@ class ExportCommand extends Command } } - $this->logger->notice(sprintf('Finished waiting on (%d) Requests.', count($requests))); + $this->logger->notice(sprintf('HTTP: Finished processing \'%d\' state comparison requests.', count($requests))); $changes = $this->mapper->getQueue(); $total = count($changes); if ($total >= 1) { - $this->logger->notice(sprintf('Waiting on (%d) (Stats Change) Requests.', $total)); + $this->logger->notice(sprintf('HTTP: Sending \'%d\' stats change requests.', $total)); foreach ($changes as $response) { $requestData = $response->getInfo('user_data'); try { if (200 !== $response->getStatusCode()) { throw new ServerException($response); } - $this->logger->debug( + $this->logger->notice( sprintf( - 'Processed: State (%s) - %s', - ag($requestData, 'state', '??'), + '%s: Marked \'%s\' as \'%s\'.', + ag($requestData, 'server', '??'), ag($requestData, 'itemName', '??'), + ag($requestData, 'state', '??'), ) ); } catch (ExceptionInterface $e) { $this->logger->error($e->getMessage()); } } - $this->logger->notice(sprintf('Finished waiting on (%d) Requests.', $total)); + $this->logger->notice(sprintf('HTTP: Finished Processing \'%d\' state change requests.', $total)); } else { - $this->logger->notice('No state change detected.'); + $this->logger->notice('No state changes detected.'); } foreach ($list as $server) { diff --git a/src/Commands/State/ImportCommand.php b/src/Commands/State/ImportCommand.php index 560c19c1..04217355 100644 --- a/src/Commands/State/ImportCommand.php +++ b/src/Commands/State/ImportCommand.php @@ -120,7 +120,7 @@ class ImportCommand extends Command $mapperOpts = []; if ($input->getOption('dry-run')) { - $output->writeln('Dry run mode. No changes will be committed to backend.'); + $output->writeln('Dry run mode. No changes will be committed to local database.'); $mapperOpts[Options::DRY_RUN] = true; } @@ -137,21 +137,21 @@ class ImportCommand extends Command $type = strtolower(ag($server, 'type', 'unknown')); if ($isCustom && !in_array($serverName, $selected, true)) { - $this->logger->info(sprintf('Ignoring \'%s\' as requested by --servers-filter.', $serverName)); + $this->logger->info(sprintf('%s: Ignoring as requested by [-s, --servers-filter].', $serverName)); continue; } if (true !== ag($server, 'import.enabled')) { - $this->logger->info(sprintf('Ignoring \'%s\' as requested by user config option.', $serverName)); + $this->logger->info(sprintf('%s: Ignoring as requested by user config option.', $serverName)); continue; } if (!isset($supported[$type])) { $this->logger->error( sprintf( - 'Unexpected type for server \'%s\'. Was Expecting one of [%s], but got \'%s\' instead.', + '%s: Unexpected backend type. Was expecting \'%s\', but got \'%s\' instead.', $serverName, - implode('|', array_keys($supported)), + implode(', ', array_keys($supported)), $type ) ); @@ -160,7 +160,7 @@ class ImportCommand extends Command } if (null === ag($server, 'url')) { - $this->logger->error(sprintf('Server \'%s\' has no URL.', $serverName)); + $this->logger->error(sprintf('%s: Backend has no valid URL.', $serverName)); return self::FAILURE; } @@ -172,7 +172,7 @@ class ImportCommand extends Command $output->writeln( sprintf( '%s', - $isCustom ? '--servers-filter/-s did not return any servers.' : 'No servers were found.' + $isCustom ? '[-s, --servers-filter] Filter did not match any server.' : 'No servers were found.' ) ); return self::FAILURE; @@ -239,7 +239,7 @@ class ImportCommand extends Command array_push($queue, ...$server['class']->pull($this->mapper, $after)); if (true === Data::get(sprintf('%s.no_import_update', $name))) { - $this->logger->notice(sprintf('%s: Not updating last sync date backend reported an error.', $name)); + $this->logger->notice(sprintf('%s: Not updating last sync date. Backend reported an error.', $name)); } else { Config::save(sprintf('servers.%s.import.lastSync', $name), time()); } @@ -263,13 +263,14 @@ class ImportCommand extends Command gc_collect_cycles(); } - unset($queue); - $this->logger->notice('HTTP: Finished waiting external requests.'); + $this->logger->notice(sprintf('HTTP: Waiting on \'%d\' external requests.', count($queue))); + + $queue = $requestData = null; $total = count($this->mapper); if ($total >= 1) { - $this->logger->notice(sprintf('MAPPER: Committing \'%d\' recorded changes.', $total)); + $this->logger->notice(sprintf('MAPPER: Updating \'%d\' items.', $total)); } $operations = $this->mapper->commit(); diff --git a/src/Commands/State/PushCommand.php b/src/Commands/State/PushCommand.php index fb24b9c3..74b63685 100644 --- a/src/Commands/State/PushCommand.php +++ b/src/Commands/State/PushCommand.php @@ -134,16 +134,16 @@ class PushCommand extends Command if (true !== (bool)ag($server, 'webhook.push')) { $output->writeln( - sprintf('Ignoring \'%s\' as requested by user config option.', $serverName), + sprintf('%s: Ignoring as requested by user config option.', $serverName), OutputInterface::VERBOSITY_VERBOSE ); continue; } if (!isset($supported[$type])) { - $output->writeln( + $this->logger->error( sprintf( - 'Server \'%s\' Used Unsupported type. Expecting one of \'%s\' but got \'%s\' instead.', + '%s: Unexpected backend type. Was expecting \'%s\', but got \'%s\' instead.', $serverName, implode(', ', array_keys($supported)), $type @@ -153,7 +153,7 @@ class PushCommand extends Command } if (null === ag($server, 'url')) { - $output->writeln(sprintf('Server \'%s\' has no url.', $serverName)); + $this->logger->error(sprintf('%s: Backend does not have valid URL.', $serverName)); return self::FAILURE; } @@ -208,7 +208,7 @@ class PushCommand extends Command $total = count($requests); if ($total >= 1) { - $this->logger->notice(sprintf('Waiting on (%d) (Stats Change) Requests.', $total)); + $this->logger->notice(sprintf('HTTP: Waiting on \'%d\' change play state requests.', $total)); foreach ($requests as $response) { $requestData = $response->getInfo('user_data'); try { @@ -227,9 +227,9 @@ class PushCommand extends Command $this->logger->error($e->getMessage()); } } - $this->logger->notice(sprintf('Finished waiting on (%d) Requests.', $total)); + $this->logger->notice(sprintf('HTTP: Finished processing \'%d\' change play state requests.', $total)); } else { - $this->logger->notice('No state change detected.'); + $this->logger->notice('No play state change detected.'); } foreach ($list as $server) { diff --git a/src/Libs/Servers/JellyfinServer.php b/src/Libs/Servers/JellyfinServer.php index b6755bf4..4aa1f3e7 100644 --- a/src/Libs/Servers/JellyfinServer.php +++ b/src/Libs/Servers/JellyfinServer.php @@ -754,9 +754,13 @@ class JellyfinServer implements ServerInterface $url = $this->url->withPath(sprintf('/Users/%s/PlayedItems/%s', $this->user, ag($json, 'Id'))); $this->logger->debug( - sprintf('%s: Changing \'%s\' remote state.', $this->name, $iName), + sprintf( + '%s: Changing \'%s\' remote state to \'%s\'.', + $this->name, + $iName, + $state->isWatched() ? 'Played' : 'Unplayed', + ), [ - 'backend' => $state->isWatched() ? 'Played' : 'Unplayed', 'remote' => $isWatched ? 'Played' : 'Unplayed', 'method' => $state->isWatched() ? 'POST' : 'DELETE', 'url' => (string)$url, @@ -772,7 +776,7 @@ class JellyfinServer implements ServerInterface 'user_data' => [ 'itemName' => $iName, 'server' => $this->name, - 'state' => $state->isWatched() ? 'Watched' : 'Unwatched', + 'state' => $state->isWatched() ? 'Played' : 'Unplayed', ], ] ) @@ -1474,12 +1478,19 @@ class JellyfinServer implements ServerInterface $url = $this->url->withPath(sprintf('/Users/%s/PlayedItems/%s', $this->user, $item->Id)); - $this->logger->info(sprintf('%s: Queuing \'%s\'.', $this->name, $iName), [ - 'backend' => $entity->isWatched() ? 'Played' : 'Unplayed', - 'remote' => $rItem->isWatched() ? 'Played' : 'Unplayed', - 'method' => $entity->isWatched() ? 'POST' : 'DELETE', - 'url' => $url, - ]); + $this->logger->debug( + sprintf( + '%s: Changing \'%s\' remote state to \'%s\'.', + $this->name, + $iName, + $entity->isWatched() ? 'Played' : 'Unplayed', + ), + [ + 'remote' => $rItem->isWatched() ? 'Played' : 'Unplayed', + 'method' => $entity->isWatched() ? 'POST' : 'DELETE', + 'url' => $url, + ] + ); $mapper->queue( $this->http->request( @@ -1488,6 +1499,7 @@ class JellyfinServer implements ServerInterface array_replace_recursive($this->getHeaders(), [ 'user_data' => [ 'itemName' => $iName, + 'server' => $this->name, 'state' => $entity->isWatched() ? 'Played' : 'Unplayed', ], ]) diff --git a/src/Libs/Servers/PlexServer.php b/src/Libs/Servers/PlexServer.php index 55db6e39..4ab55ff3 100644 --- a/src/Libs/Servers/PlexServer.php +++ b/src/Libs/Servers/PlexServer.php @@ -797,11 +797,15 @@ class PlexServer implements ServerInterface ); $this->logger->debug( - sprintf('%s: Changing \'%s\' remote state.', $this->name, $iName), + sprintf( + '%s: Changing \'%s\' remote state to \'%s\'.', + $this->name, + $iName, + $state->isWatched() ? 'Played' : 'Unplayed', + ), [ - 'backend' => $state->isWatched() ? 'Played' : 'Unplayed', 'remote' => $isWatched ? 'Played' : 'Unplayed', - 'url' => (string)$url, + 'url' => $url, ] ); @@ -812,7 +816,7 @@ class PlexServer implements ServerInterface 'user_data' => [ 'itemName' => $iName, 'server' => $this->name, - 'state' => $state->isWatched() ? 'Watched' : 'Unwatched', + 'state' => $state->isWatched() ? 'Played' : 'Unplayed', ] ]) ); @@ -1502,11 +1506,18 @@ class PlexServer implements ServerInterface ) ); - $this->logger->info(sprintf('%s: Queuing \'%s\'.', $this->name, $iName), [ - 'backend' => $entity->isWatched() ? 'Played' : 'Unplayed', - 'remote' => $rItem->isWatched() ? 'Played' : 'Unplayed', - 'url' => $url, - ]); + $this->logger->debug( + sprintf( + '%s: Changing \'%s\' remote state to \'%s\'.', + $this->name, + $iName, + $entity->isWatched() ? 'Played' : 'Unplayed', + ), + [ + 'remote' => $rItem->isWatched() ? 'Played' : 'Unplayed', + 'url' => $url, + ] + ); $mapper->queue( $this->http->request(