diff --git a/.env.development.example b/.env.development.example index 039da130..63b7dbf9 100644 --- a/.env.development.example +++ b/.env.development.example @@ -1,4 +1,4 @@ -# Enviroment +# Environment ENV=development USER_ID=1000 HTTP_PORT=80 @@ -6,13 +6,15 @@ TIMEZONE="Europe/Berlin" MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING=15 # Database -DATABASE_HOST="mysql" -DATABASE_PORT=3306 -DATABASE_NAME=movary -DATABASE_USER= -DATABASE_PASSWORD= -DATABASE_DRIVER=pdo_mysql -DATABASE_CHARSET=utf8 +DATABASE_MODE=sqlite +DATABASE_SQLITE=storage/movary.sqlite +DATABASE_MYSQL_HOST=mysql +DATABASE_MYSQL_PORT=3306 +DATABASE_MYSQL_NAME=movary +DATABASE_MYSQL_USER=movary +DATABASE_MYSQL_PASSWORD=movary +DATABASE_MYSQL_CHARSET=utf8mb4 +DATABASE_MYSQL_ROOT_PASSWORD=movary # Tmdb TMDB_API_KEY= @@ -25,7 +27,3 @@ LETTERBOXD_RATINGS_CSV_PATH="tmp/ratings.csv" LOG_LEVEL=debug LOG_ENABLE_STACKTRACE=1 LOG_ENABLE_FILE_LOGGING=1 - -# Only needed for development -DATABASE_PORT_HOST=3306 -DATABASE_ROOT_PASSWORD=movary diff --git a/.env.production.example b/.env.production.example index 19179820..6e6a8ef4 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,22 +1,21 @@ -# Enviroment +# Environment ENV=production TIMEZONE="Europe/Berlin" MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING=15 # Database -DATABASE_HOST= -DATABASE_PORT=3306 -DATABASE_NAME=movary -DATABASE_USER= -DATABASE_PASSWORD= -DATABASE_DRIVER=pdo_mysql -DATABASE_CHARSET=utf8 +DATABASE_MODE=sqlite +DATABASE_SQLITE=storage/movary.sqlite +DATABASE_MYSQL_HOST= +DATABASE_MYSQL_PORT= +DATABASE_MYSQL_NAME= +DATABASE_MYSQL_USER= +DATABASE_MYSQL_PASSWORD= +DATABASE_MYSQL_CHARSET=utf8mb4 # Tmdb TMDB_API_KEY= -TMDB_ENABLE_IMAGE_CACHING=0 +TMDB_ENABLE_IMAGE_CACHING=1 # Logging LOG_LEVEL=warning -LOG_ENABLE_STACKTRACE=0 -LOG_ENABLE_FILE_LOGGING=0 diff --git a/Makefile b/Makefile index e9bbc235..b98190d9 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ reup: down up build: docker-compose build --no-cache make up + make db_mysql_create_database make composer_install make app_database_migrate @@ -28,10 +29,10 @@ exec_app_cmd: docker-compose exec app bash -c "${CMD}" exec_mysql_cli: - docker-compose exec mysql sh -c "mysql -u${DB_USER} -p${DB_PASSWORD} ${DATABASE_NAME}" + docker-compose exec mysql sh -c "mysql -u${DB_USER} -p${DB_PASSWORD} ${DATABASE_MYSQL_NAME}" exec_mysql_query: - docker-compose exec mysql bash -c "mysql -uroot -p${DATABASE_ROOT_PASSWORD} -e \"$(QUERY)\"" + docker-compose exec mysql bash -c "mysql -uroot -p${DATABASE_MYSQL_ROOT_PASSWORD} -e \"$(QUERY)\"" # Composer ########## @@ -46,19 +47,19 @@ composer_test: # Database ########## -db_create_database: - make exec_mysql_query QUERY="DROP DATABASE IF EXISTS $(DATABASE_NAME)" - make exec_mysql_query QUERY="CREATE DATABASE $(DATABASE_NAME)" - make exec_mysql_query QUERY="GRANT ALL PRIVILEGES ON $(DATABASE_NAME).* TO $(DATABASE_USER)@'%'" +db_mysql_create_database: + make exec_mysql_query QUERY="DROP DATABASE IF EXISTS $(DATABASE_MYSQL_NAME)" + make exec_mysql_query QUERY="CREATE DATABASE $(DATABASE_MYSQL_NAME)" + make exec_mysql_query QUERY="GRANT ALL PRIVILEGES ON $(DATABASE_MYSQL_NAME).* TO $(DATABASE_MYSQL_USER)@'%'" make exec_mysql_query QUERY="FLUSH PRIVILEGES;" make app_database_migrate -db_import: +db_mysql_import: docker cp tmp/dump.sql movary_mysql_1:/tmp/dump.sql - docker-compose exec mysql bash -c 'mysql -uroot -p${DATABASE_ROOT_PASSWORD} < /tmp/dump.sql' + docker-compose exec mysql bash -c 'mysql -uroot -p${DATABASE_MYSQL_ROOT_PASSWORD} < /tmp/dump.sql' -db_export: - docker-compose exec mysql bash -c 'mysqldump --databases --add-drop-database -uroot -p$(DATABASE_ROOT_PASSWORD) $(DATABASE_NAME) > /tmp/dump.sql' +db_mysql_export: + docker-compose exec mysql bash -c 'mysqldump --databases --add-drop-database -uroot -p$(DATABASE_MYSQL_ROOT_PASSWORD) $(DATABASE_MYSQL_NAME) > /tmp/dump.sql' docker cp movary_mysql_1:/tmp/dump.sql tmp/dump.sql chown $(USER_ID):$(USER_ID) tmp/dump.sql diff --git a/README.md b/README.md index 720c73e9..707a1327 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Reddit badge](https://img.shields.io/reddit/subreddit-subscribers/movary)](https://www.reddit.com/r/movary/) [![License badge](https://img.shields.io/github/license/leepeuker/movary)](https://github.com/leepeuker/movary/blob/main/LICENSE) -Movary is a self hosted web application to track and rate your watched movies (like a digitial movie diary). You can import/export your history and ratings from/to external sources like trakt.tv or letterboxd.com, track your watches automatically via plex and more. +Movary is a self-hosted web application to track and rate your watched movies (like a digital movie diary). +You can import/export your history and ratings from/to external sources like trakt.tv or letterboxd.com, +track your watches automatically via plex and more. Demo installation can be found [here](https://demo.movary.org/) (login email `testUser@movary.org` and password `testUser`). @@ -25,21 +27,23 @@ Demo installation can be found [here](https://demo.movary.org/) (login email `te 5. [Development](#development) 6. [Support](#support) -Please report all bugs, improvement suggestions or feature wishes by creating [github issues](https://www.reddit.com/r/movary/) or visit -the [official subreddit](https://www.reddit.com/r/movary/)! +Please report all bugs, improvement suggestions or feature wishes by creating [github issues](https://www.reddit.com/r/movary/) +or visit the [official subreddit](https://www.reddit.com/r/movary/)! --- ## About -This project started because I wanted a self-hosted solution for tracking my watched movies and their ratings, so that I can really own my data and do not have to solely rely on other providers like letterboxd or trakt to keep it safe (or decide what to do with it). +This project started because I wanted a self-hosted solution for tracking my watched movies and their ratings, +so that I can really own my data and do not have to solely rely on other providers like letterboxd +or trakt to keep it safe (or decide what to do with it). **Features:** - Movie tracking: Collect and manage your watch history and ratings - Statistics: Overview over your movie watching behavior and history, like e.g. most watched actors/directors/genres/languages/years -- Third party support: Import your existing history and ratings from trakt.tv or letterboxd.com -- Plex scrobbler: Automatically add new watches and/or ratings (plex premium required) +- Third party support: Import your existing history and ratings from e.g. trakt.tv or letterboxd.com +- Plex scrobbler: Automatically add new plex watches and ratings (plex premium required) - Own your personal data: Users can decide who can see their data and export/import/delete the data and their accounts at any time - Locally stored metadata: Using e.g. themoviedb.org and imdb as sources, all metadata movary uses for your history entries can be stored locally - PWA: Can be installed as an app ([How to install PWAs in chrome](https://support.google.com/chrome/answer/9658361?hl=en&co=GENIE.Platform%3DAndroid&oco=1)) @@ -56,25 +60,39 @@ which can lead to sudden breaking changes until then, so keep the release notes This is the preferred and currently only tested way to run the app. -You must provide a tmdb api key (get one [here](https://www.themoviedb.org/settings/api)) to work correctly. +You must provide a tmdb api key (get one [here](https://www.themoviedb.org/settings/api)). -Example shell comamnds with an already existing mysql server: +Example using MySQL (recommended): ```shell $ docker volume create movary-storage +$ docker run --rm -d \ + --name movary \ + -p 80:80 \ + -e TMDB_API_KEY="" \ + -e DATABASE_MODE="mysql" \ + -e DATABASE_MYSQL_HOST="" \ + -e DATABASE_MYSQL_NAME="" \ + -e DATABASE_MYSQL_USER="" \ + -e DATABASE_MYSQL_PASSWORD="" \ + -v movary-storage:/app/storage \ + leepeuker/movary:latest +``` +Example using SQLite: + +```shell +$ docker volume create movary-storage $ docker run --rm -d \ --name movary \ -p 80:80 \ - -e DATABASE_HOST="" \ - -e DATABASE_USER="" \ - -e DATABASE_PASSWORD="" \ -e TMDB_API_KEY="" \ - -v movary-storage:/app/storage + -e DATABASE_MODE="sqlite" \ + -v movary-storage:/app/storage \ leepeuker/movary:latest ``` -Example docker-compose.yml inlcuding a mysql server +Example docker-compose.yml with a MySQL server ```yml version: "3.5" @@ -86,21 +104,22 @@ services: ports: - "80:80" environment: - DATABASE_HOST: "mysql" - DATABASE_NAME: "movary" - DATABASE_USER: "" - DATABASE_PASSWORD: "" - TMDB_API_KEY: "" + TMDB_API_KEY: "" + DATABASE_MODE: "mysql" + DATABASE_MYSQL_HOST: "mysql" + DATABASE_MYSQL_NAME: "movary" + DATABASE_MYSQL_USER: "movary_user" + DATABASE_MYSQL_PASSWORD: "movary_password" volumes: - movary-storage:/app/storage mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD: "" MYSQL_DATABASE: "movary" - MYSQL_USER: "" - MYSQL_PASSWORD: "" + MYSQL_USER: "movary_user" + MYSQL_PASSWORD: "movary_password" + MYSQL_ROOT_PASSWORD: "" volumes: - movary-db:/var/lib/mysql @@ -111,16 +130,16 @@ volumes: ## Important: First steps -You can run commands in docker via e.g. `docker exec movary php bin/console.php` (this returns a list of all available movary cli commands) +You can run movary commands in docker via e.g. `docker exec movary php bin/console.php` -- Run database migrations, e.g.: `php bin/console.php database:migration:migrate` (on initial installation and after every update) -- Create initial user: - - via web ui by visiting movary landingpage `/` +1. Execute missing database migrations: `php bin/console.php database:migration:migrate` (on initial installation and ideally after every update) +2. Create initial user + - via web UI by visiting the movary lading page for the first time - via cli `php bin/console.php user:create email@example.com password username` It is recommended to enable tmdb image caching (set env variable `TMDB_ENABLE_IMAGE_CACHING=1`). -##### Available environment variables with defaults: +##### Available environment variables with their default values: ``` ### Enviroment @@ -130,13 +149,15 @@ TIMEZONE="Europe/Berlin" MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING=15 ### Database -DATABASE_HOST= -DATABASE_PORT=3306 -DATABASE_NAME=movary -DATABASE_USER= -DATABASE_PASSWORD= -DATABASE_DRIVER=pdo_mysql -DATABASE_CHARSET=utf8 +# Supported modes: sqlite or mysql +DATABASE_MODE= +DATABASE_SQLITE=storage/movary.sqlite +DATABASE_MYSQL_HOST= +DATABASE_MYSQL_PORT=3306 +DATABASE_MYSQL_NAME= +DATABASE_MYSQL_USER= +DATABASE_MYSQL_PASSWORD= +DATABASE_MYSQL_CHARSET=utf8mb4 ### TMDB # Used for metda data collection, see: https://www.themoviedb.org/settings/api @@ -155,7 +176,7 @@ their [docs](https://dockerfile.readthedocs.io/en/latest/content/DockerImages/do ## Features -Use `php bin/console.php tmdb:movie:sync` to list all available cli commands +Use `php bin/console.php` to list all available cli commands ### tmdb sync @@ -164,10 +185,11 @@ Make sure you have added the variables `TMDB_API_KEY` to the environment. Helpful commands: -`php bin/console.php tmdb:movie:sync` -`php bin/console.php tmdb:person:sync` +`php bin/console.php tmdb:movie:sync` -> Refresh local movie meta data -**Flags:** +`php bin/console.php tmdb:person:sync` -> Refresh local person meta data + +**Interesting flags:** - `--hours` Only update movies/persons which were last synced X hours or longer ago @@ -186,8 +208,8 @@ Execute the cache refresh command regularly, e.g. via cronjob, to keep the cache Helpful commands: -- Refresh image cache: `php bin/console.php tmdb:imageCache:refresh` -- Delete cached images: `php bin/console.php tmdb:imageCache:delete` +- `php bin/console.php tmdb:imageCache:refresh` -> Refresh local image cache +- `php bin/console.php tmdb:imageCache:delete` -> Delete locally cached images ### Plex Scrobbler @@ -207,13 +229,13 @@ The trakt account used in the import process must have a trakt username and clie The import can be executed via the settings page `/settings/trakt` or via cli. -Example cli import (import history and ratings for user with id 1): +Example cli import (import history and ratings for user with id 1 and overwrite locally existing data if needed): -`php bin/console.php trakt:import --ratings --history --userId=1` +`php bin/console.php trakt:import --userId=1 --ratings --history --overwrite` **Info:** Importing hundreds or thousands of movies for the first time can take a few minutes. -**Flags:** +**Interesting flags:** - `--userId` User to import data to @@ -224,7 +246,7 @@ Example cli import (import history and ratings for user with id 1): - `--overwrite` Use if you want to overwrite the local state with the trakt state (deletes and overwrites local data) - `--ignore-cache` - Use if you want to import everything from trakt regardless if there was a change since the last import. + Use if you want to force import everything regardless if there was a change since the last import ### Trakt.tv Export diff --git a/db/migrations/20210124104021_SetupBaseTables.php b/db/migrations/mysql/20210124104021_SetupBaseTables.php similarity index 100% rename from db/migrations/20210124104021_SetupBaseTables.php rename to db/migrations/mysql/20210124104021_SetupBaseTables.php diff --git a/db/migrations/20210204195525_AddMoreMovieDetails.php b/db/migrations/mysql/20210204195525_AddMoreMovieDetails.php similarity index 100% rename from db/migrations/20210204195525_AddMoreMovieDetails.php rename to db/migrations/mysql/20210204195525_AddMoreMovieDetails.php diff --git a/db/migrations/20210204211239_RemoveYearFromMovie.php b/db/migrations/mysql/20210204211239_RemoveYearFromMovie.php similarity index 100% rename from db/migrations/20210204211239_RemoveYearFromMovie.php rename to db/migrations/mysql/20210204211239_RemoveYearFromMovie.php diff --git a/db/migrations/20210206222052_AddGenre.php b/db/migrations/mysql/20210206222052_AddGenre.php similarity index 100% rename from db/migrations/20210206222052_AddGenre.php rename to db/migrations/mysql/20210206222052_AddGenre.php diff --git a/db/migrations/20220402194534_AddPerson.php b/db/migrations/mysql/20220402194534_AddPerson.php similarity index 100% rename from db/migrations/20220402194534_AddPerson.php rename to db/migrations/mysql/20220402194534_AddPerson.php diff --git a/db/migrations/20220418073143_AddProductionCompanies.php b/db/migrations/mysql/20220418073143_AddProductionCompanies.php similarity index 100% rename from db/migrations/20220418073143_AddProductionCompanies.php rename to db/migrations/mysql/20220418073143_AddProductionCompanies.php diff --git a/db/migrations/20220418081407_AddMovieTagline.php b/db/migrations/mysql/20220418081407_AddMovieTagline.php similarity index 100% rename from db/migrations/20220418081407_AddMovieTagline.php rename to db/migrations/mysql/20220418081407_AddMovieTagline.php diff --git a/db/migrations/20220420173442_AddSecondRatingType.php b/db/migrations/mysql/20220420173442_AddSecondRatingType.php similarity index 100% rename from db/migrations/20220420173442_AddSecondRatingType.php rename to db/migrations/mysql/20220420173442_AddSecondRatingType.php diff --git a/db/migrations/20220420185953_AddLetterboxdId.php b/db/migrations/mysql/20220420185953_AddLetterboxdId.php similarity index 100% rename from db/migrations/20220420185953_AddLetterboxdId.php rename to db/migrations/mysql/20220420185953_AddLetterboxdId.php diff --git a/db/migrations/20220424171847_RemoveTimeFromWatchDate.php b/db/migrations/mysql/20220424171847_RemoveTimeFromWatchDate.php similarity index 100% rename from db/migrations/20220424171847_RemoveTimeFromWatchDate.php rename to db/migrations/mysql/20220424171847_RemoveTimeFromWatchDate.php diff --git a/db/migrations/20220505182414_AddPosterPathToMovie.php b/db/migrations/mysql/20220505182414_AddPosterPathToMovie.php similarity index 100% rename from db/migrations/20220505182414_AddPosterPathToMovie.php rename to db/migrations/mysql/20220505182414_AddPosterPathToMovie.php diff --git a/db/migrations/20220506184030_AddSyncLogTable.php b/db/migrations/mysql/20220506184030_AddSyncLogTable.php similarity index 100% rename from db/migrations/20220506184030_AddSyncLogTable.php rename to db/migrations/mysql/20220506184030_AddSyncLogTable.php diff --git a/db/migrations/20220506195137_ChangePrimaryKeyInTraktCache.php b/db/migrations/mysql/20220506195137_ChangePrimaryKeyInTraktCache.php similarity index 100% rename from db/migrations/20220506195137_ChangePrimaryKeyInTraktCache.php rename to db/migrations/mysql/20220506195137_ChangePrimaryKeyInTraktCache.php diff --git a/db/migrations/20220510185016_AddUser.php b/db/migrations/mysql/20220510185016_AddUser.php similarity index 100% rename from db/migrations/20220510185016_AddUser.php rename to db/migrations/mysql/20220510185016_AddUser.php diff --git a/db/migrations/20220514093905_AddPlaysToHistory.php b/db/migrations/mysql/20220514093905_AddPlaysToHistory.php similarity index 100% rename from db/migrations/20220514093905_AddPlaysToHistory.php rename to db/migrations/mysql/20220514093905_AddPlaysToHistory.php diff --git a/db/migrations/20220515085154_AddPosterPathToPerson.php b/db/migrations/mysql/20220515085154_AddPosterPathToPerson.php similarity index 100% rename from db/migrations/20220515085154_AddPosterPathToPerson.php rename to db/migrations/mysql/20220515085154_AddPosterPathToPerson.php diff --git a/db/migrations/20220604103136_SplitUpMovePosterPath.php b/db/migrations/mysql/20220604103136_SplitUpMovePosterPath.php similarity index 100% rename from db/migrations/20220604103136_SplitUpMovePosterPath.php rename to db/migrations/mysql/20220604103136_SplitUpMovePosterPath.php diff --git a/db/migrations/20220605074031_ChangeMovieTraktIdAndImdbIdToNullable.php b/db/migrations/mysql/20220605074031_ChangeMovieTraktIdAndImdbIdToNullable.php similarity index 100% rename from db/migrations/20220605074031_ChangeMovieTraktIdAndImdbIdToNullable.php rename to db/migrations/mysql/20220605074031_ChangeMovieTraktIdAndImdbIdToNullable.php diff --git a/db/migrations/20220606175053_ChangeTraktIdToUniqueInWatchedCache.php b/db/migrations/mysql/20220606175053_ChangeTraktIdToUniqueInWatchedCache.php similarity index 100% rename from db/migrations/20220606175053_ChangeTraktIdToUniqueInWatchedCache.php rename to db/migrations/mysql/20220606175053_ChangeTraktIdToUniqueInWatchedCache.php diff --git a/db/migrations/20220624095147_ReplaceRating10AndRating5WithPersonalRating.php b/db/migrations/mysql/20220624095147_ReplaceRating10AndRating5WithPersonalRating.php similarity index 100% rename from db/migrations/20220624095147_ReplaceRating10AndRating5WithPersonalRating.php rename to db/migrations/mysql/20220624095147_ReplaceRating10AndRating5WithPersonalRating.php diff --git a/db/migrations/20220628192946_AddPlexWebhookId.php b/db/migrations/mysql/20220628192946_AddPlexWebhookId.php similarity index 100% rename from db/migrations/20220628192946_AddPlexWebhookId.php rename to db/migrations/mysql/20220628192946_AddPlexWebhookId.php diff --git a/db/migrations/20220630135944_AddUserAuthTokenTable.php b/db/migrations/mysql/20220630135944_AddUserAuthTokenTable.php similarity index 100% rename from db/migrations/20220630135944_AddUserAuthTokenTable.php rename to db/migrations/mysql/20220630135944_AddUserAuthTokenTable.php diff --git a/db/migrations/20220708144310_AddMultiUserSetup.php b/db/migrations/mysql/20220708144310_AddMultiUserSetup.php similarity index 100% rename from db/migrations/20220708144310_AddMultiUserSetup.php rename to db/migrations/mysql/20220708144310_AddMultiUserSetup.php diff --git a/db/migrations/20220711194814_SetCorrectConstraintForMovieWatchDates.php b/db/migrations/mysql/20220711194814_SetCorrectConstraintForMovieWatchDates.php similarity index 100% rename from db/migrations/20220711194814_SetCorrectConstraintForMovieWatchDates.php rename to db/migrations/mysql/20220711194814_SetCorrectConstraintForMovieWatchDates.php diff --git a/db/migrations/20220713163724_AddTraktClientIdToUserTable.php b/db/migrations/mysql/20220713163724_AddTraktClientIdToUserTable.php similarity index 100% rename from db/migrations/20220713163724_AddTraktClientIdToUserTable.php rename to db/migrations/mysql/20220713163724_AddTraktClientIdToUserTable.php diff --git a/db/migrations/20220714094745_AddCacheTableForTmdbLanguages.php b/db/migrations/mysql/20220714094745_AddCacheTableForTmdbLanguages.php similarity index 100% rename from db/migrations/20220714094745_AddCacheTableForTmdbLanguages.php rename to db/migrations/mysql/20220714094745_AddCacheTableForTmdbLanguages.php diff --git a/db/migrations/20220714115426_AddDateFormatToUserTable.php b/db/migrations/mysql/20220714115426_AddDateFormatToUserTable.php similarity index 100% rename from db/migrations/20220714115426_AddDateFormatToUserTable.php rename to db/migrations/mysql/20220714115426_AddDateFormatToUserTable.php diff --git a/db/migrations/20220718170243_AddCoreAccountChangesDisabledToUser.php b/db/migrations/mysql/20220718170243_AddCoreAccountChangesDisabledToUser.php similarity index 100% rename from db/migrations/20220718170243_AddCoreAccountChangesDisabledToUser.php rename to db/migrations/mysql/20220718170243_AddCoreAccountChangesDisabledToUser.php diff --git a/db/migrations/20220719134322_AddJobQueueTable.php b/db/migrations/mysql/20220719134322_AddJobQueueTable.php similarity index 100% rename from db/migrations/20220719134322_AddJobQueueTable.php rename to db/migrations/mysql/20220719134322_AddJobQueueTable.php diff --git a/db/migrations/20220725113013_UpdateUsername.php b/db/migrations/mysql/20220725113013_UpdateUsername.php similarity index 100% rename from db/migrations/20220725113013_UpdateUsername.php rename to db/migrations/mysql/20220725113013_UpdateUsername.php diff --git a/db/migrations/20220728103556_AddPrivacyLevelToUser.php b/db/migrations/mysql/20220728103556_AddPrivacyLevelToUser.php similarity index 100% rename from db/migrations/20220728103556_AddPrivacyLevelToUser.php rename to db/migrations/mysql/20220728103556_AddPrivacyLevelToUser.php diff --git a/db/migrations/20220731091237_AddImdbRatingToMovie.php b/db/migrations/mysql/20220731091237_AddImdbRatingToMovie.php similarity index 100% rename from db/migrations/20220731091237_AddImdbRatingToMovie.php rename to db/migrations/mysql/20220731091237_AddImdbRatingToMovie.php diff --git a/db/migrations/20220804080839_UpdateProductionCompanyForeignKeys.php b/db/migrations/mysql/20220804080839_UpdateProductionCompanyForeignKeys.php similarity index 100% rename from db/migrations/20220804080839_UpdateProductionCompanyForeignKeys.php rename to db/migrations/mysql/20220804080839_UpdateProductionCompanyForeignKeys.php diff --git a/db/migrations/20220815113051_UpdateJobQueueTable.php b/db/migrations/mysql/20220815113051_UpdateJobQueueTable.php similarity index 100% rename from db/migrations/20220815113051_UpdateJobQueueTable.php rename to db/migrations/mysql/20220815113051_UpdateJobQueueTable.php diff --git a/db/migrations/20220816164829_RemoveScanLog.php b/db/migrations/mysql/20220816164829_RemoveScanLog.php similarity index 100% rename from db/migrations/20220816164829_RemoveScanLog.php rename to db/migrations/mysql/20220816164829_RemoveScanLog.php diff --git a/db/migrations/20221206190149_ExtendPersonMetaData.php b/db/migrations/mysql/20221206190149_ExtendPersonMetaData.php similarity index 100% rename from db/migrations/20221206190149_ExtendPersonMetaData.php rename to db/migrations/mysql/20221206190149_ExtendPersonMetaData.php diff --git a/db/migrations/20221208101424_AddPlexScrobbleOptionsToUser.php b/db/migrations/mysql/20221208101424_AddPlexScrobbleOptionsToUser.php similarity index 100% rename from db/migrations/20221208101424_AddPlexScrobbleOptionsToUser.php rename to db/migrations/mysql/20221208101424_AddPlexScrobbleOptionsToUser.php diff --git a/db/migrations/20221209195438_UpdateMoveForeignKeysToCascade.php b/db/migrations/mysql/20221209195438_UpdateMoveForeignKeysToCascade.php similarity index 100% rename from db/migrations/20221209195438_UpdateMoveForeignKeysToCascade.php rename to db/migrations/mysql/20221209195438_UpdateMoveForeignKeysToCascade.php diff --git a/db/migrations/mysql/20221220113521_InitialSqliteAdjustment.php b/db/migrations/mysql/20221220113521_InitialSqliteAdjustment.php new file mode 100644 index 00000000..0fe441ac --- /dev/null +++ b/db/migrations/mysql/20221220113521_InitialSqliteAdjustment.php @@ -0,0 +1,50 @@ +execute( + <<execute( + <<execute( + <<createUserTable(); + $this->createCompanyTable(); + $this->createGenreTable(); + $this->createMovieTable(); + $this->createPersonTable(); + $this->createMovieRelationTables(); + $this->createTraktTables(); + $this->createJobQueueTable(); + + $this->execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<execute( + <<get(Movary\ValueObject\Config::class); +$databaseMode = \Movary\Factory::getDatabaseMode($config); +if ($databaseMode === 'sqlite') { + $sqliteFile = pathinfo($config->getAsString('DATABASE_SQLITE')); + $databaseConfig = [ + 'adapter' => 'sqlite', + 'name' => $sqliteFile['dirname'] . '/' . $sqliteFile['filename'], + 'suffix' => $sqliteFile['extension'], + ]; +} elseif (\Movary\Factory::getDatabaseMode($config) === 'mysql') { + $databaseConfig = [ + 'adapter' => 'mysql', + 'host' => $config->getAsString('DATABASE_MYSQL_HOST'), + 'port' => \Movary\Factory::getDatabaseMysqlPort($config), + 'name' => $config->getAsString('DATABASE_MYSQL_NAME'), + 'user' => $config->getAsString('DATABASE_MYSQL_USER'), + 'pass' => $config->getAsString('DATABASE_MYSQL_PASSWORD'), + 'charset' => \Movary\Factory::getDatabaseMysqlCharset($config), + 'collation' => 'utf8_unicode_ci', + ]; +} else { + throw new \RuntimeException('Not supported database mode: ' . $databaseMode); +} + return [ 'paths' => [ - 'migrations' => __DIR__ . '/../db/migrations', + 'migrations' => __DIR__ . '/../db/migrations/' . $databaseMode, ], 'environments' => [ 'default_migration_table' => 'phinxlog', 'default_environment' => 'dynamic', - 'dynamic' => [ - 'adapter' => $config->getAsString('DATABASE_DRIVER') === 'pdo_mysql' ? 'mysql' : $config->getAsString('database.driver'), - 'host' => $config->getAsString('DATABASE_HOST'), - 'port' => $config->getAsString('DATABASE_PORT'), - 'name' => $config->getAsString('DATABASE_NAME'), - 'user' => $config->getAsString('DATABASE_USER'), - 'pass' => $config->getAsString('DATABASE_PASSWORD'), - 'charset' => $config->getAsString('DATABASE_CHARSET'), - 'collation' => 'utf8_unicode_ci', - ], + 'dynamic' => $databaseConfig, ], ]; diff --git a/settings/phpstan.neon b/settings/phpstan.neon index f790d244..44bb00de 100644 --- a/settings/phpstan.neon +++ b/settings/phpstan.neon @@ -9,10 +9,6 @@ parameters: - message: '#Parameter \#2 \$level of class Monolog\\Handler\\StreamHandler#' path: ../src/Factory.php - - - - message: '#Parameter \#1 \$params of static method Doctrine\\DBAL\\DriverManager#' - path: ../src/Factory.php - message: '#Call to an undefined method PHPUnit\\Framework\\MockObject\\MockObject|#' path: ../tests/unit/* diff --git a/src/Api/Tmdb/Cache/TmdbImageCache.php b/src/Api/Tmdb/Cache/TmdbImageCache.php index 9d19f06a..145e0dd3 100644 --- a/src/Api/Tmdb/Cache/TmdbImageCache.php +++ b/src/Api/Tmdb/Cache/TmdbImageCache.php @@ -78,8 +78,8 @@ private function cacheAllImagesByMovieId(int $movieId) : void FROM ( SELECT id FROM person - JOIN movie_cast cast on person.id = cast.person_id - WHERE cast.movie_id = ? + JOIN movie_cast mcast on person.id = mcast.person_id + WHERE mcast.movie_id = ? UNION SELECT id FROM person diff --git a/src/Domain/Company/CompanyRepository.php b/src/Domain/Company/CompanyRepository.php index a8c24542..5528ce63 100644 --- a/src/Domain/Company/CompanyRepository.php +++ b/src/Domain/Company/CompanyRepository.php @@ -3,6 +3,7 @@ namespace Movary\Domain\Company; use Doctrine\DBAL\Connection; +use Movary\ValueObject\DateTime; use RuntimeException; class CompanyRepository @@ -19,6 +20,7 @@ public function create(string $name, ?string $originCountry, int $tmdbId) : Comp 'name' => $name, 'origin_country' => $originCountry, 'tmdb_id' => $tmdbId, + 'created_at' => (string)DateTime::create(), ], ); diff --git a/src/Domain/Genre/GenreRepository.php b/src/Domain/Genre/GenreRepository.php index 4a93c3a2..6dd9c955 100644 --- a/src/Domain/Genre/GenreRepository.php +++ b/src/Domain/Genre/GenreRepository.php @@ -3,6 +3,7 @@ namespace Movary\Domain\Genre; use Doctrine\DBAL\Connection; +use Movary\ValueObject\DateTime; use RuntimeException; class GenreRepository @@ -18,6 +19,7 @@ public function create(string $name, int $tmdbId) : GenreEntity [ 'name' => $name, 'tmdb_id' => $tmdbId, + 'created_at' => (string)DateTime::create(), ], ); diff --git a/src/Domain/Movie/Crew/CrewRepository.php b/src/Domain/Movie/Crew/CrewRepository.php index 48412673..d197c895 100644 --- a/src/Domain/Movie/Crew/CrewRepository.php +++ b/src/Domain/Movie/Crew/CrewRepository.php @@ -31,7 +31,7 @@ public function deleteByMovieId(int $movieId) : void public function findDirectorsByMovieId(int $movieId) : CrewEntityList { - $data = $this->dbConnection->fetchAllAssociative('SELECT * FROM `movie_crew` WHERE movie_id = ? AND job = "director"', [$movieId]); + $data = $this->dbConnection->fetchAllAssociative('SELECT * FROM `movie_crew` WHERE movie_id = ? AND job = "Director"', [$movieId]); return CrewEntityList::createFromArray($data); } diff --git a/src/Domain/Movie/History/MovieHistoryRepository.php b/src/Domain/Movie/History/MovieHistoryRepository.php index 10e9c0cf..81bd1882 100644 --- a/src/Domain/Movie/History/MovieHistoryRepository.php +++ b/src/Domain/Movie/History/MovieHistoryRepository.php @@ -44,7 +44,7 @@ public function deleteByUserId(int $userId) : void public function deleteHistoryByIdAndDate(int $movieId, int $userId, Date $watchedAt) : void { $this->dbConnection->executeStatement( - 'DELETE movie_user_watch_dates + 'DELETE FROM movie_user_watch_dates WHERE movie_id = ? AND watched_at = ? AND user_id = ?', [$movieId, (string)$watchedAt, $userId], diff --git a/src/Domain/Movie/MovieApi.php b/src/Domain/Movie/MovieApi.php index 06f876a5..4fb9a6bc 100644 --- a/src/Domain/Movie/MovieApi.php +++ b/src/Domain/Movie/MovieApi.php @@ -402,6 +402,10 @@ public function updateUserRating(int $movieId, int $userId, ?PersonalRating $rat return; } - $this->repository->updateUserRating($movieId, $userId, $rating); + if ($this->repository->updateUserRating($movieId, $userId, $rating) > 0) { + return; + } + + $this->repository->insertUserRating($movieId, $userId, $rating); } } diff --git a/src/Domain/Movie/MovieRepository.php b/src/Domain/Movie/MovieRepository.php index 921604b5..ff902f7a 100644 --- a/src/Domain/Movie/MovieRepository.php +++ b/src/Domain/Movie/MovieRepository.php @@ -3,6 +3,7 @@ namespace Movary\Domain\Movie; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\SqlitePlatform; use Movary\Api\Trakt\ValueObject\TraktId; use Movary\ValueObject\Date; use Movary\ValueObject\DateTime; @@ -47,6 +48,7 @@ public function create( 'trakt_id' => $traktId?->asInt(), 'imdb_id' => $imdbId, 'tmdb_id' => $tmdbId, + 'created_at' => (string)Date::create(), ], ); @@ -401,13 +403,26 @@ public function fetchMostWatchedProductionCompanies(int $userId, ?int $limit = n public function fetchMostWatchedReleaseYears(int $userId) : array { + if ($this->dbConnection->getDatabasePlatform() instanceof SqlitePlatform) { + return $this->dbConnection->fetchAllAssociative( + <<dbConnection->fetchAllAssociative( <<dbConnection->getDatabasePlatform() instanceof SqlitePlatform) { + $whereQuery .= 'AND strftime("%Y", m.release_date) = ? '; + } $payload[] = (string)$releaseYear; } @@ -557,6 +575,19 @@ public function fetchUniqueMovieLanguages(int $userId) : array public function fetchUniqueMovieReleaseYears(int $userId) : array { + if ($this->dbConnection->getDatabasePlatform() instanceof SqlitePlatform) { + return $this->dbConnection->fetchFirstColumn( + <<dbConnection->fetchFirstColumn( <<dbConnection->insert( + 'movie_user_rating', + [ + 'movie_id' => $movieId, + 'user_id' => $userId, + 'rating' => $rating->asInt(), + 'created_at' => (string)DateTime::create(), + ], + ); + } + public function updateDetails( int $id, ?string $tagline, @@ -736,6 +780,7 @@ public function updateDetails( 'tmdb_poster_path' => $tmdbPosterPath, 'updated_at_tmdb' => (string)DateTime::create(), 'imdb_id' => $imdbId, + 'updated_at' => (string)DateTime::create(), ], ['id' => $id], ); @@ -749,24 +794,32 @@ public function updateImdbRating(int $id, ?float $imdbRating, ?int $imdbRatingVo 'imdb_rating_average' => $imdbRating, 'imdb_rating_vote_count' => $imdbRatingVoteCount, 'updated_at_imdb' => (string)DateTime::create(), + 'updated_at' => (string)DateTime::create(), ], ['id' => $id]); } public function updateLetterboxdId(int $id, string $letterboxdId) : void { - $this->dbConnection->update('movie', ['letterboxd_id' => $letterboxdId], ['id' => $id]); + $this->dbConnection->update('movie', ['letterboxd_id' => $letterboxdId, 'updated_at' => (string)DateTime::create()], ['id' => $id]); } public function updateTraktId(int $id, TraktId $traktId) : void { - $this->dbConnection->update('movie', ['trakt_id' => $traktId->asInt()], ['id' => $id]); + $this->dbConnection->update('movie', ['trakt_id' => $traktId->asInt(), 'updated_at' => (string)DateTime::create()], ['id' => $id]); } - public function updateUserRating(int $id, int $userId, PersonalRating $personalRating) : void + public function updateUserRating(int $movieId, int $userId, PersonalRating $personalRating) : int { - $this->dbConnection->executeQuery( - 'INSERT INTO movie_user_rating (movie_id, user_id, rating) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE rating=?', - [$id, $userId, $personalRating, $personalRating], + return (int)$this->dbConnection->update( + 'movie_user_rating', + [ + 'rating' => $personalRating->asInt() + ], + [ + 'movie_id' => $movieId, + 'user_id' => $userId, + 'updated_at' => (string)DateTime::create(), + ], ); } diff --git a/src/Domain/Person/PersonApi.php b/src/Domain/Person/PersonApi.php index c422fc83..314e1189 100644 --- a/src/Domain/Person/PersonApi.php +++ b/src/Domain/Person/PersonApi.php @@ -24,7 +24,17 @@ public function create( ?string $placeOfBirth = null, ?DateTime $updatedAtTmdb = null, ) : PersonEntity { - return $this->repository->create($tmdbId, $name, $gender, $knownForDepartment, $tmdbPosterPath, $birthDate, $deathDate, $placeOfBirth, $updatedAtTmdb); + return $this->repository->create( + $tmdbId, + $name, + $gender, + $knownForDepartment, + $tmdbPosterPath, + $birthDate, + $deathDate, + $placeOfBirth, + $updatedAtTmdb, + ); } public function createOrUpdatePersonByTmdbId( @@ -96,6 +106,17 @@ public function update( ?string $placeOfBirth = null, ?DateTime $updatedAtTmdb = null, ) : PersonEntity { - return $this->repository->update($id, $tmdbId, $name, $gender, $knownForDepartment, $tmdbPosterPath, $birthDate, $deathDate, $placeOfBirth, $updatedAtTmdb); + return $this->repository->update( + $id, + $tmdbId, + $name, + $gender, + $knownForDepartment, + $tmdbPosterPath, + $birthDate, + $deathDate, + $placeOfBirth, + $updatedAtTmdb, + ); } } diff --git a/src/Domain/Person/PersonRepository.php b/src/Domain/Person/PersonRepository.php index 70f3fcd4..0d62248f 100644 --- a/src/Domain/Person/PersonRepository.php +++ b/src/Domain/Person/PersonRepository.php @@ -37,6 +37,7 @@ public function create( 'death_date' => $deathDate === null ? null : (string)$deathDate, 'place_of_birth' => $placeOfBirth, 'updated_at_tmdb' => $updatedAtTmdb === null ? null : (string)$updatedAtTmdb, + 'created_at' => (string)DateTime::create(), ], ); @@ -101,22 +102,24 @@ public function update( ?string $placeOfBirth = null, ?DateTime $updatedAtTmdb = null, ) : PersonEntity { + $payload = [ + 'name' => $name, + 'gender' => $gender->asInt(), + 'known_for_department' => $knownForDepartment, + 'tmdb_id' => $tmdbId, + 'tmdb_poster_path' => $tmdbPosterPath, + 'birth_date' => $birthDate === null ? null : (string)$birthDate, + 'death_date' => $deathDate === null ? null : (string)$deathDate, + 'place_of_birth' => $placeOfBirth, + 'updated_at' => (string)DateTime::create(), + ]; + + if ($updatedAtTmdb !== null) { + $payload['updated_at_tmdb'] = (string)$updatedAtTmdb; + } + $this->dbConnection->update( - 'person', - [ - 'name' => $name, - 'gender' => $gender->asInt(), - 'known_for_department' => $knownForDepartment, - 'tmdb_id' => $tmdbId, - 'tmdb_poster_path' => $tmdbPosterPath, - 'birth_date' => $birthDate === null ? null : (string)$birthDate, - 'death_date' => $deathDate === null ? null : (string)$deathDate, - 'place_of_birth' => $placeOfBirth, - 'updated_at_tmdb' => $updatedAtTmdb === null ? null : (string)$updatedAtTmdb, - ], - [ - 'id' => $id, - ], + 'person', $payload, ['id' => $id], ); return $this->fetchById($id); diff --git a/src/Domain/User/UserRepository.php b/src/Domain/User/UserRepository.php index 969d142f..4916730a 100644 --- a/src/Domain/User/UserRepository.php +++ b/src/Domain/User/UserRepository.php @@ -20,6 +20,7 @@ public function createAuthToken(int $userId, string $token, DateTime $expiration 'user_id' => $userId, 'token' => $token, 'expiration_date' => (string)$expirationDate, + 'created_at' => (string)DateTime::create(), ], ); } @@ -32,6 +33,7 @@ public function createUser(string $email, string $passwordHash, string $name) : 'email' => $email, 'password' => $passwordHash, 'name' => $name, + 'created_at' => (string)DateTime::create(), ], ); } diff --git a/src/Factory.php b/src/Factory.php index 7b55453e..34a03a77 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -35,10 +35,27 @@ use Psr\Container\ContainerInterface; use Psr\Http\Client\ClientInterface; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Twig; class Factory { + private const DEFAULT_MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING = 15; + + private const DEFAULT_DATABASE_MYSQL_CHARSET = 'utf8mb4'; + + private const DEFAULT_DATABASE_MYSQL_PORT = 3306; + + private const DEFAULT_LOG_LEVEL = LogLevel::WARNING; + + private const DEFAULT_APPLICATION_VERSION = null; + + private const DEFAULT_TMDB_IMAGE_CACHING = false; + + private const DEFAULT_LOG_ENABLE_STACKTRACE = false; + + private const DEFAULT_ENABLE_FILE_LOGGING = false; + public static function createConfig() : Config { $dotenv = Dotenv::createMutable(__DIR__ . '/..'); @@ -78,17 +95,26 @@ public static function createDatabaseMigrationStatusCommand(ContainerInterface $ public static function createDbConnection(Config $config) : DBAL\Connection { - return DBAL\DriverManager::getConnection( - [ - 'charset' => $config->getAsString('DATABASE_CHARSET'), - 'dbname' => $config->getAsString('DATABASE_NAME'), - 'port' => $config->getAsInt('DATABASE_PORT'), - 'user' => $config->getAsString('DATABASE_USER'), - 'password' => $config->getAsString('DATABASE_PASSWORD'), - 'host' => $config->getAsString('DATABASE_HOST'), - 'driver' => $config->getAsString('DATABASE_DRIVER'), + $databaseMode = self::getDatabaseMode($config); + + $config = match ($databaseMode) { + 'sqlite' => [ + 'driver' => 'sqlite3', + 'path' => __DIR__ . '/../' . $config->getAsString('DATABASE_SQLITE'), ], - ); + 'mysql' => [ + 'driver' => 'pdo_mysql', + 'host' => $config->getAsString('DATABASE_MYSQL_HOST'), + 'port' => self::getDatabaseMysqlPort($config), + 'dbname' => $config->getAsString('DATABASE_MYSQL_NAME'), + 'user' => $config->getAsString('DATABASE_MYSQL_USER'), + 'password' => $config->getAsString('DATABASE_MYSQL_PASSWORD'), + 'charset' => self::getDatabaseMysqlCharset($config), + ], + default => throw new \RuntimeException('Not supported database mode: ' . $databaseMode) + }; + + return DBAL\DriverManager::getConnection($config); } public static function createHttpClient() : ClientInterface @@ -110,22 +136,23 @@ public static function createImageCacheService(ContainerInterface $container) : public static function createJobQueueScheduler(ContainerInterface $container, Config $config) : JobQueueScheduler { - try { - $enableImageCaching = $config->getAsBool('TMDB_ENABLE_IMAGE_CACHING'); - } catch (OutOfBoundsException) { - $enableImageCaching = false; - } - return new JobQueueScheduler( $container->get(JobQueueApi::class), - $enableImageCaching + self::getTmdbEnabledImageCaching($config) ); } public static function createLineFormatter(Config $config) : LineFormatter { $formatter = new LineFormatter(LineFormatter::SIMPLE_FORMAT, LineFormatter::SIMPLE_DATE); - $formatter->includeStacktraces($config->getAsBool('LOG_ENABLE_STACKTRACE')); + + try { + $enableStackTrace = $config->getAsBool('LOG_ENABLE_STACKTRACE'); + } catch (OutOfBoundsException) { + $enableStackTrace = self::DEFAULT_LOG_ENABLE_STACKTRACE; + } + + $formatter->includeStacktraces($enableStackTrace); return $formatter; } @@ -136,7 +163,13 @@ public static function createLogger(ContainerInterface $container, Config $confi $logger->pushHandler(self::createLoggerStreamHandlerStdout($container, $config)); - if ($config->getAsBool('LOG_ENABLE_FILE_LOGGING') === true) { + try { + $enableFileLogging = $config->getAsBool('LOG_ENABLE_FILE_LOGGING'); + } catch (OutOfBoundsException) { + $enableFileLogging = self::DEFAULT_ENABLE_FILE_LOGGING; + } + + if ($enableFileLogging === true) { $logger->pushHandler(self::createLoggerStreamHandlerFile($container, $config)); } @@ -148,7 +181,7 @@ public static function createSettingsController(ContainerInterface $container, C try { $applicationVersion = $config->getAsString('APPLICATION_VERSION'); } catch (OutOfBoundsException) { - $applicationVersion = null; + $applicationVersion = self::DEFAULT_APPLICATION_VERSION; } return new SettingsController( @@ -219,24 +252,33 @@ public static function createTwigFilesystemLoader() : Twig\Loader\FilesystemLoad public static function createUrlGenerator(ContainerInterface $container, Config $config) : UrlGenerator { - try { - $enableImageCaching = $config->getAsBool('TMDB_ENABLE_IMAGE_CACHING'); - } catch (OutOfBoundsException) { - $enableImageCaching = false; - } - return new UrlGenerator( $container->get(TmdbUrlGenerator::class), $container->get(ImageCacheService::class), - $enableImageCaching + self::getTmdbEnabledImageCaching($config) ); } + public static function getDatabaseMode(Config $config) : string + { + return $config->getAsString('DATABASE_MODE'); + } + + public static function getDatabaseMysqlCharset(mixed $config) : string + { + return $config->getAsString('DATABASE_MYSQL_CHARSET', self::DEFAULT_DATABASE_MYSQL_CHARSET); + } + + public static function getDatabaseMysqlPort(Config $config) : int + { + return $config->getAsInt('DATABASE_MYSQL_PORT', self::DEFAULT_DATABASE_MYSQL_PORT); + } + private static function createLoggerStreamHandlerFile(ContainerInterface $container, Config $config) : StreamHandler { $streamHandler = new StreamHandler( __DIR__ . '/../tmp/app.log', - $config->getAsString('LOG_LEVEL') + self::getLogLevel($config) ); $streamHandler->setFormatter($container->get(LineFormatter::class)); @@ -245,19 +287,25 @@ private static function createLoggerStreamHandlerFile(ContainerInterface $contai private static function createLoggerStreamHandlerStdout(ContainerInterface $container, Config $config) : StreamHandler { - $streamHandler = new StreamHandler('php://stdout', $config->getAsString('LOG_LEVEL')); + $streamHandler = new StreamHandler('php://stdout', self::getLogLevel($config)); $streamHandler->setFormatter($container->get(LineFormatter::class)); return $streamHandler; } + private static function getLogLevel(Config $config) : string + { + return $config->getAsString('LOG_LEVEL', self::DEFAULT_LOG_LEVEL); + } + + private static function getTmdbEnabledImageCaching(Config $config) : bool + { + return $config->getAsBool('TMDB_ENABLE_IMAGE_CACHING', self::DEFAULT_TMDB_IMAGE_CACHING); + } + public function createProcessJobCommand(ContainerInterface $container, Config $config) : Command\ProcessJobs { - try { - $minRuntimeInSeconds = $config->getAsInt('MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING'); - } catch (OutOfBoundsException) { - $minRuntimeInSeconds = null; - } + $minRuntimeInSeconds = $config->getAsInt('MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING', self::DEFAULT_MIN_RUNTIME_IN_SECONDS_FOR_JOB_PROCESSING); return new Command\ProcessJobs( $container->get(JobQueueApi::class), diff --git a/src/JobQueue/JobQueueRepository.php b/src/JobQueue/JobQueueRepository.php index 6ca511bc..dc0d52aa 100644 --- a/src/JobQueue/JobQueueRepository.php +++ b/src/JobQueue/JobQueueRepository.php @@ -24,6 +24,7 @@ public function addJob(JobType $type, JobStatus $status, ?int $userId = null, ?a 'job_status' => $status, 'user_id' => $userId, 'parameters' => $parameters !== null ? Json::encode($parameters) : null, + 'created_at' => (string)DateTime::create(), ], ); @@ -101,6 +102,15 @@ public function purgeProcessedJobs() : void public function updateJobStatus(int $id, JobStatus $status) : void { - $this->dbConnection->update('job_queue', ['job_status' => (string)$status], ['id' => $id]); + $this->dbConnection->update( + 'job_queue', + [ + 'job_status' => (string)$status, + 'updated_at' => (string)DateTime::create(), + ], + [ + 'id' => $id + ], + ); } } diff --git a/src/ValueObject/Config.php b/src/ValueObject/Config.php index d6379ad1..95da71c0 100644 --- a/src/ValueObject/Config.php +++ b/src/ValueObject/Config.php @@ -10,37 +10,51 @@ public function __construct(private readonly array $config) { } - public static function createFromEnv() : self + public static function createFromEnv(array $additionalData = []) : self { $fpmEnvironment = $_ENV; $systemEnvironment = getenv(); - return new self(array_merge($fpmEnvironment, $systemEnvironment)); + return new self(array_merge($fpmEnvironment, $systemEnvironment, $additionalData)); } - public function getAsArray(string $parameter) : array + public function getAsBool(string $parameter, ?bool $fallbackValue = null) : bool { - return (array)$this->get($parameter); - } + try { + return (bool)$this->get($parameter); + } catch (OutOfBoundsException $e) { + if ($fallbackValue === null) { + throw $e; + } - public function getAsBool(string $parameter) : bool - { - return (bool)$this->get($parameter); + return $fallbackValue; + } } - public function getAsFloat(string $parameter) : float + public function getAsInt(string $parameter, ?int $fallbackValue = null) : int { - return (float)$this->get($parameter); - } + try { + return (int)$this->get($parameter); + } catch (OutOfBoundsException $e) { + if ($fallbackValue === null) { + throw $e; + } - public function getAsInt(string $parameter) : int - { - return (int)$this->get($parameter); + return $fallbackValue; + } } - public function getAsString(string $parameter) : string + public function getAsString(string $parameter, ?string $fallbackValue = null) : string { - return (string)$this->get($parameter); + try { + return (string)$this->get($parameter); + } catch (OutOfBoundsException $e) { + if ($fallbackValue === null) { + throw $e; + } + + return $fallbackValue; + } } private function ensureKeyExists(string $key) : void diff --git a/tests/unit/ValueObject/ConfigTest.php b/tests/unit/ValueObject/ConfigTest.php new file mode 100644 index 00000000..dc794227 --- /dev/null +++ b/tests/unit/ValueObject/ConfigTest.php @@ -0,0 +1,53 @@ +subject = Config::createFromEnv( + [ + 'string_test' => 'value', + 'int_test' => 2, + 'bool_test' => true, + ], + ); + } + + public function testGetAsBool() : void + { + self::assertSame(true, $this->subject->getAsBool('bool_test')); + self::assertSame(false, $this->subject->getAsBool('bool_test_not_existing', false)); + + $this->expectException(\OutOfBoundsException::class); + $this->expectExceptionMessage('Key does not exist: bool_test_not_existing'); + $this->subject->getAsBool('bool_test_not_existing'); + } + + public function testGetAsInt() : void + { + self::assertSame(2, $this->subject->getAsInt('int_test')); + self::assertSame(3, $this->subject->getAsInt('int_test_not_existing', 3)); + + $this->expectException(\OutOfBoundsException::class); + $this->expectExceptionMessage('Key does not exist: int_test_not_existing'); + $this->subject->getAsBool('int_test_not_existing'); + } + + public function testGetAsString() : void + { + self::assertSame('value', $this->subject->getAsString('string_test')); + self::assertSame('fallback', $this->subject->getAsString('string_test_not_existing', 'fallback')); + + $this->expectException(\OutOfBoundsException::class); + $this->expectExceptionMessage('Key does not exist: string_test_not_existing'); + $this->subject->getAsBool('string_test_not_existing'); + } +}