update mapper.yaml spec

This commit is contained in:
arabcoders
2025-04-05 19:03:18 +03:00
parent ad2f7b389e
commit 45017b248e
6 changed files with 233 additions and 174 deletions

59
FAQ.md
View File

@@ -236,35 +236,42 @@ the [mapper.yaml](#whats-the-schema-for-the-mapperyaml-file) section.
The schema is simple, it's a list of users in the following format:
```yaml
# 1st user...
- my_plex_server:
name: "mike_jones"
options: { }
my_jellyfin_server:
name: "jones_mike"
options: { }
my_emby_server:
name: "mikeJones"
options: { }
# 2nd user...
- my_emby_server:
name: "jiji_jones"
options: { }
my_plex_server:
name: "jones_jiji"
options: { }
my_jellyfin_server:
name: "jijiJones"
options: { }
#.... more users
version: "1.5"
mapping:
# 1st user...
- my_plex_server:
name: "mike_jones"
options: { } # optional key.
my_jellyfin_server:
name: "jones_mike"
my_emby_server:
name: "mikeJones"
replace_with: "mike_jones" # optional action, to replace the username with the new one.
# 2nd user...
- my_emby_server:
name: "jiji_jones"
options: { } # optional key.
my_plex_server:
name: "jones_jiji"
my_jellyfin_server:
name: "jijiJones"
replace_with: "jiji_jones" # optional action, to replace the username with the new one.
#.... more users
```
> ![NOTE]
> the server names `my_plex_server`, `my_jellyfin_server`, `my_emby_server` are the names you have chosen for your
> backends.
> the `name` is the real username for that user from the backend.
> [!IMPORTANT]
> As we enforce specific naming convention for backend names and usernames they must follow the following format
> `^[a-z_0-9]+$` which means, lowercase letters, numbers and `_` are allowed. No spaces, uppercase letters or special
> characters are allowed. you can use the `replace_with` key to replace the username with the new one. if it's not
> complying with the naming convention, or you want to force specific display name.
This yaml file helps map your users username in the different backends, so the tool can sync the correct user data. If
> ![NOTE]
> the backend names `my_plex_server`, `my_jellyfin_server`, `my_emby_server` are the names you have chosen for
> your> backends.
>
> The `name` field is whatever the backend is reporting the username as.
This yaml file helps map your users usernames in the different backends, so the tool can sync the correct user data. If
you added or updated mapping, you should delete `users` directory and generate new data. by running the `backend:create`
command as described in the previous section.

14
composer.lock generated
View File

@@ -3580,12 +3580,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "0b51a6c830ba6dd6c63715ceded239a62bf2274f"
"reference": "975c081c7e430d0316a94047e5d8ab26e0a8f49e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0b51a6c830ba6dd6c63715ceded239a62bf2274f",
"reference": "0b51a6c830ba6dd6c63715ceded239a62bf2274f",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/975c081c7e430d0316a94047e5d8ab26e0a8f49e",
"reference": "975c081c7e430d0316a94047e5d8ab26e0a8f49e",
"shasum": ""
},
"conflict": {
@@ -3614,7 +3614,8 @@
"andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5",
"apache-solr-for-typo3/solr": "<2.8.3",
"apereo/phpcas": "<1.6",
"api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3|>=3.3.8,<3.3.15",
"api-platform/core": "<4.0.22",
"api-platform/graphql": "<4.0.22",
"appwrite/server-ce": "<=1.2.1",
"arc/web": "<3",
"area17/twill": "<1.2.5|>=2,<2.5.3",
@@ -3688,7 +3689,7 @@
"codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9",
"components/jquery": ">=1.0.3,<3.5",
"composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7",
"concrete5/concrete5": "<9.4.0.0-RC1-dev",
"concrete5/concrete5": "<9.4.0.0-RC2-dev",
"concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
"contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4",
@@ -3746,6 +3747,7 @@
"drupal/matomo": "<1.24",
"drupal/oauth2_client": "<4.1.3",
"drupal/oauth2_server": "<2.1",
"drupal/obfuscate": "<2.0.1",
"drupal/rapidoc_elements_field_formatter": "<1.0.1",
"drupal/spamspan": "<3.2.1",
"drupal/tfa": "<1.10",
@@ -4468,7 +4470,7 @@
"type": "tidelift"
}
],
"time": "2025-04-02T18:06:24+00:00"
"time": "2025-04-04T15:05:17+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@@ -17,7 +17,7 @@
"@xterm/xterm": "^5.5.0",
"cronstrue": "^2.57.0",
"floating-vue": "^5.2.2",
"hls.js": "^1.6.0",
"hls.js": "^1.6.1",
"marked": "^15.0.7",
"marked-base-url": "^1.1.3",
"moment": "^2.30.1",

View File

@@ -531,11 +531,11 @@
fastq "^1.6.0"
"@nuxt/cli@^3.24.0":
version "3.24.0"
resolved "https://registry.yarnpkg.com/@nuxt/cli/-/cli-3.24.0.tgz#084e3be55e4abd22c7e1c5e07b4bf3c61a7e49fe"
integrity sha512-D/rCodMHecznWZAxsb81lKSMhCcWIUI6TyEDQ3WIl2bTNBn4WvIYrZP3rIPJn7X4A+/CG5H8yhKDVP6Mq7XopA==
version "3.24.1"
resolved "https://registry.yarnpkg.com/@nuxt/cli/-/cli-3.24.1.tgz#7a864e3133506859254547dc221fab2f11b852a6"
integrity sha512-dWoB3gZj2H04x58QWNWpshQUxjsf0TB6Ppy7YKswS5hGtQkOlQ5k85f133+Bg50TJqzNuZ3OUMRduftppdJjrg==
dependencies:
c12 "^3.0.2"
c12 "^3.0.3"
chokidar "^4.0.3"
citty "^0.1.6"
clipboardy "^4.0.0"
@@ -1593,22 +1593,22 @@ bundle-name@^4.1.0:
dependencies:
run-applescript "^7.0.0"
c12@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/c12/-/c12-3.0.2.tgz#5ceba55cf081ff4cf95b22c593c80b34cf1f3d2e"
integrity sha512-6Tzk1/TNeI3WBPpK0j/Ss4+gPj3PUJYbWl/MWDJBThFvwNGNkXtd7Cz8BJtD4aRwoGHtzQD0SnxamgUiBH0/Nw==
c12@^3.0.2, c12@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/c12/-/c12-3.0.3.tgz#4d6d4d35f084606ff616d1bcae60e6676eacd4bd"
integrity sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==
dependencies:
chokidar "^4.0.3"
confbox "^0.1.8"
confbox "^0.2.2"
defu "^6.1.4"
dotenv "^16.4.7"
exsolve "^1.0.0"
exsolve "^1.0.4"
giget "^2.0.0"
jiti "^2.4.2"
ohash "^2.0.5"
ohash "^2.0.11"
pathe "^2.0.3"
perfect-debounce "^1.0.0"
pkg-types "^2.0.0"
pkg-types "^2.1.0"
rc9 "^2.1.2"
cac@^6.7.14:
@@ -1627,9 +1627,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
version "1.0.30001707"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz#c5e104d199e6f4355a898fcd995a066c7eb9bf41"
integrity sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==
version "1.0.30001711"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001711.tgz#8f9e7c5ec8a027f88e00462f796ef1fd6c63f734"
integrity sha512-OpFA8GsKtoV3lCcsI3U5XBAV+oVrMu96OS8XafKqnhOaEAW2mveD1Mx81Sx/02chERwhDakuXs28zbyEc4QMKg==
chokidar@^4.0.3:
version "4.0.3"
@@ -1726,10 +1726,10 @@ confbox@^0.1.8:
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
confbox@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.1.tgz#ae39f2c99699afa451d00206479f15f9a1208a8b"
integrity sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg==
confbox@^0.2.1, confbox@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110"
integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==
consola@^3.2.3, consola@^3.4.0, consola@^3.4.2:
version "3.4.2"
@@ -1983,9 +1983,9 @@ depd@2.0.0:
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destr@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449"
integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==
version "2.0.5"
resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb"
integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==
destroy@1.2.0:
version "1.2.0"
@@ -2070,9 +2070,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.73:
version "1.5.129"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz#fafa835aea5d15fcd5cbe9bd6bf1cb5d4b3aa06e"
integrity sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==
version "1.5.132"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.132.tgz#081b8086d7cecc58732f7cc1f1c19306c5510c5f"
integrity sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -2210,7 +2210,7 @@ execa@^8.0.1:
signal-exit "^4.1.0"
strip-final-newline "^3.0.0"
exsolve@^1.0.0, exsolve@^1.0.1, exsolve@^1.0.4:
exsolve@^1.0.1, exsolve@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.4.tgz#7de5c75af82ecd15998328fbf5f2295883be3a39"
integrity sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==
@@ -2344,9 +2344,9 @@ giget@^2.0.0:
pathe "^2.0.3"
git-up@^8.0.0:
version "8.0.1"
resolved "https://registry.yarnpkg.com/git-up/-/git-up-8.0.1.tgz#2a82cfbc77b5eb04074ab1e48593911981654fc7"
integrity sha512-2XFu1uNZMSjkyetaF+8rqn6P0XqpMq/C+2ycjI6YwrIKcszZ5/WR4UubxjN0lILOKqLkLaHDaCr2B6fP1cke6g==
version "8.1.0"
resolved "https://registry.yarnpkg.com/git-up/-/git-up-8.1.0.tgz#1494a4b2b20a8cdef1b32539f12cb546a199fc5e"
integrity sha512-cT2f5ERrhFDMPS5wLHURcjRiacC8HonX0zIAWBTwHv1fS6HheP902l6pefOX/H9lNmvCHDwomw0VeN7nhg5bxg==
dependencies:
is-ssh "^1.4.0"
parse-url "^9.2.0"
@@ -2435,10 +2435,10 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
hls.js@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.0.tgz#ec0118f1dcd0fce536e52c27f674cdf944055b30"
integrity sha512-AlW8ymcDKZuKtzXCUmEy4nOcHRkebnShH6t6hC2+QJQP0WXlTUSSO9Kp22uSEYdCgpwkXEJsfOhqxrgO2tDctQ==
hls.js@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.1.tgz#2a808924d90465e44db8bf29dde31de8e4d313cf"
integrity sha512-7GOkcqn0Y9EqU2OJZlzkwxj9Uynuln7URvr7dRjgqNJNZ5UbbjL/v1BjAvQogy57Psdd/ek1u2s6IDEFYlabrA==
hookable@^5.5.3:
version "5.5.3"
@@ -2885,9 +2885,9 @@ mime@^3.0.0:
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
mime@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.6.tgz#ca83bec0bcf2a02353d0e02da99be05603d04839"
integrity sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==
version "4.0.7"
resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.7.tgz#0b7a98b08c63bd3c10251e797d67840c9bde9f13"
integrity sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==
mimic-fn@^4.0.0:
version "4.0.0"
@@ -3227,7 +3227,7 @@ ofetch@^1.4.1:
node-fetch-native "^1.6.4"
ufo "^1.5.4"
ohash@^2.0.11, ohash@^2.0.4, ohash@^2.0.5:
ohash@^2.0.11, ohash@^2.0.4:
version "2.0.11"
resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b"
integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==
@@ -3997,9 +3997,9 @@ statuses@2.0.1:
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
std-env@^3.7.0, std-env@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.1.tgz#2b81c631c62e3d0b964b87f099b8dcab6c9a5346"
integrity sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==
version "3.9.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
streamx@^2.15.0:
version "2.22.0"
@@ -4227,9 +4227,9 @@ tslib@^2.4.0:
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
type-fest@^4.18.2:
version "4.39.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.0.tgz#c7758be50a83a5b879e7a59ea52421e9816b3928"
integrity sha512-w2IGJU1tIgcrepg9ZJ82d8UmItNQtOFJG0HCUE3SzMokKkTsruVDALl2fAdiEzJlfduoU+VyXJWIIUZ+6jV+nw==
version "4.39.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.1.tgz#7521f6944e279abaf79cf60cfbc4823f4858083e"
integrity sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==
ufo@^1.1.2, ufo@^1.5.4:
version "1.5.4"
@@ -4237,9 +4237,9 @@ ufo@^1.1.2, ufo@^1.5.4:
integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==
ultrahtml@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/ultrahtml/-/ultrahtml-1.5.3.tgz#e7a903a4b28a0e49b71b0801b444050bb0a369c7"
integrity sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==
version "1.6.0"
resolved "https://registry.yarnpkg.com/ultrahtml/-/ultrahtml-1.6.0.tgz#0d1aad7bbfeae512438d30e799c11622127a1ac8"
integrity sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==
uncrypto@^0.1.3:
version "0.1.3"
@@ -4484,9 +4484,9 @@ vite-plugin-vue-tracer@^0.1.3:
source-map-js "^1.2.1"
"vite@^5.0.0 || ^6.0.0", vite@^6.2.4:
version "6.2.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.4.tgz#05809de3f918fded87f73a838761995a4d66a680"
integrity sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==
version "6.2.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.5.tgz#d093b5fe8eb96e594761584a966ab13f24457820"
integrity sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==
dependencies:
esbuild "^0.25.0"
postcss "^8.5.3"

View File

@@ -43,6 +43,7 @@ class CreateUsersCommand extends Command
{
$this->setName(self::ROUTE)
->addOption('regenerate-tokens', 'g', InputOption::VALUE_NONE, 'Generate new tokens for PLEX users.')
->addOption('--dry-run', null, InputOption::VALUE_NONE, 'Do not commit any changes.')
->addOption(
'update',
'u',
@@ -125,6 +126,12 @@ class CreateUsersCommand extends Command
*/
protected function runCommand(iInput $input, iOutput $output): int
{
$dryRun = $input->getOption('dry-run');
if ($dryRun) {
$this->logger->notice('SYSTEM: Running in dry-run mode. No changes will be made.');
}
$supported = Config::get('supported', []);
$configFile = ConfigFile::open(Config::get('backends_file'), 'yaml');
$configFile->setLogger($this->logger);
@@ -134,7 +141,12 @@ class CreateUsersCommand extends Command
if (file_exists($mapFile) && filesize($mapFile) > 10) {
$map = ConfigFile::open(Config::get('mapper_file'), 'yaml');
$mapping = $map->getAll();
$mapping = $map->get('map', $map->getAll());
if (false === $map->has('version') || false === $map->has('map')) {
$this->logger->warning(
"SYSTEM: UPGRADE your mapper.yaml file to v1.5 format spec, check the FAQ.md for the updated format. Th old format will be removed in future releases.",
);
}
}
$backends = [];
@@ -187,6 +199,9 @@ class CreateUsersCommand extends Command
foreach ($client->getUsersList() as $user) {
/** @var array $info */
$info = $backend;
$user = $this->map_actions($user, ag($backend, 'name'), $mapping);
$info['user'] = ag($user, 'id', ag($info, 'user'));
$info['backendName'] = strtolower(r("{backend}_{user}", [
'backend' => ag($backend, 'name'),
@@ -206,7 +221,7 @@ class CreateUsersCommand extends Command
if (false === isValidName($info['displayName'])) {
$rename = substr(md5($info['displayName']), 0, 8);
$this->logger->error(
message: "SYSTEM: Renaming invalid username '{name}'. username must be in [a-z_0-9], renaming to '{renamed}'",
message: "SYSTEM: Renaming invalid display username '{name}'. username must be in [a-z_0-9], renaming to '{renamed}'",
context: ['name' => $info['displayName'], 'renamed' => $rename]
);
$info['displayName'] = $rename;
@@ -262,7 +277,7 @@ class CreateUsersCommand extends Command
}
}
$users = $this->generate_users_list($users, $mapping);
$users = $this->generate_users_list($users);
if (count($users) < 1) {
$this->logger->warning('No users were found.');
@@ -274,7 +289,7 @@ class CreateUsersCommand extends Command
]);
foreach ($users as $user) {
$userName = strtolower(ag($user, 'name', 'Unknown'));
$userName = strtolower(ag($user, 'name', 'unknown'));
if (false === isValidName($userName)) {
$rename = substr(md5($userName), 0, 8);
$this->logger->error(
@@ -292,7 +307,7 @@ class CreateUsersCommand extends Command
'path' => $subUserPath
]);
if (false === mkdir($subUserPath, 0755, true)) {
if (false === $dryRun && false === mkdir($subUserPath, 0755, true)) {
$this->logger->error("SYSTEM: Failed to create '{user}' directory '{path}'.", [
'user' => $userName,
'path' => $subUserPath
@@ -307,7 +322,14 @@ class CreateUsersCommand extends Command
'file' => $config_file
]);
$perUser = ConfigFile::open($config_file, 'yaml', autoCreate: true);
$perUser = ConfigFile::open(
file: $dryRun ? fopen("php://memory", 'a') : $config_file,
type: 'yaml',
autoSave: !$dryRun,
autoCreate: !$dryRun,
autoBackup: !$dryRun
);
$perUser->setLogger($this->logger);
$regenerateTokens = $input->getOption('regenerate-tokens');
@@ -432,10 +454,14 @@ class CreateUsersCommand extends Command
'user' => $userName,
'db' => $dbFile
]);
perUserDb($userName);
if (false === $dryRun) {
perUserDb($userName);
}
}
$perUser->persist();
if (false === $dryRun) {
$perUser->persist();
}
}
return self::SUCCESS;
@@ -445,11 +471,10 @@ class CreateUsersCommand extends Command
* Generate a list of users that are matched across all backends.
*
* @param array $users The list of users from all backends.
* @param array{string: array{string: string, options: array}} $map The map of users to match.
*
* @return array{name: string, backends: array<string, array<string, mixed>>}[] The list of matched users.
*/
private function generate_users_list(array $users, array $map = []): array
private function generate_users_list(array $users): array
{
$allBackends = [];
foreach ($users as $u) {
@@ -464,7 +489,10 @@ class CreateUsersCommand extends Command
$backend = $user['backend'];
$nameLower = strtolower($user['name']);
if (ag($user, 'id') === ag($user, 'client_data.options.' . Options::ALT_ID)) {
$this->logger->debug('Skipping main user "{name}".', ['name' => $user['name']]);
$this->logger->debug('Skipping main user "{backend}: {name}".', [
'name' => $user['name'],
'backend' => $user['backend'],
]);
continue;
}
if (!isset($usersBy[$backend])) {
@@ -570,80 +598,6 @@ class CreateUsersCommand extends Command
continue;
}
// Map-based matching first
$matchedMapEntry = null;
foreach ($map as $mapRow) {
if (isset($mapRow[$backend]['name']) && strtolower($mapRow[$backend]['name']) === $nameLower) {
$matchedMapEntry = $mapRow;
break;
}
}
if ($matchedMapEntry) {
// Build mapMatch from the map row.
$mapMatch = [$backend => $userObj];
// Gather all the other backends from the map
foreach ($allBackends as $otherBackend) {
if ($otherBackend === $backend) {
continue;
}
if (isset($matchedMapEntry[$otherBackend]['name'])) {
$mappedNameLower = strtolower($matchedMapEntry[$otherBackend]['name']);
if (isset($usersBy[$otherBackend][$mappedNameLower])) {
$mapMatch[$otherBackend] = $usersBy[$otherBackend][$mappedNameLower];
}
}
}
// If we matched ≥ 2 backends, unify them
if (count($mapMatch) >= 2) {
// --- MERGE map-based "options" into client_data => options, if any ---
foreach ($mapMatch as $b => &$matchedUser) {
// If the map entry has an 'options' array for this backend,
// merge it into $matchedUser['client_data']['options'].
if (isset($matchedMapEntry[$b]['options']) && is_array($matchedMapEntry[$b]['options'])) {
$mapOptions = $matchedMapEntry[$b]['options'];
// Ensure $matchedUser['client_data'] is an array
if (!isset($matchedUser['client_data']) || !is_array($matchedUser['client_data'])) {
$matchedUser['client_data'] = [];
}
// Ensure $matchedUser['client_data']['options'] is an array
if (!isset($matchedUser['client_data']['options']) || !is_array(
$matchedUser['client_data']['options']
)) {
$matchedUser['client_data']['options'] = [];
}
// Merge the map's options
$matchedUser['client_data']['options'] = array_replace_recursive(
$matchedUser['client_data']['options'],
$mapOptions
);
}
}
unset($matchedUser); // break reference from the loop
// Build final row
$results[] = $buildUnifiedRow($mapMatch);
// Mark & remove from $usersBy
foreach ($mapMatch as $b => $mu) {
$nm = strtolower($mu['name']);
$used[] = [$b, $nm];
unset($usersBy[$b][$nm]);
}
continue;
} else {
$this->logger->error("No partial fallback match via map for '{backend}: {user}'", [
'backend' => $userObj['backend'],
'user' => $userObj['name'],
]);
}
}
// Direct-name matching if map fails
$directMatch = [$backend => $userObj];
foreach ($allBackends as $otherBackend) {
@@ -671,7 +625,7 @@ class CreateUsersCommand extends Command
}
// If neither map nor direct matched for ≥2
$this->logger->error("Cannot match user '{backend}: {user}' in any map row or direct match.", [
$this->logger->error("Direct mapping failed for '{backend}: {user}' no match found.", [
'backend' => $userObj['backend'],
'user' => $userObj['name']
]);
@@ -702,4 +656,83 @@ class CreateUsersCommand extends Command
return $chunks;
}
/**
* Run actions on the user data, early.
*
* @param array $user The remote user data.
* @param string $backend The backend name.
* @param array $mapping the mapper file data.
*
* @return array the modified user data if any.
*
* - my_plex_server:
* name: "mike_jones"
* options: { }
* my_jellyfin_server:
* name: "jones_mike"
* options: { }
* my_emby_server:
* name: "mikeJones"
* replace_with: "mike_jones"
* options: { }
* ```
*/
private function map_actions(array $user, string $backend, array $mapping): array
{
if (null === ($username = ag($user, 'name'))) {
return $user;
}
$found = false;
$user_map = [];
foreach ($mapping as $map) {
$map_backend = array_keys($map)[0];
if ($backend !== $map_backend) {
continue;
}
if (ag($map, "{$backend}.name") !== $username) {
continue;
}
$found = true;
$user_map = ag($map, $backend, []);
break;
}
if (false === $found) {
return $user;
}
// -- replace_with action.
if (null !== ($newUsername = ag($user_map, 'replace_with'))) {
if (!is_string($newUsername) || false === isValidName($newUsername)) {
$this->logger->error(
message: "SYSTEM: Mapper failed to rename '{backend}: {username}' to '{backend}: {new_username}' name must be in [a-z_0-9] format.",
context: [
'backend' => $backend,
'username' => $username,
'new_username' => $newUsername
]
);
return $user;
}
$this->logger->notice(
message: "SYSTEM: Mapper is renaming '{backend}: {username}' to '{backend}: {new_username}'.",
context: [
'backend' => $backend,
'username' => $username,
'new_username' => $newUsername
]
);
$user['name'] = $newUsername;
}
return $user;
}
}

View File

@@ -27,16 +27,17 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
/**
* ConfigFile constructor.
*
* @param string $file The config file.
* @param string|resource $file The config file.
* @param string $type The content type. Default to 'yaml'.
* @param bool $autoSave Auto save changes. Default to 'true'.
* @param bool $autoCreate Auto create the file if it does not exist. Default is 'false'.
* @param bool $autoBackup Auto backup the file. Default is 'true'.
* @param array $opts Additional options.
*
* @throws InvalidArgumentException If the content type is invalid.
*/
public function __construct(
private readonly string $file,
private readonly mixed $file,
private readonly string $type = 'yaml',
private readonly bool $autoSave = true,
private readonly bool $autoCreate = false,
@@ -50,7 +51,7 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
]));
}
if (false === file_exists($this->file)) {
if (false === is_resource($file) && false === file_exists($this->file)) {
if (false === $this->autoCreate) {
throw new InvalidArgumentException(r("File '{file}' does not exist.", ['file' => $file]));
}
@@ -62,8 +63,20 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
$this->loadData();
}
/**
* Open a config file.
*
* @param string|resource $file The config file.
* @param string $type The content type. Default to 'yaml'.
* @param bool $autoSave Auto save changes. Default to 'true'.
* @param bool $autoCreate Auto create the file if it does not exist. Default is 'false'.
* @param bool $autoBackup Auto backup the file. Default is 'true'.
* @param array $opts Additional options.
*
* @return self
*/
public static function open(
string $file,
mixed $file,
string $type = 'yaml',
bool $autoSave = true,
bool $autoCreate = false,
@@ -181,7 +194,7 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
$json_encode = $this->opts['json_encode'];
}
if (true === $this->autoBackup) {
if (true === $this->autoBackup && false === is_resource($this->file)) {
try {
copy($this->file, $this->file . '.bak');
} catch (\Exception) {
@@ -259,7 +272,7 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
$jsonOpts = $this->opts['json_decode'];
}
$stream = $stream ?? new Stream($this->file, 'r');
$stream = Stream::make($this->file, 'r');
if ($stream->isSeekable()) {
$stream->seek(0);
@@ -351,6 +364,10 @@ final class ConfigFile implements ArrayAccess, LoggerAwareInterface
private function getFileHash(): string
{
if (is_resource($this->file)) {
return hash('sha256', (string)Stream::make($this->file, 'r'));
}
return hash_file('sha256', $this->file);
}
}